diff --git a/app/actions/auth.ts b/app/actions/auth.ts index 2e7e3487b..315210ada 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -12,7 +12,11 @@ import { organizationsService } from "@/lib/services/organizations"; export async function getCreditBalance(): Promise { const user = await requireAuthWithOrg(); - const organization = await organizationsService.getById(user.organization_id!); + const organization = await organizationsService.getById( + user.organization_id!, + ); // Convert numeric type (string) to number for UI display - return organization?.credit_balance ? Number.parseFloat(String(organization.credit_balance)) : 0; + return organization?.credit_balance + ? Number.parseFloat(String(organization.credit_balance)) + : 0; } diff --git a/app/actions/characters.ts b/app/actions/characters.ts index 467c0770f..dff955e10 100644 --- a/app/actions/characters.ts +++ b/app/actions/characters.ts @@ -54,7 +54,10 @@ export async function createCharacter(elizaCharacter: ElizaCharacter) { const user = await requireAuthWithOrg(); // Normalize isPublic to ensure consistency between is_public column and character_data - const isPublic = typeof elizaCharacter.isPublic === "boolean" ? elizaCharacter.isPublic : false; + const isPublic = + typeof elizaCharacter.isPublic === "boolean" + ? elizaCharacter.isPublic + : false; const newCharacter: NewUserCharacter = { organization_id: user.organization_id!, @@ -63,7 +66,10 @@ export async function createCharacter(elizaCharacter: ElizaCharacter) { username: elizaCharacter.username ?? null, system: elizaCharacter.system ?? null, bio: elizaCharacter.bio, - message_examples: (elizaCharacter.messageExamples ?? []) as Record[][], + message_examples: (elizaCharacter.messageExamples ?? []) as Record< + string, + unknown + >[][], post_examples: elizaCharacter.postExamples ?? [], topics: elizaCharacter.topics ?? [], adjectives: elizaCharacter.adjectives ?? [], @@ -98,7 +104,9 @@ export async function createCharacter(elizaCharacter: ElizaCharacter) { userName: user.name || user.email || null, userId: user.id, organizationName: user.organization.name, - bio: Array.isArray(elizaCharacter.bio) ? elizaCharacter.bio.join(" ") : elizaCharacter.bio, + bio: Array.isArray(elizaCharacter.bio) + ? elizaCharacter.bio.join(" ") + : elizaCharacter.bio, plugins: elizaCharacter.plugins, }) .catch((error) => { @@ -117,7 +125,10 @@ export async function createCharacter(elizaCharacter: ElizaCharacter) { * @returns The updated character in Eliza format. * @throws If the character is not found or access is denied. */ -export async function updateCharacter(characterId: string, elizaCharacter: ElizaCharacter) { +export async function updateCharacter( + characterId: string, + elizaCharacter: ElizaCharacter, +) { const user = await requireAuthWithOrg(); const updates: Partial = { @@ -125,7 +136,10 @@ export async function updateCharacter(characterId: string, elizaCharacter: Eliza username: elizaCharacter.username ?? null, system: elizaCharacter.system ?? null, bio: elizaCharacter.bio, - message_examples: (elizaCharacter.messageExamples ?? []) as Record[][], + message_examples: (elizaCharacter.messageExamples ?? []) as Record< + string, + unknown + >[][], post_examples: elizaCharacter.postExamples ?? [], topics: elizaCharacter.topics ?? [], adjectives: elizaCharacter.adjectives ?? [], @@ -145,7 +159,11 @@ export async function updateCharacter(characterId: string, elizaCharacter: Eliza avatar_url: elizaCharacter.avatarUrl ?? null, }; - const character = await charactersService.updateForUser(characterId, user.id, updates); + const character = await charactersService.updateForUser( + characterId, + user.id, + updates, + ); if (!character) { throw new Error("Character not found or access denied"); @@ -200,7 +218,10 @@ export async function listCharacters() { export async function getCharacter(characterId: string) { const user = await requireAuthWithOrg(); - const character = await charactersService.getByIdForUser(characterId, user.id); + const character = await charactersService.getByIdForUser( + characterId, + user.id, + ); if (!character) { throw new Error("Character not found"); diff --git a/app/actions/gallery.ts b/app/actions/gallery.ts index c87a42d35..58858fe64 100644 --- a/app/actions/gallery.ts +++ b/app/actions/gallery.ts @@ -87,7 +87,10 @@ export async function deleteMedia(generationId: string): Promise { } // Delete from Vercel Blob if it's a blob URL - if (generation.storage_url && generation.storage_url.includes("blob.vercel-storage.com")) { + if ( + generation.storage_url && + generation.storage_url.includes("blob.vercel-storage.com") + ) { try { await deleteBlob(generation.storage_url); } catch (error) { @@ -115,7 +118,9 @@ export async function deleteMedia(generationId: string): Promise { * @param limit - Maximum number of images to return (default 20). * @returns Array of gallery items from random users. */ -export async function listExploreImages(limit: number = 20): Promise { +export async function listExploreImages( + limit: number = 20, +): Promise { const generations = await generationsService.listRandomPublicImages(limit); return generations.map((gen) => ({ @@ -152,9 +157,16 @@ export async function getUserMediaStats(): Promise<{ const userGenerations = generations.filter((gen) => gen.storage_url); - const totalImages = userGenerations.filter((gen) => gen.type === "image").length; - const totalVideos = userGenerations.filter((gen) => gen.type === "video").length; - const totalSize = userGenerations.reduce((acc, gen) => acc + Number(gen.file_size || 0), 0); + const totalImages = userGenerations.filter( + (gen) => gen.type === "image", + ).length; + const totalVideos = userGenerations.filter( + (gen) => gen.type === "video", + ).length; + const totalSize = userGenerations.reduce( + (acc, gen) => acc + Number(gen.file_size || 0), + 0, + ); return { totalImages, diff --git a/app/actions/users.ts b/app/actions/users.ts index 80f90930d..f0e79afa9 100644 --- a/app/actions/users.ts +++ b/app/actions/users.ts @@ -59,7 +59,10 @@ export async function updateProfile(formData: FormData) { return { success: false, - error: error instanceof Error ? error.message : "Failed to update profile. Please try again.", + error: + error instanceof Error + ? error.message + : "Failed to update profile. Please try again.", }; } } @@ -79,7 +82,8 @@ export async function updateEmail(formData: FormData) { if (user.email) { return { success: false, - error: "Email already set. Please contact support to change your email.", + error: + "Email already set. Please contact support to change your email.", }; } @@ -124,7 +128,10 @@ export async function updateEmail(formData: FormData) { return { success: false, - error: error instanceof Error ? error.message : "Failed to update email. Please try again.", + error: + error instanceof Error + ? error.message + : "Failed to update email. Please try again.", }; } } @@ -199,7 +206,10 @@ export async function uploadAvatar(formData: FormData) { return { success: false, - error: error instanceof Error ? error.message : "Failed to upload avatar. Please try again.", + error: + error instanceof Error + ? error.message + : "Failed to upload avatar. Please try again.", }; } } diff --git a/app/api/a2a/route.ts b/app/api/a2a/route.ts index f08220228..626b437f2 100644 --- a/app/api/a2a/route.ts +++ b/app/api/a2a/route.ts @@ -48,22 +48,29 @@ function a2aSuccess(result: T, id: string | number | null): NextResponse { } // Method registry -type MethodHandler = (params: Record, ctx: A2AContext) => Promise; - -const METHODS: Record = { - "message/send": { - handler: (params, ctx) => handleMessageSend(params as unknown as MessageSendParams, ctx), - description: "Send a message to create/continue a task (A2A standard)", - }, - "tasks/get": { - handler: (params, ctx) => handleTasksGet(params as unknown as TaskGetParams, ctx), - description: "Get task status and history (A2A standard)", - }, - "tasks/cancel": { - handler: (params, ctx) => handleTasksCancel(params as unknown as TaskCancelParams, ctx), - description: "Cancel a running task (A2A standard)", - }, -}; +type MethodHandler = ( + params: Record, + ctx: A2AContext, +) => Promise; + +const METHODS: Record = + { + "message/send": { + handler: (params, ctx) => + handleMessageSend(params as unknown as MessageSendParams, ctx), + description: "Send a message to create/continue a task (A2A standard)", + }, + "tasks/get": { + handler: (params, ctx) => + handleTasksGet(params as unknown as TaskGetParams, ctx), + description: "Get task status and history (A2A standard)", + }, + "tasks/cancel": { + handler: (params, ctx) => + handleTasksCancel(params as unknown as TaskCancelParams, ctx), + description: "Cancel a running task (A2A standard)", + }, + }; // Request schema const JsonRpcRequestSchema = z.object({ @@ -81,7 +88,11 @@ export async function POST(request: NextRequest) { try { body = JSON.parse(bodyText); } catch { - return a2aError(A2AErrorCodes.PARSE_ERROR, "Parse error: Invalid JSON", null); + return a2aError( + A2AErrorCodes.PARSE_ERROR, + "Parse error: Invalid JSON", + null, + ); } const parsed = JsonRpcRequestSchema.safeParse(body); @@ -121,7 +132,12 @@ export async function POST(request: NextRequest) { // Find handler const methodDef = METHODS[method]; if (!methodDef) { - return a2aError(A2AErrorCodes.METHOD_NOT_FOUND, `Method not found: ${method}`, id, 404); + return a2aError( + A2AErrorCodes.METHOD_NOT_FOUND, + `Method not found: ${method}`, + id, + 404, + ); } // Execute diff --git a/app/api/admin/redemptions/route.ts b/app/api/admin/redemptions/route.ts index 95ae1b1f5..71b207791 100644 --- a/app/api/admin/redemptions/route.ts +++ b/app/api/admin/redemptions/route.ts @@ -15,7 +15,10 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { dbRead } from "@/db/client"; import { apps } from "@/db/schemas/apps"; -import { type TokenRedemption, tokenRedemptions } from "@/db/schemas/token-redemptions"; +import { + type TokenRedemption, + tokenRedemptions, +} from "@/db/schemas/token-redemptions"; import { users } from "@/db/schemas/users"; import { requireAdmin } from "@/lib/auth"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; @@ -33,7 +36,9 @@ const AdminActionSchema = z.object({ * GET /api/admin/redemptions * List redemptions pending admin review. */ -async function listPendingRedemptionsHandler(request: NextRequest): Promise { +async function listPendingRedemptionsHandler( + request: NextRequest, +): Promise { const { user: adminUser } = await requireAdmin(request); const statusFilter = request.nextUrl.searchParams.get("status") || "pending"; @@ -187,7 +192,10 @@ async function adminActionHandler(request: NextRequest): Promise { ); if (!result.success) { - return NextResponse.json({ success: false, error: result.error }, { status: 400 }); + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 }, + ); } logger.info("[Admin Redemptions] Approved redemption", { @@ -208,7 +216,10 @@ async function adminActionHandler(request: NextRequest): Promise { ); if (!result.success) { - return NextResponse.json({ success: false, error: result.error }, { status: 400 }); + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 }, + ); } logger.info("[Admin Redemptions] Rejected redemption", { @@ -224,7 +235,10 @@ async function adminActionHandler(request: NextRequest): Promise { } } -export const GET = withRateLimit(listPendingRedemptionsHandler, RateLimitPresets.STANDARD); +export const GET = withRateLimit( + listPendingRedemptionsHandler, + RateLimitPresets.STANDARD, +); export const POST = withRateLimit(adminActionHandler, RateLimitPresets.STRICT); /** @@ -236,7 +250,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/affiliate/create-character/route.ts b/app/api/affiliate/create-character/route.ts index 020e216e1..1e847fe6b 100644 --- a/app/api/affiliate/create-character/route.ts +++ b/app/api/affiliate/create-character/route.ts @@ -14,7 +14,10 @@ import { getCorsHeaders } from "@/lib/utils/cors"; import { logger } from "@/lib/utils/logger"; // Get message limit from env or default to 5 (matching auth-anonymous.ts) -const ANON_MESSAGE_LIMIT = Number.parseInt(process.env.ANON_MESSAGE_LIMIT || "5", 10); +const ANON_MESSAGE_LIMIT = Number.parseInt( + process.env.ANON_MESSAGE_LIMIT || "5", + 10, +); // Custom validator for URL or base64 data URL const urlOrBase64 = z.string().refine( @@ -51,10 +54,17 @@ const CreateCharacterSchema = z.object({ settings: z .record( z.string(), - z.union([z.string(), z.number(), z.boolean(), z.record(z.string(), z.unknown())]), + z.union([ + z.string(), + z.number(), + z.boolean(), + z.record(z.string(), z.unknown()), + ]), ) .optional(), - secrets: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), + secrets: z + .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) + .optional(), avatar_url: urlOrBase64.optional(), }), affiliateId: z.string(), @@ -104,7 +114,8 @@ async function handlePost(request: NextRequest) { return NextResponse.json( { success: false, - error: "Missing or invalid Authorization header. Expected: Bearer ", + error: + "Missing or invalid Authorization header. Expected: Bearer ", }, { status: 401 }, ); @@ -126,11 +137,14 @@ async function handlePost(request: NextRequest) { // 2. CHECK PERMISSIONS - Ensure API key has affiliate permissions if (!apiKey.permissions.includes("affiliate:create-character")) { - logger.warn(`[Affiliate API] API key ${apiKey.key_prefix} lacks affiliate permissions`); + logger.warn( + `[Affiliate API] API key ${apiKey.key_prefix} lacks affiliate permissions`, + ); return NextResponse.json( { success: false, - error: "This API key does not have permission to create characters via affiliate API", + error: + "This API key does not have permission to create characters via affiliate API", }, { status: 403 }, ); @@ -156,21 +170,31 @@ async function handlePost(request: NextRequest) { ); } - const { character, affiliateId, sessionId: providedSessionId, metadata } = validatedData; + const { + character, + affiliateId, + sessionId: providedSessionId, + metadata, + } = validatedData; - logger.info(`[Affiliate API] Creating character for affiliate: ${affiliateId}`, { - characterName: character.name, - hasSessionId: !!providedSessionId, - hasImageUrls: !!(metadata?.imageUrls && metadata.imageUrls.length > 0), - imageCount: metadata?.imageUrls?.length || 0, - }); + logger.info( + `[Affiliate API] Creating character for affiliate: ${affiliateId}`, + { + characterName: character.name, + hasSessionId: !!providedSessionId, + hasImageUrls: !!(metadata?.imageUrls && metadata.imageUrls.length > 0), + imageCount: metadata?.imageUrls?.length || 0, + }, + ); // 5. GET OR CREATE AFFILIATE ORGANIZATION // We use a special organization for all affiliate-created characters let affiliateOrg; try { // Try to find existing affiliate organization by slug - affiliateOrg = await organizationsService.getBySlug("affiliate-characters"); + affiliateOrg = await organizationsService.getBySlug( + "affiliate-characters", + ); if (!affiliateOrg) { // Create affiliate organization if it doesn't exist @@ -184,7 +208,10 @@ async function handlePost(request: NextRequest) { }); } } catch (error) { - logger.error("[Affiliate API] Failed to get/create affiliate organization", error); + logger.error( + "[Affiliate API] Failed to get/create affiliate organization", + error, + ); return NextResponse.json( { success: false, @@ -232,7 +259,9 @@ async function handlePost(request: NextRequest) { expires_at: expiresAt, messages_limit: ANON_MESSAGE_LIMIT, // Free tier: 5 messages before soft signup ip_address: - request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || undefined, + request.headers.get("x-forwarded-for") || + request.headers.get("x-real-ip") || + undefined, user_agent: request.headers.get("user-agent") || undefined, }); @@ -266,7 +295,10 @@ async function handlePost(request: NextRequest) { avatarBase64: metadata.avatarBase64, }; - processedImages = await processAffiliateImages(affiliateMetadata, tempCharacterId); + processedImages = await processAffiliateImages( + affiliateMetadata, + tempCharacterId, + ); logger.info("[Affiliate API] Processed affiliate images", { avatarUrl: processedImages.avatarUrl @@ -276,14 +308,18 @@ async function handlePost(request: NextRequest) { failedCount: processedImages.failedUploads, }); } catch (error) { - logger.error("[Affiliate API] Failed to process affiliate images", error); + logger.error( + "[Affiliate API] Failed to process affiliate images", + error, + ); } } // 9. CREATE CHARACTER let createdCharacter; try { - const resolvedAvatarUrl = processedImages.avatarUrl || character.avatar_url || null; + const resolvedAvatarUrl = + processedImages.avatarUrl || character.avatar_url || null; if (resolvedAvatarUrl) { logger.info("[Affiliate API] Avatar URL resolved", { @@ -314,7 +350,10 @@ async function handlePost(request: NextRequest) { user_id: anonymousUser.id, name: elizaCharacter.name, bio: elizaCharacter.bio, - message_examples: (elizaCharacter.messageExamples || []) as Record[][], + message_examples: (elizaCharacter.messageExamples || []) as Record< + string, + unknown + >[][], post_examples: [], topics: elizaCharacter.topics || [], adjectives: elizaCharacter.adjectives || [], @@ -324,7 +363,10 @@ async function handlePost(request: NextRequest) { string, string | number | boolean | Record >, - secrets: (elizaCharacter.secrets || {}) as Record, + secrets: (elizaCharacter.secrets || {}) as Record< + string, + string | number | boolean + >, style: elizaCharacter.style || {}, character_data: { ...elizaCharacter, @@ -409,7 +451,10 @@ async function handlePost(request: NextRequest) { ); } catch (error) { const duration = Date.now() - startTime; - logger.error(`[Affiliate API] ❌ Request failed after ${duration}ms`, error); + logger.error( + `[Affiliate API] ❌ Request failed after ${duration}ms`, + error, + ); return NextResponse.json( { diff --git a/app/api/affiliate/create-session/route.ts b/app/api/affiliate/create-session/route.ts index 9c525f565..c777145ef 100644 --- a/app/api/affiliate/create-session/route.ts +++ b/app/api/affiliate/create-session/route.ts @@ -10,10 +10,16 @@ import { logger } from "@/lib/utils/logger"; const ANON_SESSION_COOKIE = "eliza-anon-session"; // Session expiry in days -const ANON_SESSION_EXPIRY_DAYS = Number.parseInt(process.env.ANON_SESSION_EXPIRY_DAYS || "7", 10); +const ANON_SESSION_EXPIRY_DAYS = Number.parseInt( + process.env.ANON_SESSION_EXPIRY_DAYS || "7", + 10, +); // Get message limit from env or default -const ANON_MESSAGE_LIMIT = Number.parseInt(process.env.ANON_MESSAGE_LIMIT || "5", 10); +const ANON_MESSAGE_LIMIT = Number.parseInt( + process.env.ANON_MESSAGE_LIMIT || "5", + 10, +); // Schema validation for incoming request const CreateSessionSchema = z.object({ @@ -36,7 +42,10 @@ export async function POST(request: NextRequest) { const validationResult = CreateSessionSchema.safeParse(body); if (!validationResult.success) { - logger.warn("[Create Session] Invalid request body:", validationResult.error.format()); + logger.warn( + "[Create Session] Invalid request body:", + validationResult.error.format(), + ); return NextResponse.json( { success: false, @@ -53,12 +62,15 @@ export async function POST(request: NextRequest) { const sessionToken = nanoid(32); // Session expires in configured days - const expiresAt = new Date(Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000); + const expiresAt = new Date( + Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000, + ); // Extract client info const realIp = request.headers.get("x-real-ip")?.trim(); const forwardedFor = request.headers.get("x-forwarded-for"); - const ipAddress = realIp || forwardedFor?.split(",")[0]?.trim() || undefined; + const ipAddress = + realIp || forwardedFor?.split(",")[0]?.trim() || undefined; const userAgent = request.headers.get("user-agent") || undefined; // NOTE: IP-based anonymous-session abuse checks intentionally removed. @@ -86,10 +98,13 @@ export async function POST(request: NextRequest) { expires: expiresAt, }); - logger.info(`[Create Session] Created anonymous session for character ${characterId}`, { - userId: result.user.id, - source, - }); + logger.info( + `[Create Session] Created anonymous session for character ${characterId}`, + { + userId: result.user.id, + source, + }, + ); return NextResponse.json({ success: true, diff --git a/app/api/agents/[id]/a2a/route.ts b/app/api/agents/[id]/a2a/route.ts index 27b840364..35caa7426 100644 --- a/app/api/agents/[id]/a2a/route.ts +++ b/app/api/agents/[id]/a2a/route.ts @@ -25,7 +25,11 @@ import type { UserCharacter } from "@/db/schemas/user-characters"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { CORS_ALLOW_HEADERS, CORS_ALLOW_METHODS } from "@/lib/cors-constants"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { calculateCost, estimateRequestCost, getProviderFromModel } from "@/lib/pricing"; +import { + calculateCost, + estimateRequestCost, + getProviderFromModel, +} from "@/lib/pricing"; import { mergeAnthropicCotProviderOptions, parseThinkingBudgetFromCharacterSettings, @@ -34,7 +38,10 @@ import { import { agentMonetizationService } from "@/lib/services/agent-monetization"; import { charactersService } from "@/lib/services/characters/characters"; import type { CreditReservation } from "@/lib/services/credits"; -import { creditsService, InsufficientCreditsError } from "@/lib/services/credits"; +import { + creditsService, + InsufficientCreditsError, +} from "@/lib/services/credits"; import { logger } from "@/lib/utils/logger"; export const maxDuration = 60; @@ -58,7 +65,9 @@ const JsonRpcRequestSchema = z.object({ * Generate A2A Agent Card for a character */ function generateAgentCard(character: UserCharacter, baseUrl: string) { - const bioText = Array.isArray(character.bio) ? character.bio.join("\n") : character.bio; + const bioText = Array.isArray(character.bio) + ? character.bio.join("\n") + : character.bio; const markupPct = Number(character.inference_markup_percentage || 0); const hasMonetization = character.monetization_enabled && markupPct > 0; @@ -129,9 +138,15 @@ function generateAgentCard(character: UserCharacter, baseUrl: string) { * GET /api/agents/{id}/a2a * Returns the A2A Agent Card for this agent */ -async function handleGET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { +async function handleGET( + request: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { if (!ctx) { - return NextResponse.json({ error: "Missing route context" }, { status: 500 }); + return NextResponse.json( + { error: "Missing route context" }, + { status: 500 }, + ); } const { id } = await ctx.params; @@ -147,10 +162,14 @@ async function handleGET(request: NextRequest, ctx: { params: Promise<{ id: stri // Check if A2A is enabled for this agent if (!character.a2a_enabled) { - return NextResponse.json({ error: "A2A not enabled for this agent" }, { status: 403 }); + return NextResponse.json( + { error: "A2A not enabled for this agent" }, + { status: 403 }, + ); } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const agentCard = generateAgentCard(character, baseUrl); return NextResponse.json(agentCard, { @@ -166,9 +185,15 @@ async function handleGET(request: NextRequest, ctx: { params: Promise<{ id: stri * POST /api/agents/{id}/a2a * JSON-RPC endpoint for agent interaction */ -async function handlePOST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { +async function handlePOST( + request: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { if (!ctx) { - return NextResponse.json({ error: "Missing route context" }, { status: 500 }); + return NextResponse.json( + { error: "Missing route context" }, + { status: 500 }, + ); } const { id } = await ctx.params; @@ -214,7 +239,9 @@ async function handlePOST(request: NextRequest, ctx: { params: Promise<{ id: str const { method, params, id: rpcId } = validation.data; // Authenticate with API key or session - const authResult = await requireAuthOrApiKeyWithOrg(request).catch(() => null); + const authResult = await requireAuthOrApiKeyWithOrg(request).catch( + () => null, + ); if (!authResult) { return NextResponse.json( @@ -291,8 +318,11 @@ async function handleChat( } // Build system prompt from character - const bioText = Array.isArray(character.bio) ? character.bio.join("\n") : character.bio; - const systemPrompt = character.system || `You are ${character.name}. ${bioText}`; + const bioText = Array.isArray(character.bio) + ? character.bio.join("\n") + : character.bio; + const systemPrompt = + character.system || `You are ${character.name}. ${bioText}`; const fullMessages = [ { role: "system" as const, content: systemPrompt }, @@ -306,7 +336,9 @@ async function handleChat( // Use resolveAnthropicThinkingBudgetTokens to get effective budget (same as MCP route) // Add thinking budget on top of base output tokens for accurate credit reservation const provider = getProviderFromModel(model); - const agentThinkingBudget = parseThinkingBudgetFromCharacterSettings(character.settings); + const agentThinkingBudget = parseThinkingBudgetFromCharacterSettings( + character.settings, + ); const effectiveThinkingBudget = resolveAnthropicThinkingBudgetTokens( model, process.env, @@ -315,11 +347,17 @@ async function handleChat( // Add thinking budget to base output estimate (500 tokens) to match MCP route behavior const maxOutputTokens = effectiveThinkingBudget != null ? 500 + effectiveThinkingBudget : undefined; - const baseCost = await estimateRequestCost(model, fullMessages, maxOutputTokens); + const baseCost = await estimateRequestCost( + model, + fullMessages, + maxOutputTokens, + ); // Apply markup if monetization is enabled const markupPct = Number(character.inference_markup_percentage || 0); - const creatorMarkup = character.monetization_enabled ? baseCost * (markupPct / 100) : 0; + const creatorMarkup = character.monetization_enabled + ? baseCost * (markupPct / 100) + : 0; const totalCost = baseCost + creatorMarkup; // Reserve credits BEFORE LLM call to prevent TOCTOU race condition @@ -350,7 +388,11 @@ async function handleChat( const result = await streamText({ model: gateway.languageModel(model), messages: fullMessages, - ...mergeAnthropicCotProviderOptions(model, process.env, effectiveThinkingBudget ?? undefined), + ...mergeAnthropicCotProviderOptions( + model, + process.env, + effectiveThinkingBudget ?? undefined, + ), }); let fullText = ""; @@ -386,11 +428,14 @@ async function handleChat( protocol: "a2a", }); - logger.info("[Agent A2A] Creator earnings credited to redeemable balance", { - agentId: character.id, - ownerId: character.user_id, - earnings: actualCreatorMarkup, - }); + logger.info( + "[Agent A2A] Creator earnings credited to redeemable balance", + { + agentId: character.id, + ownerId: character.user_id, + earnings: actualCreatorMarkup, + }, + ); } // Reconcile with actual cost (handles refund or overage) diff --git a/app/api/agents/[id]/headscale-ip/route.ts b/app/api/agents/[id]/headscale-ip/route.ts index abc0f00e9..1133bf5ab 100644 --- a/app/api/agents/[id]/headscale-ip/route.ts +++ b/app/api/agents/[id]/headscale-ip/route.ts @@ -4,7 +4,8 @@ import { miladySandboxesRepository } from "@/db/repositories/milady-sandboxes"; export const dynamic = "force-dynamic"; -const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function getInternalToken(request: NextRequest): string | null { const direct = request.headers.get("x-internal-token"); @@ -30,13 +31,19 @@ function getInternalToken(request: NextRequest): string | null { * Access is restricted with a shared internal token injected by the * trusted reverse proxy. Do not expose this endpoint publicly. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { const { id: agentId } = await params; const expectedToken = process.env.HEADSCALE_INTERNAL_TOKEN?.trim(); if (!expectedToken) { console.error("[headscale-ip] HEADSCALE_INTERNAL_TOKEN is not configured"); - return NextResponse.json({ error: "internal auth not configured" }, { status: 503 }); + return NextResponse.json( + { error: "internal auth not configured" }, + { status: 503 }, + ); } const providedToken = getInternalToken(request) ?? ""; @@ -50,7 +57,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ // --- Validate UUID format ----------------------------------------------- if (!UUID_RE.test(agentId)) { - return NextResponse.json({ error: "invalid agent ID format" }, { status: 400 }); + return NextResponse.json( + { error: "invalid agent ID format" }, + { status: 400 }, + ); } try { @@ -75,12 +85,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } if (!ip) { - return NextResponse.json({ error: "agent has no routable IP" }, { status: 503 }); + return NextResponse.json( + { error: "agent has no routable IP" }, + { status: 503 }, + ); } const webUiPort = sandbox.web_ui_port ?? 0; if (!webUiPort) { - return NextResponse.json({ error: "agent has no web UI port" }, { status: 503 }); + return NextResponse.json( + { error: "agent has no web UI port" }, + { status: 503 }, + ); } return NextResponse.json({ diff --git a/app/api/agents/[id]/mcp/route.ts b/app/api/agents/[id]/mcp/route.ts index 533c3cdf6..39c4077ec 100644 --- a/app/api/agents/[id]/mcp/route.ts +++ b/app/api/agents/[id]/mcp/route.ts @@ -24,7 +24,11 @@ import { z } from "zod"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { CORS_ALLOW_HEADERS, CORS_ALLOW_METHODS } from "@/lib/cors-constants"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { calculateCost, estimateTokens, getProviderFromModel } from "@/lib/pricing"; +import { + calculateCost, + estimateTokens, + getProviderFromModel, +} from "@/lib/pricing"; import { mergeAnthropicCotProviderOptions, parseThinkingBudgetFromCharacterSettings, @@ -33,7 +37,10 @@ import { import { agentMonetizationService } from "@/lib/services/agent-monetization"; import { charactersService } from "@/lib/services/characters/characters"; import type { CreditReservation } from "@/lib/services/credits"; -import { creditsService, InsufficientCreditsError } from "@/lib/services/credits"; +import { + creditsService, + InsufficientCreditsError, +} from "@/lib/services/credits"; import { logger } from "@/lib/utils/logger"; /** @@ -63,9 +70,15 @@ const MCPRequestSchema = z.object({ * GET /api/agents/{id}/mcp * Returns MCP server metadata */ -async function handleGET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { +async function handleGET( + request: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { if (!ctx) { - return NextResponse.json({ error: "Missing route context" }, { status: 500 }); + return NextResponse.json( + { error: "Missing route context" }, + { status: 500 }, + ); } const { id } = await ctx.params; @@ -75,11 +88,17 @@ async function handleGET(request: NextRequest, ctx: { params: Promise<{ id: stri } if (!character.is_public || !character.mcp_enabled) { - return NextResponse.json({ error: "MCP not accessible for this agent" }, { status: 403 }); + return NextResponse.json( + { error: "MCP not accessible for this agent" }, + { status: 403 }, + ); } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; - const bioText = Array.isArray(character.bio) ? character.bio.join("\n") : character.bio; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const bioText = Array.isArray(character.bio) + ? character.bio.join("\n") + : character.bio; const markupPct = Number(character.inference_markup_percentage || 0); @@ -144,9 +163,15 @@ async function handleGET(request: NextRequest, ctx: { params: Promise<{ id: stri * POST /api/agents/{id}/mcp * MCP protocol handler */ -async function handlePOST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { +async function handlePOST( + request: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { if (!ctx) { - return NextResponse.json({ error: "Missing route context" }, { status: 500 }); + return NextResponse.json( + { error: "Missing route context" }, + { status: 500 }, + ); } const { id } = await ctx.params; @@ -190,7 +215,9 @@ async function handlePOST(request: NextRequest, ctx: { params: Promise<{ id: str const { method, params, id: rpcId } = validation.data; // Authenticate with API key or session - const authResult = await requireAuthOrApiKeyWithOrg(request).catch(() => null); + const authResult = await requireAuthOrApiKeyWithOrg(request).catch( + () => null, + ); if (!authResult) { return NextResponse.json( @@ -298,7 +325,9 @@ async function handleToolCall( }; if (name === "get_info") { - const bioText = Array.isArray(character.bio) ? character.bio.join("\n") : character.bio; + const bioText = Array.isArray(character.bio) + ? character.bio.join("\n") + : character.bio; return NextResponse.json({ jsonrpc: "2.0", @@ -333,8 +362,11 @@ async function handleToolCall( }); } - const bioText = Array.isArray(character.bio) ? character.bio.join("\n") : character.bio; - const systemPrompt = character.system || `You are ${character.name}. ${bioText}`; + const bioText = Array.isArray(character.bio) + ? character.bio.join("\n") + : character.bio; + const systemPrompt = + character.system || `You are ${character.name}. ${bioText}`; const messages = [ { role: "system" as const, content: systemPrompt }, @@ -346,7 +378,9 @@ async function handleToolCall( const markupPct = Number(character.inference_markup_percentage || 0); // Resolve effective thinking budget before reservation (applies ANTHROPIC_COT_BUDGET_MAX cap) - const agentThinkingBudget = parseThinkingBudgetFromCharacterSettings(character.settings); + const agentThinkingBudget = parseThinkingBudgetFromCharacterSettings( + character.settings, + ); const effectiveThinkingBudget = resolveAnthropicThinkingBudgetTokens( model, process.env, @@ -390,7 +424,8 @@ async function handleToolCall( // Anthropic API requires maxOutputTokens >= budgetTokens when thinking is enabled // Also need to reserve capacity for actual response generation beyond thinking const maxOutputTokens = effectiveThinkingBudget - ? Math.max(DEFAULT_MIN_OUTPUT_TOKENS, effectiveThinkingBudget) + DEFAULT_MIN_OUTPUT_TOKENS + ? Math.max(DEFAULT_MIN_OUTPUT_TOKENS, effectiveThinkingBudget) + + DEFAULT_MIN_OUTPUT_TOKENS : undefined; try { @@ -398,7 +433,11 @@ async function handleToolCall( model: gateway.languageModel(model), messages, ...(maxOutputTokens && { maxOutputTokens }), - ...mergeAnthropicCotProviderOptions(model, process.env, agentThinkingBudget), + ...mergeAnthropicCotProviderOptions( + model, + process.env, + agentThinkingBudget, + ), }); let fullText = ""; @@ -431,11 +470,14 @@ async function handleToolCall( protocol: "mcp", }); - logger.info("[Agent MCP] Creator earnings credited to redeemable balance", { - agentId: character.id, - ownerId: character.user_id, - earnings: actualCreatorMarkup, - }); + logger.info( + "[Agent MCP] Creator earnings credited to redeemable balance", + { + agentId: character.id, + ownerId: character.user_id, + earnings: actualCreatorMarkup, + }, + ); } // Reconcile with actual cost (handles refund or overage) diff --git a/app/api/analytics/export/route.ts b/app/api/analytics/export/route.ts index aa5f079dd..7d4be851f 100644 --- a/app/api/analytics/export/route.ts +++ b/app/api/analytics/export/route.ts @@ -55,7 +55,8 @@ async function handleGET(req: NextRequest) { : new Date(); // Validate time range - const timeRangeDays = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24); + const timeRangeDays = + (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24); if (timeRangeDays > EXPORT_LIMITS.MAX_TIME_RANGE_DAYS) { return NextResponse.json( @@ -68,7 +69,10 @@ async function handleGET(req: NextRequest) { } if (startDate >= endDate) { - return NextResponse.json({ error: "startDate must be before endDate" }, { status: 400 }); + return NextResponse.json( + { error: "startDate must be before endDate" }, + { status: 400 }, + ); } const granularityParam = searchParams.get("granularity") || "day"; @@ -121,10 +125,13 @@ async function handleGET(req: NextRequest) { ]; filename = `user-analytics-${startDate.toISOString().split("T")[0]}-to-${endDate.toISOString().split("T")[0]}`; } else if (dataType === "providers") { - const providerBreakdown = await getProviderBreakdown(user.organization_id!, { - startDate, - endDate, - }); + const providerBreakdown = await getProviderBreakdown( + user.organization_id!, + { + startDate, + endDate, + }, + ); data = providerBreakdown.map((p) => ({ provider: p.provider, requests: p.totalRequests, @@ -239,7 +246,9 @@ async function handleGET(req: NextRequest) { `${filename}.json`, "application/json", ); - Object.entries(responseHeaders).forEach(([key, value]) => response.headers.set(key, value)); + Object.entries(responseHeaders).forEach(([key, value]) => + response.headers.set(key, value), + ); return response; } @@ -250,14 +259,17 @@ async function handleGET(req: NextRequest) { `${filename}.xlsx`, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ); - Object.entries(responseHeaders).forEach(([key, value]) => response.headers.set(key, value)); + Object.entries(responseHeaders).forEach(([key, value]) => + response.headers.set(key, value), + ); return response; } if (format === "pdf") { return NextResponse.json( { - error: "PDF export requires 'pdfkit' package. Install with: bun add pdfkit @types/pdfkit", + error: + "PDF export requires 'pdfkit' package. Install with: bun add pdfkit @types/pdfkit", }, { status: 501 }, ); @@ -268,13 +280,18 @@ async function handleGET(req: NextRequest) { `${filename}.csv`, "text/csv", ); - Object.entries(responseHeaders).forEach(([key, value]) => response.headers.set(key, value)); + Object.entries(responseHeaders).forEach(([key, value]) => + response.headers.set(key, value), + ); return response; } catch (error) { logger.error("[Analytics Export] Error:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to export analytics data", + error: + error instanceof Error + ? error.message + : "Failed to export analytics data", }, { status: 500 }, ); diff --git a/app/api/analytics/overview/route.ts b/app/api/analytics/overview/route.ts index 8b1d1ac84..3263738e1 100644 --- a/app/api/analytics/overview/route.ts +++ b/app/api/analytics/overview/route.ts @@ -18,9 +18,14 @@ async function handleGET(req: NextRequest) { const { user } = await requireAuthOrApiKeyWithOrg(req); const searchParams = req.nextUrl.searchParams; - const timeRange = (searchParams.get("timeRange") as "daily" | "weekly" | "monthly") || "daily"; + const timeRange = + (searchParams.get("timeRange") as "daily" | "weekly" | "monthly") || + "daily"; - const overview = await analyticsService.getOverview(user.organization_id!, timeRange); + const overview = await analyticsService.getOverview( + user.organization_id!, + timeRange, + ); const now = new Date(); let startDate: Date; @@ -41,10 +46,14 @@ async function handleGET(req: NextRequest) { const data = { totalRequests: overview.summary.totalRequests, - successfulRequests: Math.round(overview.summary.totalRequests * overview.summary.successRate), + successfulRequests: Math.round( + overview.summary.totalRequests * overview.summary.successRate, + ), failedRequests: overview.summary.totalRequests - - Math.round(overview.summary.totalRequests * overview.summary.successRate), + Math.round( + overview.summary.totalRequests * overview.summary.successRate, + ), successRate: overview.summary.successRate, totalCost: overview.summary.totalCost, avgCostPerRequest: overview.summary.avgCostPerRequest, @@ -68,7 +77,8 @@ async function handleGET(req: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch analytics", + error: + error instanceof Error ? error.message : "Failed to fetch analytics", }, { status: 500 }, ); diff --git a/app/api/anonymous-session/route.ts b/app/api/anonymous-session/route.ts index 8ab72e550..78a321e98 100644 --- a/app/api/anonymous-session/route.ts +++ b/app/api/anonymous-session/route.ts @@ -78,12 +78,18 @@ async function handleGET(request: NextRequest) { // Input validation if (!token) { - return NextResponse.json({ error: "Session token is required" }, { status: 400 }); + return NextResponse.json( + { error: "Session token is required" }, + { status: 400 }, + ); } if (!isValidTokenFormat(token)) { logger.warn("[Anonymous Session API] Invalid token format"); - return NextResponse.json({ error: "Invalid session token format" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid session token format" }, + { status: 400 }, + ); } // Rate limiting per token @@ -111,8 +117,13 @@ async function handleGET(request: NextRequest) { const session = await anonymousSessionsService.getByToken(token); if (!session) { - logger.warn(`[Anonymous Session API] Session not found for token hash: ${tokenHash}`); - return NextResponse.json({ error: "Session not found or expired" }, { status: 404 }); + logger.warn( + `[Anonymous Session API] Session not found for token hash: ${tokenHash}`, + ); + return NextResponse.json( + { error: "Session not found or expired" }, + { status: 404 }, + ); } logger.debug("[Anonymous Session API] Returning session data:", { @@ -141,7 +152,10 @@ async function handleGET(request: NextRequest) { ); } catch (error) { logger.error("[Anonymous Session API] Error:", error); - return NextResponse.json({ error: "Failed to get session data" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get session data" }, + { status: 500 }, + ); } } diff --git a/app/api/auth/cli-session/[sessionId]/complete/route.ts b/app/api/auth/cli-session/[sessionId]/complete/route.ts index fd2a7ed0c..9eb0c79ed 100644 --- a/app/api/auth/cli-session/[sessionId]/complete/route.ts +++ b/app/api/auth/cli-session/[sessionId]/complete/route.ts @@ -17,7 +17,10 @@ export async function POST( const { sessionId } = await params; if (!sessionId) { - return NextResponse.json({ error: "Session ID is required" }, { status: 400 }); + return NextResponse.json( + { error: "Session ID is required" }, + { status: 400 }, + ); } // Require user to be authenticated via Privy diff --git a/app/api/auth/cli-session/[sessionId]/route.ts b/app/api/auth/cli-session/[sessionId]/route.ts index 1715963e5..11beb0ed4 100644 --- a/app/api/auth/cli-session/[sessionId]/route.ts +++ b/app/api/auth/cli-session/[sessionId]/route.ts @@ -34,7 +34,10 @@ export async function GET( if (!session) { return applyCorsHeaders( - NextResponse.json({ error: "Session not found or expired" }, { status: 404 }), + NextResponse.json( + { error: "Session not found or expired" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -42,7 +45,8 @@ export async function GET( // Check if session is authenticated if (session.status === "authenticated") { // Retrieve and clear the plain API key (one-time retrieval for security) - const apiKeyData = await cliAuthSessionsService.getAndClearApiKey(sessionId); + const apiKeyData = + await cliAuthSessionsService.getAndClearApiKey(sessionId); if (!apiKeyData) { return applyCorsHeaders( @@ -84,7 +88,10 @@ export async function GET( } catch (error) { logger.error("[CLI Auth] Error getting CLI auth session", { error }); return applyCorsHeaders( - NextResponse.json({ error: "Failed to get session status" }, { status: 500 }), + NextResponse.json( + { error: "Failed to get session status" }, + { status: 500 }, + ), CORS_METHODS, ); } diff --git a/app/api/auth/cli-session/route.ts b/app/api/auth/cli-session/route.ts index 6a112fe27..ebea28a9f 100644 --- a/app/api/auth/cli-session/route.ts +++ b/app/api/auth/cli-session/route.ts @@ -45,7 +45,10 @@ export async function POST(request: NextRequest) { } catch (error) { logger.error("Error creating CLI auth session:", error); return applyCorsHeaders( - NextResponse.json({ error: "Failed to create authentication session" }, { status: 500 }), + NextResponse.json( + { error: "Failed to create authentication session" }, + { status: 500 }, + ), CORS_METHODS, ); } diff --git a/app/api/auth/create-anonymous-session/route.ts b/app/api/auth/create-anonymous-session/route.ts index eded23755..b482a875b 100644 --- a/app/api/auth/create-anonymous-session/route.ts +++ b/app/api/auth/create-anonymous-session/route.ts @@ -17,7 +17,9 @@ function parsePositiveIntEnv( ): number { const value = Number.parseInt(envValue || String(defaultValue), 10); if (Number.isNaN(value) || value <= 0) { - logger.warn(`[create-anonymous-session] Invalid ${envName}, using default: ${defaultValue}`); + logger.warn( + `[create-anonymous-session] Invalid ${envName}, using default: ${defaultValue}`, + ); return defaultValue; } return value; @@ -40,7 +42,10 @@ async function getClientIp(): Promise { if (realIp) { return realIp; } - const forwardedFor = headersList.get("x-forwarded-for")?.split(",")[0]?.trim(); + const forwardedFor = headersList + .get("x-forwarded-for") + ?.split(",")[0] + ?.trim(); return forwardedFor || undefined; } @@ -73,7 +78,9 @@ export async function GET(request: NextRequest) { } const newSessionToken = nanoid(32); - const expiresAt = new Date(Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000); + const expiresAt = new Date( + Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000, + ); const ipAddress = await getClientIp(); const userAgent = await getUserAgent(); // NOTE: IP-based anonymous-session abuse checks intentionally removed. @@ -106,6 +113,8 @@ export async function GET(request: NextRequest) { } catch (error) { logger.error("[create-anonymous-session] Error creating session:", error); - return NextResponse.redirect(new URL("/login?error=session_error", request.url)); + return NextResponse.redirect( + new URL("/login?error=session_error", request.url), + ); } } diff --git a/app/api/auth/migrate-anonymous/route.ts b/app/api/auth/migrate-anonymous/route.ts index 600260e9d..1bf2f88b8 100644 --- a/app/api/auth/migrate-anonymous/route.ts +++ b/app/api/auth/migrate-anonymous/route.ts @@ -39,7 +39,10 @@ export async function POST(request: NextRequest) { const user = await requireAuth(); if (!user.privy_user_id) { - return NextResponse.json({ error: "User does not have a Privy ID" }, { status: 400 }); + return NextResponse.json( + { error: "User does not have a Privy ID" }, + { status: 400 }, + ); } logger.info("[Migrate Anonymous] Starting migration for user:", user.id); @@ -55,7 +58,9 @@ export async function POST(request: NextRequest) { } if (!sessionToken) { - logger.info("[Migrate Anonymous] No anonymous session found, nothing to migrate"); + logger.info( + "[Migrate Anonymous] No anonymous session found, nothing to migrate", + ); return NextResponse.json({ success: true, message: "No anonymous session to migrate", @@ -79,7 +84,10 @@ export async function POST(request: NextRequest) { // Check if already converted if (anonSession.converted_at) { - logger.info("[Migrate Anonymous] Session already converted:", anonSession.id); + logger.info( + "[Migrate Anonymous] Session already converted:", + anonSession.id, + ); // Clean up the cookie cookieStore.delete(ANON_SESSION_COOKIE); @@ -99,7 +107,10 @@ export async function POST(request: NextRequest) { messageCount: anonSession.message_count, }); - const migrationResult = await migrateAnonymousSession(anonSession.user_id, user.privy_user_id); + const migrationResult = await migrateAnonymousSession( + anonSession.user_id, + user.privy_user_id, + ); logger.info("[Migrate Anonymous] Migration completed", migrationResult); @@ -116,7 +127,10 @@ export async function POST(request: NextRequest) { logger.error("[Migrate Anonymous] Error during migration:", error); // "Anonymous user not found" from migrateAnonymousSession means nothing to migrate — treat as success. - if (error instanceof Error && error.message === "Anonymous user not found") { + if ( + error instanceof Error && + error.message === "Anonymous user not found" + ) { return NextResponse.json({ success: true, message: "No anonymous data to migrate", diff --git a/app/api/auth/pair/route.ts b/app/api/auth/pair/route.ts index 34f313cd1..231f5fe0e 100644 --- a/app/api/auth/pair/route.ts +++ b/app/api/auth/pair/route.ts @@ -22,19 +22,28 @@ async function handler(request: NextRequest) { const token = body?.token; if (!token) { - return NextResponse.json({ error: "Pairing code required" }, { status: 400 }); + return NextResponse.json( + { error: "Pairing code required" }, + { status: 400 }, + ); } const origin = request.headers.get("origin") ?? null; if (!origin) { - return NextResponse.json({ error: "Origin header required" }, { status: 400 }); + return NextResponse.json( + { error: "Origin header required" }, + { status: 400 }, + ); } const tokenService = getPairingTokenService(); const pairingToken = await tokenService.validateToken(token, origin); if (!pairingToken) { - return NextResponse.json({ error: "Invalid or expired pairing code" }, { status: 401 }); + return NextResponse.json( + { error: "Invalid or expired pairing code" }, + { status: 401 }, + ); } // Look up the sandbox scoped to the org to prevent cross-org access @@ -61,7 +70,10 @@ async function handler(request: NextRequest) { agentName: sandbox.agent_name ?? "Agent", }); - response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate"); + response.headers.set( + "Cache-Control", + "no-store, no-cache, must-revalidate", + ); return response; } catch (err: unknown) { diff --git a/app/api/auth/siwe/nonce/route.ts b/app/api/auth/siwe/nonce/route.ts index aee73d67e..a457461bd 100644 --- a/app/api/auth/siwe/nonce/route.ts +++ b/app/api/auth/siwe/nonce/route.ts @@ -12,10 +12,16 @@ import { getAppHost, getAppUrl } from "@/lib/utils/app-url"; * Rate limit STRICT to prevent nonce flooding. */ async function handler(request: NextRequest) { - const chainId = Number.parseInt(request.nextUrl.searchParams.get("chainId") ?? "1", 10); + const chainId = Number.parseInt( + request.nextUrl.searchParams.get("chainId") ?? "1", + 10, + ); if (!cache.isAvailable()) { - return NextResponse.json({ error: "Nonce storage unavailable" }, { status: 503 }); + return NextResponse.json( + { error: "Nonce storage unavailable" }, + { status: 503 }, + ); } const nonce = crypto.randomBytes(16).toString("hex"); diff --git a/app/api/auth/siwe/verify/route.ts b/app/api/auth/siwe/verify/route.ts index 5dfc70a0e..ae941d130 100644 --- a/app/api/auth/siwe/verify/route.ts +++ b/app/api/auth/siwe/verify/route.ts @@ -17,7 +17,11 @@ function buildResponse( plainKey: string, address: string, isNewAccount: boolean, - user: { id: string; wallet_address: string | null; organization_id: string | null }, + user: { + id: string; + wallet_address: string | null; + organization_id: string | null; + }, org: Organization | null, ) { return NextResponse.json({ @@ -40,12 +44,18 @@ function buildResponse( */ async function handler(request: NextRequest) { if (!cache.isAvailable()) { - return NextResponse.json({ error: "Service temporarily unavailable" }, { status: 503 }); + return NextResponse.json( + { error: "Service temporarily unavailable" }, + { status: 503 }, + ); } const body = (await request.json().catch(() => null)) as VerifyBody | null; if (!body?.message || !body?.signature) { - return NextResponse.json({ error: "message and signature are required" }, { status: 400 }); + return NextResponse.json( + { error: "message and signature are required" }, + { status: 400 }, + ); } let address: string; @@ -56,7 +66,10 @@ async function handler(request: NextRequest) { logger.warn("[SIWE Verify] Validation failed", { error: err instanceof Error ? err.message : String(err), }); - return NextResponse.json({ error: "SIWE verification failed" }, { status: 401 }); + return NextResponse.json( + { error: "SIWE verification failed" }, + { status: 401 }, + ); } const { user, isNewAccount } = await findOrCreateUserByWalletAddress(address); @@ -78,7 +91,13 @@ async function handler(request: NextRequest) { is_active: true, }); - return buildResponse(plainKey, address, isNewAccount, user, user.organization ?? null); + return buildResponse( + plainKey, + address, + isNewAccount, + user, + user.organization ?? null, + ); } export const POST = withRateLimit(handler, RateLimitPresets.STRICT); diff --git a/app/api/auth/steward-debug/route.ts b/app/api/auth/steward-debug/route.ts index 21fc9b1d1..a001c5cd7 100644 --- a/app/api/auth/steward-debug/route.ts +++ b/app/api/auth/steward-debug/route.ts @@ -7,10 +7,15 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); const token = body?.token; - if (!token) return NextResponse.json({ error: "no token" }, { status: 400 }); + if (!token) + return NextResponse.json({ error: "no token" }, { status: 400 }); const claims = await verifyStewardTokenCached(token); - if (!claims) return NextResponse.json({ error: "verification failed", step: "verify" }); + if (!claims) + return NextResponse.json({ + error: "verification failed", + step: "verify", + }); let user = await usersService.getByStewardId(claims.userId); let synced = false; @@ -28,7 +33,11 @@ export async function POST(request: NextRequest) { { error: "sync failed", message: syncErr.message, - claims: { userId: claims.userId, email: claims.email, tenantId: claims.tenantId }, + claims: { + userId: claims.userId, + email: claims.email, + tenantId: claims.tenantId, + }, }, { status: 500 }, ); @@ -37,7 +46,11 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: true, - claims: { userId: claims.userId, email: claims.email, tenantId: claims.tenantId }, + claims: { + userId: claims.userId, + email: claims.email, + tenantId: claims.tenantId, + }, userFound: true, synced, userId: user?.id, diff --git a/app/api/auto-top-up/trigger/route.ts b/app/api/auto-top-up/trigger/route.ts index 3d23bcacf..a5c6cd05e 100644 --- a/app/api/auto-top-up/trigger/route.ts +++ b/app/api/auto-top-up/trigger/route.ts @@ -22,7 +22,10 @@ async function handleTriggerAutoTopUp(req: NextRequest) { // Get organization details const org = await organizationsRepository.findById(organizationId); if (!org) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } // Check if auto top-up is enabled @@ -77,9 +80,15 @@ async function handleTriggerAutoTopUp(req: NextRequest) { return NextResponse.json({ error: error.message }, { status: 500 }); } - return NextResponse.json({ error: "Failed to trigger auto top-up" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to trigger auto top-up" }, + { status: 500 }, + ); } } // Export rate-limited handler -export const POST = withRateLimit(handleTriggerAutoTopUp, RateLimitPresets.STRICT); +export const POST = withRateLimit( + handleTriggerAutoTopUp, + RateLimitPresets.STRICT, +); diff --git a/app/api/characters/[characterId]/mcps/route.ts b/app/api/characters/[characterId]/mcps/route.ts index f5c692c01..3558ada86 100644 --- a/app/api/characters/[characterId]/mcps/route.ts +++ b/app/api/characters/[characterId]/mcps/route.ts @@ -59,7 +59,9 @@ function safeJsonParse( ): unknown { // Check size limit (100KB default) if (jsonString.length > maxSize) { - throw new Error(`JSON string too large: ${jsonString.length} bytes (max: ${maxSize})`); + throw new Error( + `JSON string too large: ${jsonString.length} bytes (max: ${maxSize})`, + ); } // Parse JSON @@ -67,7 +69,9 @@ function safeJsonParse( try { parsed = JSON.parse(jsonString); } catch (error) { - throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : "Parse error"}`); + throw new Error( + `Invalid JSON: ${error instanceof Error ? error.message : "Parse error"}`, + ); } // Check depth limit @@ -97,7 +101,10 @@ function safeJsonParse( * Transform MCP settings by expanding pathname URLs to full URLs * Used when returning settings to the client for display/testing */ -function transformMcpUrlsForDisplay(mcpSettings: McpSettings, request: NextRequest): McpSettings { +function transformMcpUrlsForDisplay( + mcpSettings: McpSettings, + request: NextRequest, +): McpSettings { if (!mcpSettings?.servers) { return mcpSettings; } @@ -114,7 +121,9 @@ function transformMcpUrlsForDisplay(mcpSettings: McpSettings, request: NextReque transformedServers[serverId] = { ...serverConfig, // If URL starts with /, prepend baseUrl for display; otherwise use as-is - url: serverConfig.url.startsWith("/") ? `${baseUrl}${serverConfig.url}` : serverConfig.url, + url: serverConfig.url.startsWith("/") + ? `${baseUrl}${serverConfig.url}` + : serverConfig.url, }; } @@ -160,7 +169,10 @@ function parseMcpSettings(mcpSetting: unknown): McpSettings { * @param ctx - Route context containing the character ID parameter. * @returns MCP settings with server configurations and plugin status. */ -export async function GET(request: NextRequest, ctx: { params: Promise<{ characterId: string }> }) { +export async function GET( + request: NextRequest, + ctx: { params: Promise<{ characterId: string }> }, +) { try { const { user } = await requireAuthOrApiKey(request); const { characterId } = await ctx.params; @@ -169,11 +181,17 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ charact const character = await charactersService.getById(characterId); if (!character) { - return NextResponse.json({ error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { error: "Character not found" }, + { status: 404 }, + ); } // Check ownership - if (character.user_id !== user.id && character.organization_id !== user.organization_id) { + if ( + character.user_id !== user.id && + character.organization_id !== user.organization_id + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } @@ -199,7 +217,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ charact const pluginMcpEnabled = plugins.includes("@elizaos/plugin-mcp"); // Transform pathnames to full URLs for display/testing in the UI - const mcpSettingsForDisplay = transformMcpUrlsForDisplay(mcpSettings, request); + const mcpSettingsForDisplay = transformMcpUrlsForDisplay( + mcpSettings, + request, + ); return NextResponse.json({ characterId, @@ -212,7 +233,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ charact logger.error("[Characters/MCPs] Error getting MCP config:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to get MCP configuration", + error: + error instanceof Error + ? error.message + : "Failed to get MCP configuration", }, { status: 500 }, ); @@ -228,7 +252,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ charact * @param ctx - Route context containing the character ID parameter. * @returns Updated MCP settings and server list. */ -export async function PUT(request: NextRequest, ctx: { params: Promise<{ characterId: string }> }) { +export async function PUT( + request: NextRequest, + ctx: { params: Promise<{ characterId: string }> }, +) { try { const { user } = await requireAuthOrApiKey(request); const { characterId } = await ctx.params; @@ -261,11 +288,17 @@ export async function PUT(request: NextRequest, ctx: { params: Promise<{ charact const character = await charactersService.getById(characterId); if (!character) { - return NextResponse.json({ error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { error: "Character not found" }, + { status: 404 }, + ); } // Check ownership - if (character.user_id !== user.id && character.organization_id !== user.organization_id) { + if ( + character.user_id !== user.id && + character.organization_id !== user.organization_id + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } @@ -299,9 +332,13 @@ export async function PUT(request: NextRequest, ctx: { params: Promise<{ charact // CRITICAL FIX: Invalidate any cached character data // This ensures the next runtime creation will use fresh MCP settings try { - const { invalidateCharacterCache } = await import("@/lib/cache/character-cache"); + const { invalidateCharacterCache } = await import( + "@/lib/cache/character-cache" + ); await invalidateCharacterCache(characterId); - logger.info(`[Characters/MCPs] Invalidated cache for character ${characterId}`); + logger.info( + `[Characters/MCPs] Invalidated cache for character ${characterId}`, + ); } catch (cacheError) { // Don't fail the update if cache invalidation fails logger.warn(`[Characters/MCPs] Failed to invalidate cache:`, cacheError); @@ -323,7 +360,10 @@ export async function PUT(request: NextRequest, ctx: { params: Promise<{ charact logger.error("[Characters/MCPs] Error updating MCP config:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to update MCP configuration", + error: + error instanceof Error + ? error.message + : "Failed to update MCP configuration", }, { status: 500 }, ); @@ -375,11 +415,17 @@ export async function POST( const character = await charactersService.getById(characterId); if (!character) { - return NextResponse.json({ error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { error: "Character not found" }, + { status: 404 }, + ); } // Check ownership - if (character.user_id !== user.id && character.organization_id !== user.organization_id) { + if ( + character.user_id !== user.id && + character.organization_id !== user.organization_id + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } @@ -426,14 +472,20 @@ export async function POST( // CRITICAL FIX: Invalidate any cached character data try { - const { invalidateCharacterCache } = await import("@/lib/cache/character-cache"); + const { invalidateCharacterCache } = await import( + "@/lib/cache/character-cache" + ); await invalidateCharacterCache(characterId); - logger.info(`[Characters/MCPs] Invalidated cache for character ${characterId}`); + logger.info( + `[Characters/MCPs] Invalidated cache for character ${characterId}`, + ); } catch (cacheError) { logger.warn(`[Characters/MCPs] Failed to invalidate cache:`, cacheError); } - logger.info(`[Characters/MCPs] Added MCP server ${serverId} to character ${characterId}`); + logger.info( + `[Characters/MCPs] Added MCP server ${serverId} to character ${characterId}`, + ); return NextResponse.json({ success: true, @@ -446,7 +498,8 @@ export async function POST( logger.error("[Characters/MCPs] Error adding MCP server:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to add MCP server", + error: + error instanceof Error ? error.message : "Failed to add MCP server", }, { status: 500 }, ); @@ -474,18 +527,27 @@ export async function DELETE( const serverId = request.nextUrl.searchParams.get("serverId"); if (!serverId) { - return NextResponse.json({ error: "Missing serverId query parameter" }, { status: 400 }); + return NextResponse.json( + { error: "Missing serverId query parameter" }, + { status: 400 }, + ); } // Get character const character = await charactersService.getById(characterId); if (!character) { - return NextResponse.json({ error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { error: "Character not found" }, + { status: 404 }, + ); } // Check ownership - if (character.user_id !== user.id && character.organization_id !== user.organization_id) { + if ( + character.user_id !== user.id && + character.organization_id !== user.organization_id + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } @@ -534,14 +596,20 @@ export async function DELETE( // CRITICAL FIX: Invalidate any cached character data try { - const { invalidateCharacterCache } = await import("@/lib/cache/character-cache"); + const { invalidateCharacterCache } = await import( + "@/lib/cache/character-cache" + ); await invalidateCharacterCache(characterId); - logger.info(`[Characters/MCPs] Invalidated cache for character ${characterId}`); + logger.info( + `[Characters/MCPs] Invalidated cache for character ${characterId}`, + ); } catch (cacheError) { logger.warn(`[Characters/MCPs] Failed to invalidate cache:`, cacheError); } - logger.info(`[Characters/MCPs] Removed MCP server ${serverId} from character ${characterId}`); + logger.info( + `[Characters/MCPs] Removed MCP server ${serverId} from character ${characterId}`, + ); return NextResponse.json({ success: true, @@ -554,7 +622,10 @@ export async function DELETE( logger.error("[Characters/MCPs] Error removing MCP server:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to remove MCP server", + error: + error instanceof Error + ? error.message + : "Failed to remove MCP server", }, { status: 500 }, ); diff --git a/app/api/characters/[characterId]/public/route.ts b/app/api/characters/[characterId]/public/route.ts index 0e6d95568..a12471503 100644 --- a/app/api/characters/[characterId]/public/route.ts +++ b/app/api/characters/[characterId]/public/route.ts @@ -32,7 +32,10 @@ export async function GET( if (!character) { logger.warn(`[Public Character API] Character not found: ${characterId}`); - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } // Only return cloud-source characters (not miniapp-only characters) @@ -50,16 +53,20 @@ export async function GET( const isPublic = character.is_public === true; // Check if this is a claimable affiliate character - const claimCheck = await charactersService.isClaimableAffiliateCharacter(characterId); + const claimCheck = + await charactersService.isClaimableAffiliateCharacter(characterId); const isClaimableAffiliate = claimCheck.claimable; // Only return info if: character is public, user is owner, or it's a claimable affiliate if (!isPublic && !isOwner && !isClaimableAffiliate) { - logger.warn(`[Public Character API] Access denied to private character: ${characterId}`, { - userId: user?.id, - characterOwnerId: character.user_id, - isPublic: character.is_public, - }); + logger.warn( + `[Public Character API] Access denied to private character: ${characterId}`, + { + userId: user?.id, + characterOwnerId: character.user_id, + isPublic: character.is_public, + }, + ); return NextResponse.json( { success: false, error: "Character not available" }, { status: 404 }, @@ -84,11 +91,14 @@ export async function GET( monetizationEnabled: character.monetization_enabled, }; - logger.debug(`[Public Character API] Returning public info for: ${characterId}`, { - isPublic, - isOwner, - isClaimableAffiliate, - }); + logger.debug( + `[Public Character API] Returning public info for: ${characterId}`, + { + isPublic, + isOwner, + isClaimableAffiliate, + }, + ); return NextResponse.json({ success: true, diff --git a/app/api/compat/_lib/auth.ts b/app/api/compat/_lib/auth.ts index f894b01bb..23cd7b883 100644 --- a/app/api/compat/_lib/auth.ts +++ b/app/api/compat/_lib/auth.ts @@ -25,7 +25,9 @@ export interface CompatAuthResult { /** * Authenticate a compat route request. */ -export async function requireCompatAuth(request: NextRequest): Promise { +export async function requireCompatAuth( + request: NextRequest, +): Promise { // 1. X-Service-Key (milady-cloud S2S) const serviceKeyHeader = request.headers.get("X-Service-Key"); if (serviceKeyHeader !== null) { diff --git a/app/api/compat/_lib/error-handler.ts b/app/api/compat/_lib/error-handler.ts index ce672c31a..789aee3cc 100644 --- a/app/api/compat/_lib/error-handler.ts +++ b/app/api/compat/_lib/error-handler.ts @@ -17,11 +17,21 @@ import { ServiceKeyAuthError } from "@/lib/auth/service-key"; import { applyCorsHeaders } from "@/lib/services/proxy/cors"; import { logger } from "@/lib/utils/logger"; -function compatErrorResponse(message: string, status: number, methods: string): Response { - return applyCorsHeaders(NextResponse.json(errorEnvelope(message), { status }), methods); +function compatErrorResponse( + message: string, + status: number, + methods: string, +): Response { + return applyCorsHeaders( + NextResponse.json(errorEnvelope(message), { status }), + methods, + ); } -export function handleCompatError(err: unknown, methods = "GET, POST, DELETE, OPTIONS"): Response { +export function handleCompatError( + err: unknown, + methods = "GET, POST, DELETE, OPTIONS", +): Response { // 1. Typed API errors — use their built-in status / message. if (err instanceof ApiError) { return compatErrorResponse(err.message, err.status, methods); diff --git a/app/api/compat/agents/[id]/restart/route.ts b/app/api/compat/agents/[id]/restart/route.ts index a7593f4c3..2d1a78d42 100644 --- a/app/api/compat/agents/[id]/restart/route.ts +++ b/app/api/compat/agents/[id]/restart/route.ts @@ -3,7 +3,11 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { envelope, errorEnvelope, toCompatOpResult } from "@/lib/api/compat-envelope"; +import { + envelope, + errorEnvelope, + toCompatOpResult, +} from "@/lib/api/compat-envelope"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { logger } from "@/lib/utils/logger"; import { requireCompatAuth } from "../../../_lib/auth"; @@ -25,7 +29,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const { user } = await requireCompatAuth(request); const { id: agentId } = await params; - const agent = await miladySandboxService.getAgent(agentId, user.organization_id); + const agent = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!agent) { return withCompatCors( NextResponse.json(errorEnvelope("Agent not found"), { status: 404 }), @@ -44,15 +51,23 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); } - const result = await miladySandboxService.provision(agentId, user.organization_id); - const response = envelope(toCompatOpResult(agentId, "restart", result.success)); + const result = await miladySandboxService.provision( + agentId, + user.organization_id, + ); + const response = envelope( + toCompatOpResult(agentId, "restart", result.success), + ); if (!result.success) { logger.warn("[compat] Restart failed", { agentId, error: result.error, }); - return withCompatCors(NextResponse.json(response, { status: 502 }), CORS_METHODS); + return withCompatCors( + NextResponse.json(response, { status: 502 }), + CORS_METHODS, + ); } return withCompatCors(NextResponse.json(response), CORS_METHODS); diff --git a/app/api/compat/agents/[id]/resume/route.ts b/app/api/compat/agents/[id]/resume/route.ts index 4e1a490a9..01209dfd9 100644 --- a/app/api/compat/agents/[id]/resume/route.ts +++ b/app/api/compat/agents/[id]/resume/route.ts @@ -3,7 +3,11 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { envelope, errorEnvelope, toCompatOpResult } from "@/lib/api/compat-envelope"; +import { + envelope, + errorEnvelope, + toCompatOpResult, +} from "@/lib/api/compat-envelope"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { logger } from "@/lib/utils/logger"; import { requireCompatAuth } from "../../../_lib/auth"; @@ -27,7 +31,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { // Org-scoped pre-check: verify agent exists and belongs to this org // before attempting provision (matches restart route pattern). - const agent = await miladySandboxService.getAgentForWrite(agentId, user.organization_id); + const agent = await miladySandboxService.getAgentForWrite( + agentId, + user.organization_id, + ); if (!agent) { return withCompatCors( NextResponse.json(errorEnvelope("Agent not found"), { status: 404 }), @@ -37,11 +44,17 @@ export async function POST(request: NextRequest, { params }: RouteParams) { logger.info("[compat] Resume requested", { agentId }); - const result = await miladySandboxService.provision(agentId, user.organization_id); + const result = await miladySandboxService.provision( + agentId, + user.organization_id, + ); if (!result.success) { - const status = result.error === "Agent is already being provisioned" ? 409 : 500; + const status = + result.error === "Agent is already being provisioned" ? 409 : 500; return withCompatCors( - NextResponse.json(errorEnvelope(result.error ?? "Resume failed"), { status }), + NextResponse.json(errorEnvelope(result.error ?? "Resume failed"), { + status, + }), CORS_METHODS, ); } diff --git a/app/api/compat/agents/[id]/route.ts b/app/api/compat/agents/[id]/route.ts index 788fae298..63f3a5b2d 100644 --- a/app/api/compat/agents/[id]/route.ts +++ b/app/api/compat/agents/[id]/route.ts @@ -32,7 +32,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { user } = await requireCompatAuth(request); const { id: agentId } = await params; - const agent = await miladySandboxService.getAgent(agentId, user.organization_id); + const agent = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!agent) { return withCompatCors( NextResponse.json(errorEnvelope("Agent not found"), { @@ -43,12 +46,17 @@ export async function GET(request: NextRequest, { params }: RouteParams) { } // Resolve wallet info for Docker-backed agents - let walletInfo: { address: string | null; provider: "steward" | "privy" | null } | undefined; + let walletInfo: + | { address: string | null; provider: "steward" | "privy" | null } + | undefined; if (agent.node_id) { try { const stewardAgent = await getStewardAgent(agentId); if (stewardAgent?.walletAddress) { - walletInfo = { address: stewardAgent.walletAddress, provider: "steward" }; + walletInfo = { + address: stewardAgent.walletAddress, + provider: "steward", + }; } } catch { // Steward unreachable — wallet fields will be null @@ -69,7 +77,10 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const { user } = await requireCompatAuth(request); const { id: agentId } = await params; - const deleted = await miladySandboxService.deleteAgent(agentId, user.organization_id); + const deleted = await miladySandboxService.deleteAgent( + agentId, + user.organization_id, + ); if (!deleted.success) { const status = deleted.error === "Agent not found" @@ -84,8 +95,12 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { } const characterId = deleted.deletedSandbox.character_id; - const sandboxConfig = deleted.deletedSandbox.agent_config as Record | null; - const reusesExistingCharacter = reusesExistingMiladyCharacter(sandboxConfig); + const sandboxConfig = deleted.deletedSandbox.agent_config as Record< + string, + unknown + > | null; + const reusesExistingCharacter = + reusesExistingMiladyCharacter(sandboxConfig); // Clean up the linked character row so the token_address unique constraint // is released. Best-effort: log but don't fail the delete if cleanup fails. @@ -97,11 +112,14 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { characterId, }); } catch (charErr) { - logger.warn("[compat] Failed to clean up linked character after agent delete", { - agentId, - characterId, - error: charErr instanceof Error ? charErr.message : String(charErr), - }); + logger.warn( + "[compat] Failed to clean up linked character after agent delete", + { + agentId, + characterId, + error: charErr instanceof Error ? charErr.message : String(charErr), + }, + ); } } diff --git a/app/api/compat/agents/[id]/status/route.ts b/app/api/compat/agents/[id]/status/route.ts index d2fc6c1f8..106e7566f 100644 --- a/app/api/compat/agents/[id]/status/route.ts +++ b/app/api/compat/agents/[id]/status/route.ts @@ -3,7 +3,11 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { envelope, errorEnvelope, toCompatStatus } from "@/lib/api/compat-envelope"; +import { + envelope, + errorEnvelope, + toCompatStatus, +} from "@/lib/api/compat-envelope"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { requireCompatAuth } from "../../../_lib/auth"; import { handleCompatCorsOptions, withCompatCors } from "../../../_lib/cors"; @@ -23,7 +27,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { user } = await requireCompatAuth(request); const { id: agentId } = await params; - const agent = await miladySandboxService.getAgent(agentId, user.organization_id); + const agent = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!agent) { return withCompatCors( NextResponse.json(errorEnvelope("Agent not found"), { status: 404 }), @@ -31,7 +38,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { ); } - return withCompatCors(NextResponse.json(envelope(toCompatStatus(agent))), CORS_METHODS); + return withCompatCors( + NextResponse.json(envelope(toCompatStatus(agent))), + CORS_METHODS, + ); } catch (err) { return handleCompatError(err, CORS_METHODS); } diff --git a/app/api/compat/agents/[id]/suspend/route.ts b/app/api/compat/agents/[id]/suspend/route.ts index f03d6f069..e5d3c2287 100644 --- a/app/api/compat/agents/[id]/suspend/route.ts +++ b/app/api/compat/agents/[id]/suspend/route.ts @@ -4,7 +4,11 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { envelope, errorEnvelope, toCompatOpResult } from "@/lib/api/compat-envelope"; +import { + envelope, + errorEnvelope, + toCompatOpResult, +} from "@/lib/api/compat-envelope"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { logger } from "@/lib/utils/logger"; import { requireCompatAuth } from "../../../_lib/auth"; @@ -31,11 +35,16 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const body = await request.json().catch(() => ({})); const parsed = suspendSchema.safeParse(body); - const reason = parsed.success ? parsed.data.reason : "owner requested suspension"; + const reason = parsed.success + ? parsed.data.reason + : "owner requested suspension"; logger.info("[compat] Suspend requested", { agentId, reason }); - const agent = await miladySandboxService.getAgentForWrite(agentId, user.organization_id); + const agent = await miladySandboxService.getAgentForWrite( + agentId, + user.organization_id, + ); if (!agent) { return withCompatCors( NextResponse.json(errorEnvelope("Agent not found"), { @@ -45,7 +54,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { ); } - const result = await miladySandboxService.shutdown(agentId, user.organization_id); + const result = await miladySandboxService.shutdown( + agentId, + user.organization_id, + ); if (!result.success) { const status = result.error === "Agent not found" diff --git a/app/api/compat/agents/route.ts b/app/api/compat/agents/route.ts index 016ccd524..24b8be2e4 100644 --- a/app/api/compat/agents/route.ts +++ b/app/api/compat/agents/route.ts @@ -4,7 +4,11 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { envelope, toCompatAgent, toCompatCreateResult } from "@/lib/api/compat-envelope"; +import { + envelope, + toCompatAgent, + toCompatCreateResult, +} from "@/lib/api/compat-envelope"; import { stripReservedMiladyConfigKeys } from "@/lib/services/milady-agent-config"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { logger } from "@/lib/utils/logger"; @@ -60,7 +64,9 @@ export async function POST(request: NextRequest) { // Strip reserved __milady* keys from user-supplied agentConfig to prevent // callers from spoofing internal lifecycle flags. - const sanitizedConfig = stripReservedMiladyConfigKeys(parsed.data.agentConfig); + const sanitizedConfig = stripReservedMiladyConfigKeys( + parsed.data.agentConfig, + ); let agent = await miladySandboxService.createAgent({ organizationId: user.organization_id, @@ -78,7 +84,10 @@ export async function POST(request: NextRequest) { let provisionWarning: string | undefined; if (process.env.WAIFU_AUTO_PROVISION === "true") { try { - const result = await miladySandboxService.provision(agent.id, user.organization_id); + const result = await miladySandboxService.provision( + agent.id, + user.organization_id, + ); if (result.success && result.sandboxRecord) { agent = result.sandboxRecord; } else if (!result.success) { @@ -104,7 +113,10 @@ export async function POST(request: NextRequest) { ? { ...envelope(data), warning: provisionWarning } : envelope(data); - return withCompatCors(NextResponse.json(responseBody, { status: 201 }), CORS_METHODS); + return withCompatCors( + NextResponse.json(responseBody, { status: 201 }), + CORS_METHODS, + ); } catch (err) { return handleCompatError(err, CORS_METHODS); } diff --git a/app/api/compat/jobs/[jobId]/route.ts b/app/api/compat/jobs/[jobId]/route.ts index 0e3727fcf..a2a382ba2 100644 --- a/app/api/compat/jobs/[jobId]/route.ts +++ b/app/api/compat/jobs/[jobId]/route.ts @@ -5,7 +5,11 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { envelope, errorEnvelope, toCompatJob } from "@/lib/api/compat-envelope"; +import { + envelope, + errorEnvelope, + toCompatJob, +} from "@/lib/api/compat-envelope"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { requireCompatAuth } from "../../_lib/auth"; import { handleCompatCorsOptions, withCompatCors } from "../../_lib/cors"; @@ -25,7 +29,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { user } = await requireCompatAuth(request); const { jobId } = await params; - const agent = await miladySandboxService.getAgent(jobId, user.organization_id); + const agent = await miladySandboxService.getAgent( + jobId, + user.organization_id, + ); if (!agent) { return withCompatCors( NextResponse.json(errorEnvelope("Job not found"), { status: 404 }), @@ -33,7 +40,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { ); } - return withCompatCors(NextResponse.json(envelope(toCompatJob(agent))), CORS_METHODS); + return withCompatCors( + NextResponse.json(envelope(toCompatJob(agent))), + CORS_METHODS, + ); } catch (err) { return handleCompatError(err, CORS_METHODS); } diff --git a/app/api/credits/balance/route.ts b/app/api/credits/balance/route.ts index 2a567af49..a8081bd9e 100644 --- a/app/api/credits/balance/route.ts +++ b/app/api/credits/balance/route.ts @@ -1,5 +1,8 @@ import { type NextRequest, NextResponse } from "next/server"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { organizationsService } from "@/lib/services/organizations"; import { logger } from "@/lib/utils/logger"; diff --git a/app/api/credits/transactions/route.ts b/app/api/credits/transactions/route.ts index b02ff312c..bf9a00afe 100644 --- a/app/api/credits/transactions/route.ts +++ b/app/api/credits/transactions/route.ts @@ -17,7 +17,10 @@ async function handleGET(req: NextRequest) { const { user } = await requireAuthOrApiKeyWithOrg(req); if (!user.organization_id) { - return NextResponse.json({ error: "No organization found" }, { status: 404 }); + return NextResponse.json( + { error: "No organization found" }, + { status: 404 }, + ); } const searchParams = req.nextUrl.searchParams; @@ -36,12 +39,15 @@ async function handleGET(req: NextRequest) { if (hours !== null) { const cutoffTime = new Date(Date.now() - hours * 60 * 60 * 1000); - transactions = allTransactions.filter((t) => new Date(t.created_at) >= cutoffTime); + transactions = allTransactions.filter( + (t) => new Date(t.created_at) >= cutoffTime, + ); } const periodStart = hours ? new Date(Date.now() - hours * 60 * 60 * 1000).toISOString() - : transactions[transactions.length - 1]?.created_at || new Date().toISOString(); + : transactions[transactions.length - 1]?.created_at || + new Date().toISOString(); const periodEnd = new Date().toISOString(); @@ -71,7 +77,10 @@ async function handleGET(req: NextRequest) { return NextResponse.json({ error: error.message }, { status: 500 }); } - return NextResponse.json({ error: "Failed to fetch transactions" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch transactions" }, + { status: 500 }, + ); } } diff --git a/app/api/cron/agent-budgets/route.ts b/app/api/cron/agent-budgets/route.ts index 14c1251bc..065e66e25 100644 --- a/app/api/cron/agent-budgets/route.ts +++ b/app/api/cron/agent-budgets/route.ts @@ -72,6 +72,10 @@ export async function GET(request: NextRequest): Promise { return NextResponse.json({ status: "ready", description: "Agent budget maintenance cron job", - tasks: ["Auto-refill low budgets", "Reset daily spending limits", "Send low budget alerts"], + tasks: [ + "Auto-refill low budgets", + "Reset daily spending limits", + "Send low budget alerts", + ], }); } diff --git a/app/api/cron/auto-top-up/route.ts b/app/api/cron/auto-top-up/route.ts index 79ae21918..4f1ea8a46 100644 --- a/app/api/cron/auto-top-up/route.ts +++ b/app/api/cron/auto-top-up/route.ts @@ -43,7 +43,10 @@ export async function GET(request: NextRequest) { if (!cronSecret) { logger.error("auto-top-up-cron", "CRON_SECRET not configured"); - return NextResponse.json({ error: "Cron not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Cron not configured" }, + { status: 500 }, + ); } const providedSecret = authHeader?.replace("Bearer ", "") || ""; @@ -107,7 +110,8 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Auto top-up check failed", + error: + error instanceof Error ? error.message : "Auto top-up check failed", duration: `${duration}ms`, }, { status: 500 }, diff --git a/app/api/cron/cleanup-anonymous-sessions/route.ts b/app/api/cron/cleanup-anonymous-sessions/route.ts index bb903e1bb..a4b47470a 100644 --- a/app/api/cron/cleanup-anonymous-sessions/route.ts +++ b/app/api/cron/cleanup-anonymous-sessions/route.ts @@ -46,7 +46,10 @@ export async function GET(request: NextRequest) { if (!cronSecret) { logger.error("cleanup-cron", "CRON_SECRET not configured"); - return NextResponse.json({ error: "Cron not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Cron not configured" }, + { status: 500 }, + ); } const providedSecret = authHeader?.replace("Bearer ", ""); @@ -67,20 +70,31 @@ export async function GET(request: NextRequest) { .select({ id: users.id }) .from(users) .innerJoin(userIdentities, eq(users.id, userIdentities.user_id)) - .where(and(eq(userIdentities.is_anonymous, true), lt(userIdentities.expires_at!, now))); + .where( + and( + eq(userIdentities.is_anonymous, true), + lt(userIdentities.expires_at!, now), + ), + ); const expiredUsers = expiredUserRows; - logger.info("cleanup-cron", `Found ${expiredUsers.length} expired anonymous users`); + logger.info( + "cleanup-cron", + `Found ${expiredUsers.length} expired anonymous users`, + ); // Step 2: Delete conversations for expired users (optional - decide if you want to keep them) if (expiredUsers.length > 0) { const userIds = expiredUsers.map((u) => u.id); // Count conversations to be deleted - const conversationsToDelete = await dbRead.select().from(conversations).where( - eq(conversations.user_id, userIds[0]), // We'll delete one by one - ); + const conversationsToDelete = await dbRead + .select() + .from(conversations) + .where( + eq(conversations.user_id, userIds[0]), // We'll delete one by one + ); deletedConversations = conversationsToDelete.length; @@ -117,7 +131,12 @@ export async function GET(request: NextRequest) { }) .from(users) .leftJoin(anonymousSessions, eq(anonymousSessions.user_id, users.id)) - .where(and(eq(userIdentities.is_anonymous, true), lt(users.created_at, sevenDaysAgo))); + .where( + and( + eq(userIdentities.is_anonymous, true), + lt(users.created_at, sevenDaysAgo), + ), + ); let deletedInactiveUsers = 0; for (const record of inactiveUsersWithSessions) { @@ -127,7 +146,10 @@ export async function GET(request: NextRequest) { } } - logger.info("cleanup-cron", `Deleted ${deletedInactiveUsers} inactive anonymous users`); + logger.info( + "cleanup-cron", + `Deleted ${deletedInactiveUsers} inactive anonymous users`, + ); // Return summary return NextResponse.json({ diff --git a/app/api/cron/cleanup-cli-sessions/route.ts b/app/api/cron/cleanup-cli-sessions/route.ts index 851d86251..704dcfaaa 100644 --- a/app/api/cron/cleanup-cli-sessions/route.ts +++ b/app/api/cron/cleanup-cli-sessions/route.ts @@ -29,6 +29,9 @@ export async function GET(request: NextRequest) { }); } catch (error) { logger.error("Error cleaning up CLI auth sessions:", error); - return NextResponse.json({ error: "Failed to clean up sessions" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to clean up sessions" }, + { status: 500 }, + ); } } diff --git a/app/api/cron/cleanup-expired-crypto-payments/route.ts b/app/api/cron/cleanup-expired-crypto-payments/route.ts index 52c94167a..c1ccc4137 100644 --- a/app/api/cron/cleanup-expired-crypto-payments/route.ts +++ b/app/api/cron/cleanup-expired-crypto-payments/route.ts @@ -27,7 +27,8 @@ export async function GET(req: NextRequest) { try { logger.info("[Crypto Payments Cleanup] Starting expired payments cleanup"); - const expiredPayments = await cryptoPaymentsService.listExpiredPendingPayments(); + const expiredPayments = + await cryptoPaymentsService.listExpiredPendingPayments(); if (expiredPayments.length === 0) { logger.info("[Crypto Payments Cleanup] No expired payments found"); @@ -53,10 +54,13 @@ export async function GET(req: NextRequest) { }); } catch (error) { errors++; - logger.error("[Crypto Payments Cleanup] Failed to mark payment as expired", { - paymentId: payment.id, - error, - }); + logger.error( + "[Crypto Payments Cleanup] Failed to mark payment as expired", + { + paymentId: payment.id, + error, + }, + ); } } diff --git a/app/api/cron/cleanup-webhook-events/route.ts b/app/api/cron/cleanup-webhook-events/route.ts index af5aed8ec..ef5965dd9 100644 --- a/app/api/cron/cleanup-webhook-events/route.ts +++ b/app/api/cron/cleanup-webhook-events/route.ts @@ -31,9 +31,12 @@ export async function GET(req: NextRequest) { } try { - logger.info("[Webhook Events Cleanup] Starting old webhook events cleanup", { - retentionDays: WEBHOOK_EVENT_RETENTION_DAYS, - }); + logger.info( + "[Webhook Events Cleanup] Starting old webhook events cleanup", + { + retentionDays: WEBHOOK_EVENT_RETENTION_DAYS, + }, + ); const deletedCount = await webhookEventsRepository.cleanupOldEvents( WEBHOOK_EVENT_RETENTION_DAYS, diff --git a/app/api/cron/compute-metrics/route.ts b/app/api/cron/compute-metrics/route.ts index edf860c29..ae4bb0d54 100644 --- a/app/api/cron/compute-metrics/route.ts +++ b/app/api/cron/compute-metrics/route.ts @@ -44,7 +44,9 @@ function verifyCronSecret(request: NextRequest): boolean { return timingSafeEqual(a, b); } -async function handleComputeMetrics(request: NextRequest): Promise { +async function handleComputeMetrics( + request: NextRequest, +): Promise { const startTime = Date.now(); if (!verifyCronSecret(request)) { @@ -83,7 +85,8 @@ async function handleComputeMetrics(request: NextRequest): Promise return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Metrics computation failed", + error: + error instanceof Error ? error.message : "Metrics computation failed", }, { status: 500 }, ); diff --git a/app/api/cron/container-billing/route.ts b/app/api/cron/container-billing/route.ts index ad9450b65..0d897b8ce 100644 --- a/app/api/cron/container-billing/route.ts +++ b/app/api/cron/container-billing/route.ts @@ -20,7 +20,10 @@ import { creditTransactions } from "@/db/schemas/credit-transactions"; import { organizationBilling } from "@/db/schemas/organization-billing"; import { organizations } from "@/db/schemas/organizations"; import { trackServerEvent } from "@/lib/analytics/posthog-server"; -import { CONTAINER_PRICING, calculateDailyContainerCost } from "@/lib/constants/pricing"; +import { + CONTAINER_PRICING, + calculateDailyContainerCost, +} from "@/lib/constants/pricing"; import { emailService } from "@/lib/services/email"; import { logger } from "@/lib/utils/logger"; @@ -58,7 +61,8 @@ function verifyCronSecret(request: NextRequest): boolean { const secretBuffer = Buffer.from(cronSecret, "utf8"); return ( - providedBuffer.length === secretBuffer.length && timingSafeEqual(providedBuffer, secretBuffer) + providedBuffer.length === secretBuffer.length && + timingSafeEqual(providedBuffer, secretBuffer) ); } @@ -130,12 +134,16 @@ async function processContainerBilling( .where(eq(containers.id, containerId)); // Track shutdown event - trackServerEvent(container.user_id, "container_shutdown_insufficient_credits", { - container_id: containerId, - container_name: containerName, - organization_id: organizationId, - balance_at_shutdown: currentBalance, - }); + trackServerEvent( + container.user_id, + "container_shutdown_insufficient_credits", + { + container_id: containerId, + container_name: containerName, + organization_id: organizationId, + balance_at_shutdown: currentBalance, + }, + ); return { containerId, @@ -148,10 +156,14 @@ async function processContainerBilling( // Check if we have enough credits if (currentBalance < dailyCost) { // Insufficient credits - check if we need to send warning - if (container.billing_status === "active" || !container.shutdown_warning_sent_at) { + if ( + container.billing_status === "active" || + !container.shutdown_warning_sent_at + ) { // Send 48-hour warning and schedule shutdown const shutdownTime = new Date( - now.getTime() + CONTAINER_PRICING.SHUTDOWN_WARNING_HOURS * 60 * 60 * 1000, + now.getTime() + + CONTAINER_PRICING.SHUTDOWN_WARNING_HOURS * 60 * 60 * 1000, ); await dbWrite @@ -165,9 +177,11 @@ async function processContainerBilling( .where(eq(containers.id, containerId)); // Send warning email - const recipientEmail = org.billing_email || (await getOrgUserEmail(organizationId)); + const recipientEmail = + org.billing_email || (await getOrgUserEmail(organizationId)); if (recipientEmail) { - const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const appUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; await emailService.sendContainerShutdownWarningEmail({ email: recipientEmail, organizationName: org.name, @@ -299,11 +313,14 @@ async function processContainerBilling( return { newBalance, transactionId: creditTx.id }; }); - logger.info(`[Container Billing] Billed ${containerName}: $${dailyCost.toFixed(2)}`, { - containerId, - newBalance: billingResult.newBalance, - transactionId: billingResult.transactionId, - }); + logger.info( + `[Container Billing] Billed ${containerName}: $${dailyCost.toFixed(2)}`, + { + containerId, + newBalance: billingResult.newBalance, + transactionId: billingResult.transactionId, + }, + ); // Track billing event trackServerEvent(container.user_id, "container_daily_billed", { @@ -343,7 +360,9 @@ async function getOrgUserEmail(organizationId: string): Promise { /** * Main billing handler */ -async function handleContainerBilling(request: NextRequest): Promise { +async function handleContainerBilling( + request: NextRequest, +): Promise { const startTime = Date.now(); if (!verifyCronSecret(request)) { @@ -375,7 +394,11 @@ async function handleContainerBilling(request: NextRequest): Promise c.organization_id))]; + const orgIds = [ + ...new Set(runningContainers.map((c) => c.organization_id)), + ]; // Fetch all organizations at once const orgs = await dbRead @@ -419,10 +446,15 @@ async function handleContainerBilling(request: NextRequest): Promise [b.organization_id, b.billing_email])); + const billingEmailMap = new Map( + billingData.map((b) => [b.organization_id, b.billing_email]), + ); const orgMap = new Map( - orgs.map((o) => [o.id, { ...o, billing_email: billingEmailMap.get(o.id) ?? null }]), + orgs.map((o) => [ + o.id, + { ...o, billing_email: billingEmailMap.get(o.id) ?? null }, + ]), ); // Process each container @@ -464,7 +496,10 @@ async function handleContainerBilling(request: NextRequest): Promise { - if (sandbox.billing_status === "shutdown_pending" || sandbox.shutdown_warning_sent_at) { + if ( + sandbox.billing_status === "shutdown_pending" || + sandbox.shutdown_warning_sent_at + ) { return { sandboxId, agentName, @@ -189,9 +206,11 @@ async function processSandboxBilling( }) .where(eq(miladySandboxes.id, sandboxId)); - const recipientEmail = org.billing_email || (await getOrgUserEmail(organizationId)); + const recipientEmail = + org.billing_email || (await getOrgUserEmail(organizationId)); if (recipientEmail) { - const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const appUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; // Reuse the container shutdown warning email template — content is generic enough await emailService.sendContainerShutdownWarningEmail({ email: recipientEmail, @@ -216,7 +235,9 @@ async function processSandboxBilling( dashboardUrl: `${appUrl}/dashboard/milady`, }); - logger.info(`[Milady Billing] Sent shutdown warning for ${agentName} to ${recipientEmail}`); + logger.info( + `[Milady Billing] Sent shutdown warning for ${agentName} to ${recipientEmail}`, + ); } trackServerEvent(sandbox.user_id, "milady_agent_shutdown_warning_sent", { @@ -251,7 +272,9 @@ async function processSandboxBilling( sandbox.scheduled_shutdown_at && new Date(sandbox.scheduled_shutdown_at) <= now ) { - logger.info(`[Milady Billing] Shutting down agent ${agentName} due to insufficient credits`); + logger.info( + `[Milady Billing] Shutting down agent ${agentName} due to insufficient credits`, + ); await dbWrite .update(miladySandboxes) @@ -265,12 +288,16 @@ async function processSandboxBilling( }) .where(eq(miladySandboxes.id, sandboxId)); - trackServerEvent(sandbox.user_id, "milady_agent_shutdown_insufficient_credits", { - sandbox_id: sandboxId, - agent_name: agentName, - organization_id: organizationId, - balance_at_shutdown: currentBalance, - }); + trackServerEvent( + sandbox.user_id, + "milady_agent_shutdown_insufficient_credits", + { + sandbox_id: sandboxId, + agent_name: agentName, + organization_id: organizationId, + balance_at_shutdown: currentBalance, + }, + ); return { sandboxId, agentName, organizationId, action: "shutdown" }; } @@ -283,7 +310,9 @@ async function processSandboxBilling( let billingResult: { newBalance: number; transactionId: string }; try { billingResult = await dbWrite.transaction(async (tx) => { - const rebillCutoff = new Date(now.getTime() - REBILL_GUARD_MINUTES * 60_000); + const rebillCutoff = new Date( + now.getTime() - REBILL_GUARD_MINUTES * 60_000, + ); // Claim the sandbox row up front so overlapping cron runs serialize on the same record. const [claimedSandbox] = await tx .update(miladySandboxes) @@ -336,7 +365,8 @@ async function processSandboxBilling( metadata: { sandbox_id: sandboxId, agent_name: agentName, - billing_type: sandbox.status === "running" ? "milady_running" : "milady_idle", + billing_type: + sandbox.status === "running" ? "milady_running" : "milady_idle", hourly_rate: hourlyCost, billing_hour: now.toISOString(), }, @@ -387,11 +417,14 @@ async function processSandboxBilling( throw error; } - logger.info(`[Milady Billing] Billed ${agentName}: $${hourlyCost.toFixed(4)}`, { - sandboxId, - newBalance: billingResult.newBalance, - transactionId: billingResult.transactionId, - }); + logger.info( + `[Milady Billing] Billed ${agentName}: $${hourlyCost.toFixed(4)}`, + { + sandboxId, + newBalance: billingResult.newBalance, + transactionId: billingResult.transactionId, + }, + ); trackServerEvent(sandbox.user_id, "milady_agent_hourly_billed", { sandbox_id: sandboxId, @@ -413,7 +446,9 @@ async function processSandboxBilling( // ── Main Handler ────────────────────────────────────────────────────── -async function handleMiladyBilling(request: NextRequest): Promise { +async function handleMiladyBilling( + request: NextRequest, +): Promise { const startTime = Date.now(); const now = new Date(); const rebillCutoff = new Date(now.getTime() - REBILL_GUARD_MINUTES * 60_000); @@ -540,9 +575,14 @@ async function handleMiladyBilling(request: NextRequest): Promise .from(organizationBilling) .where(inArray(organizationBilling.organization_id, orgIds)); - const billingEmailMap = new Map(billingData.map((b) => [b.organization_id, b.billing_email])); + const billingEmailMap = new Map( + billingData.map((b) => [b.organization_id, b.billing_email]), + ); const orgMap = new Map( - orgs.map((o) => [o.id, { ...o, billing_email: billingEmailMap.get(o.id) ?? null }]), + orgs.map((o) => [ + o.id, + { ...o, billing_email: billingEmailMap.get(o.id) ?? null }, + ]), ); // ── Process each sandbox ──────────────────────────────────────── diff --git a/app/api/cron/process-redemptions/route.ts b/app/api/cron/process-redemptions/route.ts index 8b9906a6a..cf6038d9b 100644 --- a/app/api/cron/process-redemptions/route.ts +++ b/app/api/cron/process-redemptions/route.ts @@ -40,7 +40,9 @@ function verifyCronSecret(request: NextRequest): boolean { } // Support both "Bearer " and just "" - const providedSecret = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader; + const providedSecret = authHeader.startsWith("Bearer ") + ? authHeader.slice(7) + : authHeader; // SECURITY: Timing-safe comparison to prevent timing attacks try { @@ -66,13 +68,18 @@ export async function POST(request: NextRequest): Promise { // Verify cron secret if (!verifyCronSecret(request)) { logger.warn("[Redemption Cron] Unauthorized access attempt"); - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); } logger.info("[Redemption Cron] Starting redemption processing"); // Check if payout processor is configured (support both naming conventions) - const evmConfigured = !!(process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY); + const evmConfigured = !!( + process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY + ); const solanaConfigured = !!process.env.SOLANA_PAYOUT_PRIVATE_KEY; if (!evmConfigured && !solanaConfigured) { @@ -100,7 +107,9 @@ export async function POST(request: NextRequest): Promise { solanaConfigured, balances: { evm: balances.evm.configured ? balances.evm.balances : "not configured", - solana: balances.solana.configured ? balances.solana.balance : "not configured", + solana: balances.solana.configured + ? balances.solana.balance + : "not configured", }, }); } @@ -110,7 +119,9 @@ export async function POST(request: NextRequest): Promise { */ export async function GET(request: NextRequest): Promise { // Allow health checks without auth for monitoring (support both naming conventions) - const evmConfigured = !!(process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY); + const evmConfigured = !!( + process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY + ); const solanaConfigured = !!process.env.SOLANA_PAYOUT_PRIVATE_KEY; return NextResponse.json({ diff --git a/app/api/cron/release-pending-earnings/route.ts b/app/api/cron/release-pending-earnings/route.ts index 25461471c..2f5e60e1e 100644 --- a/app/api/cron/release-pending-earnings/route.ts +++ b/app/api/cron/release-pending-earnings/route.ts @@ -22,7 +22,10 @@ import { timingSafeEqual } from "crypto"; import { and, gt, lte, sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; import { dbRead, dbWrite } from "@/db/client"; -import { appEarnings, appEarningsTransactions } from "@/db/schemas/app-earnings"; +import { + appEarnings, + appEarningsTransactions, +} from "@/db/schemas/app-earnings"; import { VESTING_CONFIG } from "@/lib/config/redemption-addresses"; import { logger } from "@/lib/utils/logger"; @@ -43,7 +46,9 @@ function verifyCronSecret(request: NextRequest): boolean { return false; } - const providedSecret = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader; + const providedSecret = authHeader.startsWith("Bearer ") + ? authHeader.slice(7) + : authHeader; // SECURITY: Timing-safe comparison to prevent timing attacks try { @@ -73,7 +78,9 @@ export async function GET(request: NextRequest) { let totalReleased = 0; // Calculate cutoff date for standard earnings (7 days ago) - const cutoffDate = new Date(Date.now() - VESTING_CONFIG.APP_EARNINGS_HOLD_PERIOD_MS); + const cutoffDate = new Date( + Date.now() - VESTING_CONFIG.APP_EARNINGS_HOLD_PERIOD_MS, + ); // Find all apps with pending balances where earnings are old enough // We look at the oldest transaction to determine if balance can be released @@ -153,7 +160,8 @@ export async function GET(request: NextRequest) { description: `Vesting release: $${amountToRelease.toFixed(2)} now withdrawable`, metadata: { released_at: new Date().toISOString(), - vesting_period_days: VESTING_CONFIG.APP_EARNINGS_HOLD_PERIOD_MS / (24 * 60 * 60 * 1000), + vesting_period_days: + VESTING_CONFIG.APP_EARNINGS_HOLD_PERIOD_MS / (24 * 60 * 60 * 1000), }, }); }); diff --git a/app/api/cron/sample-eliza-price/route.ts b/app/api/cron/sample-eliza-price/route.ts index 10e9104d4..5f3bd1fde 100644 --- a/app/api/cron/sample-eliza-price/route.ts +++ b/app/api/cron/sample-eliza-price/route.ts @@ -15,7 +15,10 @@ import { timingSafeEqual } from "crypto"; import { NextRequest, NextResponse } from "next/server"; -import { elizaTokenPriceService, type SupportedNetwork } from "@/lib/services/eliza-token-price"; +import { + elizaTokenPriceService, + type SupportedNetwork, +} from "@/lib/services/eliza-token-price"; import { twapPriceOracle } from "@/lib/services/twap-price-oracle"; import { logger } from "@/lib/utils/logger"; @@ -38,7 +41,9 @@ function verifyCronSecret(request: NextRequest): boolean { return false; } - const providedSecret = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader; + const providedSecret = authHeader.startsWith("Bearer ") + ? authHeader.slice(7) + : authHeader; // SECURITY: Timing-safe comparison to prevent timing attacks try { @@ -71,7 +76,10 @@ export async function POST(request: NextRequest): Promise { // Verify cron secret if (!verifyCronSecret(request)) { logger.warn("[PriceSample Cron] Unauthorized access attempt"); - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); } logger.info("[PriceSample Cron] Starting price sampling"); @@ -87,7 +95,11 @@ export async function POST(request: NextRequest): Promise { const quote = await elizaTokenPriceService.getPrice(network); // Record the sample for TWAP calculation - await twapPriceOracle.recordPriceSample(network, quote.priceUsd, quote.source); + await twapPriceOracle.recordPriceSample( + network, + quote.priceUsd, + quote.source, + ); results.push({ network, @@ -102,7 +114,8 @@ export async function POST(request: NextRequest): Promise { source: quote.source, }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; results.push({ network, success: false, diff --git a/app/api/cron/social-automation/route.ts b/app/api/cron/social-automation/route.ts index 1d87e3da3..b0f9e109c 100644 --- a/app/api/cron/social-automation/route.ts +++ b/app/api/cron/social-automation/route.ts @@ -80,10 +80,12 @@ function isAnnouncementDue( ): boolean { if (!config.enabled) return false; - const autoEnabled = type === "announcement" ? config.autoAnnounce : config.autoPost; + const autoEnabled = + type === "announcement" ? config.autoAnnounce : config.autoPost; if (!autoEnabled) return false; - const lastTime = type === "announcement" ? config.lastAnnouncementAt : config.lastPostAt; + const lastTime = + type === "announcement" ? config.lastAnnouncementAt : config.lastPostAt; if (!lastTime) return true; const lastDate = new Date(lastTime); @@ -106,7 +108,8 @@ function isAnnouncementDue( // Between min and max: use hash-based threshold to distribute posts // Each app gets a different position in the window based on its ID - const windowProgress = (minutesSince - minInterval) / (maxInterval - minInterval); + const windowProgress = + (minutesSince - minInterval) / (maxInterval - minInterval); const threshold = appId ? hashToFraction(appId + type) : 0.5; return windowProgress >= threshold; } @@ -145,7 +148,10 @@ async function processDiscordAutomation({ const isDue = isAnnouncementDue(automationConfig, "announcement", app.id); if (!isDue) return null; - const result = await discordAppAutomationService.postAnnouncement(app.organization_id, app.id); + const result = await discordAppAutomationService.postAnnouncement( + app.organization_id, + app.id, + ); return { appId: app.id, @@ -161,13 +167,17 @@ async function processTelegramAutomation({ app, config, }: AppWithConfig): Promise { - const automationConfig = config.telegram_automation as AutomationConfig | null; + const automationConfig = + config.telegram_automation as AutomationConfig | null; if (!automationConfig?.enabled || !automationConfig.autoAnnounce) return null; const isDue = isAnnouncementDue(automationConfig, "announcement", app.id); if (!isDue) return null; - const result = await telegramAppAutomationService.postAnnouncement(app.organization_id, app.id); + const result = await telegramAppAutomationService.postAnnouncement( + app.organization_id, + app.id, + ); return { appId: app.id, @@ -189,7 +199,10 @@ async function processTwitterAutomation({ const isDue = isAnnouncementDue(automationConfig, "post", app.id); if (!isDue) return null; - const result = await twitterAppAutomationService.postAppTweet(app.organization_id, app.id); + const result = await twitterAppAutomationService.postAppTweet( + app.organization_id, + app.id, + ); return { appId: app.id, @@ -295,7 +308,10 @@ export async function POST(request: NextRequest): Promise { }); // Process apps in parallel with concurrency limit - const results = await processAppsWithConcurrency(appsWithAutomation, MAX_CONCURRENT_POSTS); + const results = await processAppsWithConcurrency( + appsWithAutomation, + MAX_CONCURRENT_POSTS, + ); const duration = Date.now() - startTime; const successCount = results.filter((r) => r.success).length; diff --git a/app/api/crypto/payments/[id]/confirm/route.ts b/app/api/crypto/payments/[id]/confirm/route.ts index 7d472559a..20f810897 100644 --- a/app/api/crypto/payments/[id]/confirm/route.ts +++ b/app/api/crypto/payments/[id]/confirm/route.ts @@ -57,11 +57,17 @@ function validateTransactionHashFormat(hash: string, network: string): boolean { return ethereumTxHashRegex.test(hash); } - if (normalizedNetwork.includes("TRC20") || normalizedNetwork.includes("TRON")) { + if ( + normalizedNetwork.includes("TRC20") || + normalizedNetwork.includes("TRON") + ) { return tronTxHashRegex.test(hash); } - if (normalizedNetwork.includes("SOL") || normalizedNetwork.includes("SOLANA")) { + if ( + normalizedNetwork.includes("SOL") || + normalizedNetwork.includes("SOLANA") + ) { return solanaTxHashRegex.test(hash); } @@ -91,7 +97,10 @@ async function handleConfirmPayment( const { id } = await context.params; if (!user.organization_id) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } const payment = await cryptoPaymentsRepository.findById(id); @@ -125,7 +134,10 @@ async function handleConfirmPayment( } if (payment.status === "expired") { - return NextResponse.json({ error: "Payment has expired" }, { status: 400 }); + return NextResponse.json( + { error: "Payment has expired" }, + { status: 400 }, + ); } const body = await req.json(); @@ -150,13 +162,16 @@ async function handleConfirmPayment( // Format validation for fast rejection - full on-chain verification via OxaPay happens in verifyAndConfirmByTxHash() if (!validateTransactionHashFormat(transactionHash, payment.network)) { - logger.warn("[Crypto Payments API] Invalid transaction hash format for network", { - paymentId: redact.paymentId(id), - ip: redact.ip(ip), - userId: redact.userId(user.id), - network: payment.network, - txHashLength: transactionHash.length, - }); + logger.warn( + "[Crypto Payments API] Invalid transaction hash format for network", + { + paymentId: redact.paymentId(id), + ip: redact.ip(ip), + userId: redact.userId(user.id), + network: payment.network, + txHashLength: transactionHash.length, + }, + ); return NextResponse.json( { error: `Invalid transaction hash format for ${payment.network} network`, @@ -173,7 +188,10 @@ async function handleConfirmPayment( ip: redact.ip(ip), }); - const result = await cryptoPaymentsService.verifyAndConfirmByTxHash(id, transactionHash); + const result = await cryptoPaymentsService.verifyAndConfirmByTxHash( + id, + transactionHash, + ); if (result.success) { logger.info("[Crypto Payments API] Manual confirmation successful", { @@ -208,8 +226,14 @@ async function handleConfirmPayment( ip: redact.ip(ip), error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to process confirmation" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to process confirmation" }, + { status: 500 }, + ); } } -export const POST = withRateLimit(handleConfirmPayment, RateLimitPresets.STRICT); +export const POST = withRateLimit( + handleConfirmPayment, + RateLimitPresets.STRICT, +); diff --git a/app/api/crypto/payments/[id]/route.ts b/app/api/crypto/payments/[id]/route.ts index 3676736d0..0d83231c3 100644 --- a/app/api/crypto/payments/[id]/route.ts +++ b/app/api/crypto/payments/[id]/route.ts @@ -14,16 +14,25 @@ async function handleGetPayment(req: NextRequest, context: RouteContext) { try { const { user } = await requireAuthOrApiKeyWithOrg(req); if (!context) { - return NextResponse.json({ error: "Missing route params" }, { status: 400 }); + return NextResponse.json( + { error: "Missing route params" }, + { status: 400 }, + ); } const { id } = await context.params; if (!z.string().uuid().safeParse(id).success) { - return NextResponse.json({ error: "Invalid payment ID" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid payment ID" }, + { status: 400 }, + ); } if (!user.organization_id) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } const payment = await cryptoPaymentsRepository.findById(id); @@ -36,7 +45,8 @@ async function handleGetPayment(req: NextRequest, context: RouteContext) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } - const { confirmed, payment: status } = await cryptoPaymentsService.checkAndConfirmPayment(id); + const { confirmed, payment: status } = + await cryptoPaymentsService.checkAndConfirmPayment(id); return NextResponse.json({ ...status, @@ -44,7 +54,10 @@ async function handleGetPayment(req: NextRequest, context: RouteContext) { }); } catch (error) { logger.error("[Crypto Payments API] Get payment error:", error); - return NextResponse.json({ error: "Failed to get payment status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get payment status" }, + { status: 500 }, + ); } } diff --git a/app/api/crypto/payments/route.ts b/app/api/crypto/payments/route.ts index e683ea015..20da86dfd 100644 --- a/app/api/crypto/payments/route.ts +++ b/app/api/crypto/payments/route.ts @@ -4,15 +4,23 @@ import { trackServerEvent } from "@/lib/analytics/posthog-server"; import { requireAuthOrApiKeyWithOrg, requireAuthWithOrg } from "@/lib/auth"; import { SUPPORTED_PAY_CURRENCIES } from "@/lib/config/crypto"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { CryptoPaymentError, cryptoPaymentsService } from "@/lib/services/crypto-payments"; +import { + CryptoPaymentError, + cryptoPaymentsService, +} from "@/lib/services/crypto-payments"; import { isOxaPayConfigured } from "@/lib/services/oxapay"; import { logger } from "@/lib/utils/logger"; const createPaymentSchema = z.object({ - amount: z.number().min(1, "Minimum amount is $1").max(10000, "Maximum amount is $10,000"), + amount: z + .number() + .min(1, "Minimum amount is $1") + .max(10000, "Maximum amount is $10,000"), currency: z.string().default("USD"), payCurrency: z.enum(SUPPORTED_PAY_CURRENCIES).default("USDT"), - network: z.enum(["ERC20", "TRC20", "BEP20", "POLYGON", "SOL", "BASE", "ARB", "OP"]).optional(), + network: z + .enum(["ERC20", "TRC20", "BEP20", "POLYGON", "SOL", "BASE", "ARB", "OP"]) + .optional(), }); async function handleCreatePayment(req: NextRequest) { @@ -20,11 +28,17 @@ async function handleCreatePayment(req: NextRequest) { const user = await requireAuthWithOrg(); if (!user.organization_id) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } if (!isOxaPayConfigured()) { - return NextResponse.json({ error: "Crypto payments not available" }, { status: 503 }); + return NextResponse.json( + { error: "Crypto payments not available" }, + { status: 503 }, + ); } const body = await req.json(); @@ -96,10 +110,16 @@ async function handleCreatePayment(req: NextRequest) { status: 500, message: error.message, }; - return NextResponse.json({ error: response.message }, { status: response.status }); + return NextResponse.json( + { error: response.message }, + { status: response.status }, + ); } - return NextResponse.json({ error: "Failed to process payment request" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to process payment request" }, + { status: 500 }, + ); } } @@ -108,10 +128,15 @@ async function handleListPayments(req: NextRequest) { const { user } = await requireAuthOrApiKeyWithOrg(req); if (!user.organization_id) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } - const payments = await cryptoPaymentsService.listPaymentsByOrganization(user.organization_id); + const payments = await cryptoPaymentsService.listPaymentsByOrganization( + user.organization_id, + ); return NextResponse.json({ payments }); } catch (error) { @@ -119,11 +144,17 @@ async function handleListPayments(req: NextRequest) { if (error instanceof CryptoPaymentError) { if (error.code === "INVALID_UUID") { - return NextResponse.json({ error: "Invalid request format" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request format" }, + { status: 400 }, + ); } } - return NextResponse.json({ error: "Failed to retrieve payments" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to retrieve payments" }, + { status: 500 }, + ); } } diff --git a/app/api/crypto/webhook/route.ts b/app/api/crypto/webhook/route.ts index dd1020a6f..0fcd0569f 100644 --- a/app/api/crypto/webhook/route.ts +++ b/app/api/crypto/webhook/route.ts @@ -72,7 +72,11 @@ function getWebhookHmacSecret(): string | null { return process.env.OXAPAY_MERCHANT_API_KEY || null; } -function verifyOxaPaySignature(payload: string, signature: string | null, ip: string): boolean { +function verifyOxaPaySignature( + payload: string, + signature: string | null, + ip: string, +): boolean { const secret = getWebhookHmacSecret(); if (!secret) { @@ -90,7 +94,9 @@ function verifyOxaPaySignature(payload: string, signature: string | null, ip: st return false; } - const expectedSignature = createHmac("sha512", secret).update(payload).digest("hex"); + const expectedSignature = createHmac("sha512", secret) + .update(payload) + .digest("hex"); try { const sigBuffer = Buffer.from(signature, "hex"); @@ -119,7 +125,11 @@ function verifyOxaPaySignature(payload: string, signature: string | null, ip: st * Generates a unique event ID for webhook deduplication. * Combines track_id, status, and payload hash to create a unique identifier. */ -function generateWebhookEventId(trackId: string, status: string, payloadHash: string): string { +function generateWebhookEventId( + trackId: string, + status: string, + payloadHash: string, +): string { return `oxapay_${trackId}_${status}_${payloadHash}`; } @@ -144,7 +154,10 @@ async function handleWebhook(req: NextRequest) { logger.warn("[Crypto Webhook] Service not configured", { ip: redact.ip(ip), }); - return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); + return NextResponse.json( + { error: "Service unavailable" }, + { status: 503 }, + ); } // Validate that the merchant API key is set - required for portable audit hashes @@ -154,12 +167,16 @@ async function handleWebhook(req: NextRequest) { "[Crypto Webhook] OXAPAY_MERCHANT_API_KEY is required when crypto payments are enabled", { ip: redact.ip(ip) }, ); - return NextResponse.json({ error: "Service misconfigured" }, { status: 503 }); + return NextResponse.json( + { error: "Service misconfigured" }, + { status: 503 }, + ); } const rawBody = await req.text(); const signature = req.headers.get("hmac"); - const timestampHeader = req.headers.get("x-webhook-timestamp") || req.headers.get("timestamp"); + const timestampHeader = + req.headers.get("x-webhook-timestamp") || req.headers.get("timestamp"); // Generate unique hash for audit logging and deduplication const payloadHash = createHmac("sha256", auditHashKey) @@ -168,11 +185,14 @@ async function handleWebhook(req: NextRequest) { .slice(0, 16); if (!verifyOxaPaySignature(rawBody, signature, ip)) { - logger.error("[Crypto Webhook] Signature verification failed - potential security threat", { - ip: redact.ip(ip), - payloadHash, - hasSignature: !!signature, - }); + logger.error( + "[Crypto Webhook] Signature verification failed - potential security threat", + { + ip: redact.ip(ip), + payloadHash, + hasSignature: !!signature, + }, + ); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -197,19 +217,28 @@ async function handleWebhook(req: NextRequest) { hasTrackId: !!normalizedPayload.trackId, hasStatus: !!normalizedPayload.status, }); - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 }, + ); } // Validate webhook timestamp to prevent replay attacks - const webhookTimestampMs = extractWebhookTimestamp(timestampHeader, payload); + const webhookTimestampMs = extractWebhookTimestamp( + timestampHeader, + payload, + ); const timestampValidation = validateWebhookTimestamp(webhookTimestampMs); if (!timestampValidation.isValid) { - logger.warn("[Crypto Webhook] Timestamp validation failed - potential replay attack", { - ip: redact.ip(ip), - payloadHash, - trackId: redact.trackId(normalizedPayload.trackId), - error: timestampValidation.error, - }); + logger.warn( + "[Crypto Webhook] Timestamp validation failed - potential replay attack", + { + ip: redact.ip(ip), + payloadHash, + trackId: redact.trackId(normalizedPayload.trackId), + error: timestampValidation.error, + }, + ); return NextResponse.json( { error: `Webhook rejected: ${timestampValidation.error}` }, { status: 400 }, @@ -296,25 +325,38 @@ async function handleWebhook(req: NextRequest) { ].includes(statusLower); if (needsPaymentLookup) { - let payment: Awaited> | null = null; + let payment: Awaited< + ReturnType + > | null = null; try { - payment = await cryptoPaymentsRepository.findByTrackId(normalizedPayload.trackId); + payment = await cryptoPaymentsRepository.findByTrackId( + normalizedPayload.trackId, + ); } catch (analyticsError) { logger.warn("[Crypto Webhook] Failed to fetch payment for analytics", { trackId: normalizedPayload.trackId, - error: analyticsError instanceof Error ? analyticsError.message : "Unknown error", + error: + analyticsError instanceof Error + ? analyticsError.message + : "Unknown error", }); } if (!payment) { - logger.warn("[Crypto Webhook] Cannot track analytics - payment not found", { - trackId: normalizedPayload.trackId, - }); + logger.warn( + "[Crypto Webhook] Cannot track analytics - payment not found", + { + trackId: normalizedPayload.trackId, + }, + ); } else if (!payment.user_id) { - logger.warn("[Crypto Webhook] Cannot track analytics - missing user_id", { - trackId: normalizedPayload.trackId, - paymentId: payment.id, - }); + logger.warn( + "[Crypto Webhook] Cannot track analytics - missing user_id", + { + trackId: normalizedPayload.trackId, + paymentId: payment.id, + }, + ); } else { // Map crypto status to descriptive error reason for consistency with Stripe const getErrorReason = (status: string): string => { @@ -326,7 +368,11 @@ async function handleWebhook(req: NextRequest) { return errorMap[status] || `Crypto payment ${status}`; }; - if (statusLower === "paid" || statusLower === "complete" || statusLower === "confirmed") { + if ( + statusLower === "paid" || + statusLower === "complete" || + statusLower === "confirmed" + ) { // Payment confirmed - track success events const webhookAmount = normalizedPayload.amount ?? 0; const storedCredits = Number(payment.credits_to_add); diff --git a/app/api/elevenlabs/voices/[id]/route.ts b/app/api/elevenlabs/voices/[id]/route.ts index 87484b4d6..b2426da8f 100644 --- a/app/api/elevenlabs/voices/[id]/route.ts +++ b/app/api/elevenlabs/voices/[id]/route.ts @@ -5,7 +5,8 @@ import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { voiceCloningService } from "@/lib/services/voice-cloning"; import { logger } from "@/lib/utils/logger"; -const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function isValidVoiceId(voiceId: string) { return uuidRegex.test(voiceId); @@ -39,7 +40,8 @@ function getInvalidVoiceIdResponseIfNeeded( function isInvalidVoiceIdError(error: unknown) { return ( error instanceof Error && - (error.message.includes("invalid input syntax for type uuid") || error.message.includes("uuid")) + (error.message.includes("invalid input syntax for type uuid") || + error.message.includes("uuid")) ); } @@ -52,7 +54,10 @@ function isInvalidVoiceIdError(error: unknown) { * @param context - Route context containing the voice ID parameter. * @returns Voice details including ElevenLabs voice ID and metadata. */ -export async function GET(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -69,7 +74,10 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: return invalidVoiceIdResponse; } - const voice = await voiceCloningService.getVoiceById(voiceId, user.organization_id!); + const voice = await voiceCloningService.getVoiceById( + voiceId, + user.organization_id!, + ); if (!voice) { return NextResponse.json( @@ -106,7 +114,10 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: * @param context - Route context containing the voice ID parameter. * @returns Success confirmation. */ -export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function DELETE( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -168,7 +179,10 @@ export async function DELETE(request: NextRequest, context: { params: Promise<{ * @param context - Route context containing the voice ID parameter. * @returns Updated voice details. */ -export async function PATCH(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function PATCH( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -188,12 +202,16 @@ export async function PATCH(request: NextRequest, context: { params: Promise<{ i const { name, description, settings, isActive } = body; - const updatedVoice = await voiceCloningService.updateVoice(voiceId, user.organization_id!, { - name, - description, - settings, - isActive, - }); + const updatedVoice = await voiceCloningService.updateVoice( + voiceId, + user.organization_id!, + { + name, + description, + settings, + isActive, + }, + ); return NextResponse.json({ success: true, diff --git a/app/api/elevenlabs/voices/jobs/route.ts b/app/api/elevenlabs/voices/jobs/route.ts index d4aa95095..2fba4163c 100644 --- a/app/api/elevenlabs/voices/jobs/route.ts +++ b/app/api/elevenlabs/voices/jobs/route.ts @@ -20,7 +20,10 @@ export async function GET(request: NextRequest) { logger.info(`[Voice Jobs API] Fetching jobs for user ${user.id}`); // Get user's jobs (only in-progress ones) - const allJobs = await voiceCloningService.getUserJobs(user.organization_id!, user.id); + const allJobs = await voiceCloningService.getUserJobs( + user.organization_id!, + user.id, + ); // Filter for only processing/pending jobs const activeJobs = allJobs.filter( diff --git a/app/api/elevenlabs/voices/route.ts b/app/api/elevenlabs/voices/route.ts index 0abb7422b..a83943cd8 100644 --- a/app/api/elevenlabs/voices/route.ts +++ b/app/api/elevenlabs/voices/route.ts @@ -44,11 +44,20 @@ export async function GET(request: NextRequest) { const status = getErrorStatusCode(error); if (status !== 500) { - return NextResponse.json({ error: getSafeErrorMessage(error) }, { status }); + return NextResponse.json( + { error: getSafeErrorMessage(error) }, + { status }, + ); } - if (error instanceof Error && error.message.includes("ELEVENLABS_API_KEY")) { - return NextResponse.json({ error: "Service not configured" }, { status: 500 }); + if ( + error instanceof Error && + error.message.includes("ELEVENLABS_API_KEY") + ) { + return NextResponse.json( + { error: "Service not configured" }, + { status: 500 }, + ); } return NextResponse.json( diff --git a/app/api/elevenlabs/voices/user/route.ts b/app/api/elevenlabs/voices/user/route.ts index 52ad0ec8b..d3afd31bb 100644 --- a/app/api/elevenlabs/voices/user/route.ts +++ b/app/api/elevenlabs/voices/user/route.ts @@ -102,7 +102,10 @@ export async function GET(request: NextRequest) { const status = getErrorStatusCode(error); if (status !== 500) { - return NextResponse.json({ error: getSafeErrorMessage(error) }, { status }); + return NextResponse.json( + { error: getSafeErrorMessage(error) }, + { status }, + ); } return NextResponse.json( diff --git a/app/api/elevenlabs/voices/verify/[id]/route.ts b/app/api/elevenlabs/voices/verify/[id]/route.ts index 52d86ce2c..ac4051074 100644 --- a/app/api/elevenlabs/voices/verify/[id]/route.ts +++ b/app/api/elevenlabs/voices/verify/[id]/route.ts @@ -14,7 +14,10 @@ import { logger } from "@/lib/utils/logger"; * @param context - Route context containing the voice ID parameter. * @returns Voice verification status including readiness, TTS capability, and fine-tuning information. */ -export async function GET(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -23,7 +26,10 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: logger.info(`[Voice Verify API] Verifying voice ${voiceId}`); // Get voice from database - const voice = await voiceCloningService.getVoiceById(voiceId, user.organization_id!); + const voice = await voiceCloningService.getVoiceById( + voiceId, + user.organization_id!, + ); if (!voice) { return NextResponse.json({ error: "Voice not found" }, { status: 404 }); @@ -33,7 +39,9 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: const elevenlabs = getElevenLabsService(); try { - const elevenLabsVoice = await elevenlabs.getVoiceById(voice.elevenlabsVoiceId); + const elevenLabsVoice = await elevenlabs.getVoiceById( + voice.elevenlabsVoiceId, + ); // For professional voices, check fine-tuning status const isProfessional = voice.cloneType === "professional"; @@ -45,17 +53,20 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: // Try a test TTS call to verify it actually works let canGenerateTTS = false; try { - await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice.elevenlabsVoiceId}`, { - method: "POST", - headers: { - "xi-api-key": process.env.ELEVENLABS_API_KEY!, - "Content-Type": "application/json", + await fetch( + `https://api.elevenlabs.io/v1/text-to-speech/${voice.elevenlabsVoiceId}`, + { + method: "POST", + headers: { + "xi-api-key": process.env.ELEVENLABS_API_KEY!, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: "Test", + model_id: "eleven_multilingual_v2", + }), }, - body: JSON.stringify({ - text: "Test", - model_id: "eleven_multilingual_v2", - }), - }); + ); canGenerateTTS = true; } catch { canGenerateTTS = false; @@ -101,6 +112,9 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: } catch (error) { logger.error("[Voice Verify API] Error:", error); - return NextResponse.json({ error: "Failed to verify voice status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to verify voice status" }, + { status: 500 }, + ); } } diff --git a/app/api/eliza-app/auth/connection-success/route.ts b/app/api/eliza-app/auth/connection-success/route.ts index 88fd20308..48c1299e2 100644 --- a/app/api/eliza-app/auth/connection-success/route.ts +++ b/app/api/eliza-app/auth/connection-success/route.ts @@ -79,7 +79,10 @@ function buildHtml(platform: string): string { `; } -function buildElizaAppHtml(provider: string, connectionId: string | null): string { +function buildElizaAppHtml( + provider: string, + connectionId: string | null, +): string { const providerLabel = PROVIDER_LABELS[provider] ?? "Your account"; const payload = JSON.stringify({ type: "eliza-app-oauth-complete", @@ -181,7 +184,8 @@ function buildElizaAppHtml(provider: string, connectionId: string | null): strin export async function GET(request: NextRequest) { const source = request.nextUrl.searchParams.get("source"); if (source === "eliza-app") { - const provider = request.nextUrl.searchParams.get("platform") || "connection"; + const provider = + request.nextUrl.searchParams.get("platform") || "connection"; const connectionId = request.nextUrl.searchParams.get("connection_id"); return new NextResponse(buildElizaAppHtml(provider, connectionId), { diff --git a/app/api/eliza-app/auth/discord/route.ts b/app/api/eliza-app/auth/discord/route.ts index 779a74148..ed5d1055d 100644 --- a/app/api/eliza-app/auth/discord/route.ts +++ b/app/api/eliza-app/auth/discord/route.ts @@ -22,7 +22,10 @@ import { type ValidatedSession, } from "@/lib/services/eliza-app"; import { logger } from "@/lib/utils/logger"; -import { isValidE164, normalizePhoneNumber } from "@/lib/utils/phone-normalization"; +import { + isValidE164, + normalizePhoneNumber, +} from "@/lib/utils/phone-normalization"; /** * Optional E.164 phone number validation (after normalization) @@ -36,7 +39,8 @@ const optionalPhoneSchema = z if (!isValidE164(normalized)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Invalid phone number format. Please use international format (e.g., +1234567890)", + message: + "Invalid phone number format. Please use international format (e.g., +1234567890)", }); return z.NEVER; } @@ -140,7 +144,8 @@ async function handleDiscordAuth( const authHeader = request.headers.get("authorization"); let existingSession: ValidatedSession | null = null; if (authHeader) { - existingSession = await elizaAppSessionService.validateAuthHeader(authHeader); + existingSession = + await elizaAppSessionService.validateAuthHeader(authHeader); if (existingSession) { logger.info("[ElizaApp DiscordAuth] Session-based linking detected", { existingUserId: existingSession.userId, @@ -149,7 +154,10 @@ async function handleDiscordAuth( } // Exchange OAuth2 code for Discord user data - const discordUser = await discordAuthService.verifyOAuthCode(code, redirectUri); + const discordUser = await discordAuthService.verifyOAuthCode( + code, + redirectUri, + ); if (!discordUser) { logger.warn("[ElizaApp DiscordAuth] OAuth2 verification failed"); @@ -164,7 +172,10 @@ async function handleDiscordAuth( } // Build avatar URL - const avatarUrl = discordAuthService.getAvatarUrl(discordUser.id, discordUser.avatar); + const avatarUrl = discordAuthService.getAvatarUrl( + discordUser.id, + discordUser.avatar, + ); let user: User; let organization: Organization; @@ -172,18 +183,23 @@ async function handleDiscordAuth( if (existingSession) { // ---- SESSION-BASED LINKING: Link Discord to existing user ---- - const linkResult = await elizaAppUserService.linkDiscordToUser(existingSession.userId, { - discordId: discordUser.id, - username: discordUser.username, - globalName: discordUser.global_name, - avatarUrl, - }); + const linkResult = await elizaAppUserService.linkDiscordToUser( + existingSession.userId, + { + discordId: discordUser.id, + username: discordUser.username, + globalName: discordUser.global_name, + avatarUrl, + }, + ); if (!linkResult.success) { return NextResponse.json( { success: false, - error: linkResult.error || "This Discord account is already linked to another account", + error: + linkResult.error || + "This Discord account is already linked to another account", code: "DISCORD_ALREADY_LINKED", }, { status: 409 }, @@ -191,10 +207,16 @@ async function handleDiscordAuth( } // Fetch the updated user - const updatedUser = await elizaAppUserService.getById(existingSession.userId); + const updatedUser = await elizaAppUserService.getById( + existingSession.userId, + ); if (!updatedUser || !updatedUser.organization) { return NextResponse.json( - { success: false, error: "User not found after linking", code: "INTERNAL_ERROR" }, + { + success: false, + error: "User not found after linking", + code: "INTERNAL_ERROR", + }, { status: 500 }, ); } @@ -203,10 +225,13 @@ async function handleDiscordAuth( organization = updatedUser.organization; isNew = false; - logger.info("[ElizaApp DiscordAuth] Session-based Discord linking successful", { - userId: user.id, - discordId: discordUser.id, - }); + logger.info( + "[ElizaApp DiscordAuth] Session-based Discord linking successful", + { + userId: user.id, + discordId: discordUser.id, + }, + ); } else { // ---- STANDARD FLOW: Find or create user by Discord ID (with optional phone cross-linking) ---- let result; @@ -227,7 +252,8 @@ async function handleDiscordAuth( return NextResponse.json( { success: false, - error: "This phone number is already linked to a different account", + error: + "This phone number is already linked to a different account", code: "PHONE_ALREADY_LINKED", }, { status: 409 }, @@ -244,10 +270,13 @@ async function handleDiscordAuth( ); } } - logger.error("[ElizaApp DiscordAuth] Unexpected error during user creation", { - error: error instanceof Error ? error.message : String(error), - discordId: discordUser.id, - }); + logger.error( + "[ElizaApp DiscordAuth] Unexpected error during user creation", + { + error: error instanceof Error ? error.message : String(error), + discordId: discordUser.id, + }, + ); return NextResponse.json( { success: false, @@ -265,19 +294,26 @@ async function handleDiscordAuth( // If phone was provided but not linked during findOrCreate (e.g., user already existed without phone), // attempt to link it separately if (phoneNumber && !user.phone_number) { - const linkResult = await elizaAppUserService.linkPhoneToUser(user.id, phoneNumber); + const linkResult = await elizaAppUserService.linkPhoneToUser( + user.id, + phoneNumber, + ); if (!linkResult.success) { return NextResponse.json( { success: false, - error: linkResult.error || "This phone number is already linked to a different account", + error: + linkResult.error || + "This phone number is already linked to a different account", code: "PHONE_ALREADY_LINKED", }, { status: 409 }, ); } // Refetch user to reflect the newly linked phone number in the response - const updatedUser = await elizaAppUserService.getByDiscordId(discordUser.id); + const updatedUser = await elizaAppUserService.getByDiscordId( + discordUser.id, + ); if (updatedUser) { user = updatedUser; } @@ -294,12 +330,16 @@ async function handleDiscordAuth( }); // Create session (new session includes discord identity) - const session = await elizaAppSessionService.createSession(user.id, organization.id, { - discordId: discordUser.id, - ...(user.phone_number && { phoneNumber: user.phone_number }), - ...(user.telegram_id && { telegramId: user.telegram_id }), - ...(user.whatsapp_id && { whatsappId: user.whatsapp_id }), - }); + const session = await elizaAppSessionService.createSession( + user.id, + organization.id, + { + discordId: discordUser.id, + ...(user.phone_number && { phoneNumber: user.phone_number }), + ...(user.telegram_id && { telegramId: user.telegram_id }), + ...(user.whatsapp_id && { whatsappId: user.whatsapp_id }), + }, + ); return NextResponse.json({ success: true, diff --git a/app/api/eliza-app/auth/telegram/route.ts b/app/api/eliza-app/auth/telegram/route.ts index f4cefe963..3f6654f58 100644 --- a/app/api/eliza-app/auth/telegram/route.ts +++ b/app/api/eliza-app/auth/telegram/route.ts @@ -23,7 +23,10 @@ import { type ValidatedSession, } from "@/lib/services/eliza-app"; import { logger } from "@/lib/utils/logger"; -import { isValidE164, normalizePhoneNumber } from "@/lib/utils/phone-normalization"; +import { + isValidE164, + normalizePhoneNumber, +} from "@/lib/utils/phone-normalization"; /** * E.164 phone number validation (after normalization) @@ -36,7 +39,8 @@ const phoneNumberSchema = z if (!isValidE164(normalized)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Invalid phone number format. Please use international format (e.g., +1234567890)", + message: + "Invalid phone number format. Please use international format (e.g., +1234567890)", }); return z.NEVER; } @@ -119,7 +123,11 @@ async function handleTelegramAuth( ); } - const { phone_number: phoneNumber, signup_code: signupCode, ...telegramData } = parseResult.data; + const { + phone_number: phoneNumber, + signup_code: signupCode, + ...telegramData + } = parseResult.data; const authData: TelegramAuthData = telegramData; // Verify Telegram authentication data @@ -144,7 +152,8 @@ async function handleTelegramAuth( const authHeader = request.headers.get("authorization"); let existingSession: ValidatedSession | null = null; if (authHeader) { - existingSession = await elizaAppSessionService.validateAuthHeader(authHeader); + existingSession = + await elizaAppSessionService.validateAuthHeader(authHeader); if (existingSession) { logger.info("[ElizaApp TelegramAuth] Session-based linking detected", { existingUserId: existingSession.userId, @@ -177,7 +186,9 @@ async function handleTelegramAuth( } // Also link phone number if the existing user doesn't have one - const existingUser = await elizaAppUserService.getById(existingSession.userId); + const existingUser = await elizaAppUserService.getById( + existingSession.userId, + ); if (existingUser && !existingUser.phone_number) { const linkPhoneResult = await elizaAppUserService.linkPhoneToUser( existingSession.userId, @@ -185,18 +196,27 @@ async function handleTelegramAuth( ); if (!linkPhoneResult.success) { // Phone conflict is non-fatal for session linking — Telegram is linked, phone just couldn't be added - logger.warn("[ElizaApp TelegramAuth] Phone link failed during session-based linking", { - userId: existingSession.userId, - error: linkPhoneResult.error, - }); + logger.warn( + "[ElizaApp TelegramAuth] Phone link failed during session-based linking", + { + userId: existingSession.userId, + error: linkPhoneResult.error, + }, + ); } } // Fetch the updated user - const updatedUser = await elizaAppUserService.getById(existingSession.userId); + const updatedUser = await elizaAppUserService.getById( + existingSession.userId, + ); if (!updatedUser || !updatedUser.organization) { return NextResponse.json( - { success: false, error: "User not found after linking", code: "INTERNAL_ERROR" }, + { + success: false, + error: "User not found after linking", + code: "INTERNAL_ERROR", + }, { status: 500 }, ); } @@ -205,10 +225,13 @@ async function handleTelegramAuth( organization = updatedUser.organization; isNew = false; - logger.info("[ElizaApp TelegramAuth] Session-based Telegram linking successful", { - userId: user.id, - telegramId: authData.id, - }); + logger.info( + "[ElizaApp TelegramAuth] Session-based Telegram linking successful", + { + userId: user.id, + telegramId: authData.id, + }, + ); } else { // ---- STANDARD FLOW: Find or create user with both Telegram and phone number ---- // Note: Conflict checks are handled in the service layer with database constraints @@ -226,7 +249,8 @@ async function handleTelegramAuth( return NextResponse.json( { success: false, - error: "This phone number is already linked to a different account", + error: + "This phone number is already linked to a different account", code: "PHONE_ALREADY_LINKED", }, { status: 409 }, @@ -236,7 +260,8 @@ async function handleTelegramAuth( return NextResponse.json( { success: false, - error: "Your Telegram account is already linked to a different phone number", + error: + "Your Telegram account is already linked to a different phone number", code: "PHONE_MISMATCH", }, { status: 409 }, @@ -264,10 +289,13 @@ async function handleTelegramAuth( } } // Log unexpected errors and return generic 500 - logger.error("[ElizaApp TelegramAuth] Unexpected error during authentication", { - error: error instanceof Error ? error.message : String(error), - telegramId: authData.id, - }); + logger.error( + "[ElizaApp TelegramAuth] Unexpected error during authentication", + { + error: error instanceof Error ? error.message : String(error), + telegramId: authData.id, + }, + ); return NextResponse.json( { success: false, @@ -294,12 +322,16 @@ async function handleTelegramAuth( }); // Create session (new session includes all known identities) - const session = await elizaAppSessionService.createSession(user.id, organization.id, { - telegramId: String(authData.id), - phoneNumber: user.phone_number || phoneNumber, - ...(user.discord_id && { discordId: user.discord_id }), - ...(user.whatsapp_id && { whatsappId: user.whatsapp_id }), - }); + const session = await elizaAppSessionService.createSession( + user.id, + organization.id, + { + telegramId: String(authData.id), + phoneNumber: user.phone_number || phoneNumber, + ...(user.discord_id && { discordId: user.discord_id }), + ...(user.whatsapp_id && { whatsappId: user.whatsapp_id }), + }, + ); return NextResponse.json({ success: true, @@ -320,7 +352,10 @@ async function handleTelegramAuth( } // Export with rate limiting (60 requests/min per API key) -export const POST = withRateLimit(handleTelegramAuth, RateLimitPresets.STANDARD); +export const POST = withRateLimit( + handleTelegramAuth, + RateLimitPresets.STANDARD, +); // Health check export async function GET(): Promise { diff --git a/app/api/eliza-app/auth/whatsapp/route.ts b/app/api/eliza-app/auth/whatsapp/route.ts index 2f10df7c5..d16d3605f 100644 --- a/app/api/eliza-app/auth/whatsapp/route.ts +++ b/app/api/eliza-app/auth/whatsapp/route.ts @@ -65,7 +65,11 @@ async function handleWhatsAppAuth( const parseResult = whatsappAuthSchema.safeParse(body); if (!parseResult.success) { return NextResponse.json( - { success: false, error: "Invalid request body", code: "INVALID_REQUEST" }, + { + success: false, + error: "Invalid request body", + code: "INVALID_REQUEST", + }, { status: 400 }, ); } @@ -74,7 +78,8 @@ async function handleWhatsAppAuth( const authHeader = request.headers.get("authorization"); let existingSession: ValidatedSession | null = null; if (authHeader) { - existingSession = await elizaAppSessionService.validateAuthHeader(authHeader); + existingSession = + await elizaAppSessionService.validateAuthHeader(authHeader); } if (!existingSession) { @@ -92,15 +97,20 @@ async function handleWhatsAppAuth( existingUserId: existingSession.userId, }); - const linkResult = await elizaAppUserService.linkWhatsAppToUser(existingSession.userId, { - whatsappId, - }); + const linkResult = await elizaAppUserService.linkWhatsAppToUser( + existingSession.userId, + { + whatsappId, + }, + ); if (!linkResult.success) { return NextResponse.json( { success: false, - error: linkResult.error || "This WhatsApp account is already linked to another account", + error: + linkResult.error || + "This WhatsApp account is already linked to another account", code: "WHATSAPP_ALREADY_LINKED", }, { status: 409 }, @@ -110,7 +120,11 @@ async function handleWhatsAppAuth( const updatedUser = await elizaAppUserService.getById(existingSession.userId); if (!updatedUser || !updatedUser.organization) { return NextResponse.json( - { success: false, error: "User not found after linking", code: "INTERNAL_ERROR" }, + { + success: false, + error: "User not found after linking", + code: "INTERNAL_ERROR", + }, { status: 500 }, ); } @@ -126,10 +140,13 @@ async function handleWhatsAppAuth( }, ); - logger.info("[ElizaApp WhatsAppAuth] Session-based WhatsApp linking successful", { - userId: updatedUser.id, - whatsappId, - }); + logger.info( + "[ElizaApp WhatsAppAuth] Session-based WhatsApp linking successful", + { + userId: updatedUser.id, + whatsappId, + }, + ); return NextResponse.json({ success: true, @@ -148,7 +165,10 @@ async function handleWhatsAppAuth( }); } -export const POST = withRateLimit(handleWhatsAppAuth, RateLimitPresets.STANDARD); +export const POST = withRateLimit( + handleWhatsAppAuth, + RateLimitPresets.STANDARD, +); export async function GET(): Promise { return NextResponse.json({ diff --git a/app/api/eliza-app/cli-auth/complete/route.ts b/app/api/eliza-app/cli-auth/complete/route.ts index 461ad9b5d..49a5e0dd9 100644 --- a/app/api/eliza-app/cli-auth/complete/route.ts +++ b/app/api/eliza-app/cli-auth/complete/route.ts @@ -14,17 +14,27 @@ export async function POST(request: NextRequest) { try { const authHeader = request.headers.get("authorization"); if (!authHeader) { - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); } - const userSession = await elizaAppSessionService.validateAuthHeader(authHeader); + const userSession = + await elizaAppSessionService.validateAuthHeader(authHeader); if (!userSession) { - return NextResponse.json({ success: false, error: "Invalid session" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Invalid session" }, + { status: 401 }, + ); } const { session_id } = await request.json(); if (!session_id) { - return NextResponse.json({ success: false, error: "Missing session_id" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Missing session_id" }, + { status: 400 }, + ); } const [cliSession] = await db @@ -33,7 +43,11 @@ export async function POST(request: NextRequest) { .where(eq(cliAuthSessions.session_id, session_id)) .limit(1); - if (!cliSession || cliSession.status !== "pending" || new Date() > cliSession.expires_at) { + if ( + !cliSession || + cliSession.status !== "pending" || + new Date() > cliSession.expires_at + ) { return NextResponse.json( { success: false, error: "Invalid or expired CLI session" }, { status: 400 }, diff --git a/app/api/eliza-app/cli-auth/poll/route.ts b/app/api/eliza-app/cli-auth/poll/route.ts index 46719d588..35e635ad0 100644 --- a/app/api/eliza-app/cli-auth/poll/route.ts +++ b/app/api/eliza-app/cli-auth/poll/route.ts @@ -13,7 +13,10 @@ export async function GET(request: NextRequest) { const sessionId = searchParams.get("session_id"); if (!sessionId) { - return NextResponse.json({ success: false, error: "Missing session_id" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Missing session_id" }, + { status: 400 }, + ); } const [session] = await db @@ -23,7 +26,10 @@ export async function GET(request: NextRequest) { .limit(1); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } if (session.status === "expired" || new Date() > session.expires_at) { @@ -53,6 +59,9 @@ export async function GET(request: NextRequest) { }); } catch (error) { console.error("[CLI Auth Poll] Error:", error); - return NextResponse.json({ success: false, error: "Failed to poll session" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Failed to poll session" }, + { status: 500 }, + ); } } diff --git a/app/api/eliza-app/connections/[platform]/initiate/route.ts b/app/api/eliza-app/connections/[platform]/initiate/route.ts index f26e1c854..7afa9a54b 100644 --- a/app/api/eliza-app/connections/[platform]/initiate/route.ts +++ b/app/api/eliza-app/connections/[platform]/initiate/route.ts @@ -86,7 +86,8 @@ export async function POST( return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to initiate OAuth", + error: + error instanceof Error ? error.message : "Failed to initiate OAuth", code: "INITIATE_FAILED", }, { status: 500 }, diff --git a/app/api/eliza-app/connections/route.ts b/app/api/eliza-app/connections/route.ts index e8b58ec31..bd8e87675 100644 --- a/app/api/eliza-app/connections/route.ts +++ b/app/api/eliza-app/connections/route.ts @@ -3,7 +3,8 @@ import { elizaAppSessionService } from "@/lib/services/eliza-app"; import { getProvider } from "@/lib/services/oauth/provider-registry"; function getRequestedPlatform(request: NextRequest): string | null { - const platform = request.nextUrl.searchParams.get("platform")?.toLowerCase() || "google"; + const platform = + request.nextUrl.searchParams.get("platform")?.toLowerCase() || "google"; return getProvider(platform) ? platform : null; } @@ -40,8 +41,12 @@ export async function GET(request: NextRequest): Promise { platform, }); - const active = connections.find((connection) => connection.status === "active"); - const expired = connections.find((connection) => connection.status === "expired"); + const active = connections.find( + (connection) => connection.status === "active", + ); + const expired = connections.find( + (connection) => connection.status === "expired", + ); const current = active ?? expired ?? null; return NextResponse.json({ @@ -61,7 +66,10 @@ export async function GET(request: NextRequest): Promise { } catch (error) { return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to load connection status", + error: + error instanceof Error + ? error.message + : "Failed to load connection status", code: "CONNECTION_STATUS_FAILED", }, { status: 500 }, diff --git a/app/api/eliza-app/gateway/[agentId]/route.ts b/app/api/eliza-app/gateway/[agentId]/route.ts index 0a6afbe69..4d95c7a07 100644 --- a/app/api/eliza-app/gateway/[agentId]/route.ts +++ b/app/api/eliza-app/gateway/[agentId]/route.ts @@ -1,16 +1,25 @@ import { NextResponse } from "next/server"; // Simple stateful memory for the demo gateway -const agentMemories = new Map>(); +const agentMemories = new Map< + string, + Array<{ role: string; content: string }> +>(); -export async function POST(req: Request, { params }: { params: Promise<{ agentId: string }> }) { +export async function POST( + req: Request, + { params }: { params: Promise<{ agentId: string }> }, +) { try { const { agentId } = await params; const body = await req.json(); const { message } = body; if (!message) { - return NextResponse.json({ success: false, error: "Empty message" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Empty message" }, + { status: 400 }, + ); } // Initialize memory @@ -29,7 +38,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ agentId if (lowerMessage.includes("hello") || lowerMessage.includes("hi")) { reply = `Hello there! I'm your dedicated agent (${agentId}). How can I help you today?`; - } else if (lowerMessage.includes("status") || lowerMessage.includes("mode")) { + } else if ( + lowerMessage.includes("status") || + lowerMessage.includes("mode") + ) { reply = `I am operating normally. If I were a workflow agent, I'd trigger n8n here. If I were autonomous, I'd spawn an isolated container.`; } else { reply = `I received your message: "${message}". I will process it based on my configured capabilities.`; @@ -46,6 +58,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ agentId historyLength: history.length, }); } catch (e: any) { - return NextResponse.json({ success: false, error: e.message }, { status: 500 }); + return NextResponse.json( + { success: false, error: e.message }, + { status: 500 }, + ); } } diff --git a/app/api/eliza-app/provision-agent/route.ts b/app/api/eliza-app/provision-agent/route.ts index eeb47c68f..62632810a 100644 --- a/app/api/eliza-app/provision-agent/route.ts +++ b/app/api/eliza-app/provision-agent/route.ts @@ -37,7 +37,8 @@ export async function POST(req: Request) { } else if (mode === "Workflow") { message = "Allocated cloud-hosted agent + n8n plugin connected."; } else if (mode === "Autonomous") { - message = "Provisioned dedicated Vercel Sandbox for autonomous execution."; + message = + "Provisioned dedicated Vercel Sandbox for autonomous execution."; } return NextResponse.json({ @@ -47,6 +48,9 @@ export async function POST(req: Request) { gatewayUrl: `http://localhost:3000/api/eliza-app/gateway/${agentId}`, }); } catch (e: any) { - return NextResponse.json({ success: false, error: e.message }, { status: 500 }); + return NextResponse.json( + { success: false, error: e.message }, + { status: 500 }, + ); } } diff --git a/app/api/eliza-app/user/me/route.ts b/app/api/eliza-app/user/me/route.ts index 6eea0bf65..b199fb1c7 100644 --- a/app/api/eliza-app/user/me/route.ts +++ b/app/api/eliza-app/user/me/route.ts @@ -10,7 +10,10 @@ import { NextRequest, NextResponse } from "next/server"; import { organizationsRepository } from "@/db/repositories/organizations"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { elizaAppSessionService, elizaAppUserService } from "@/lib/services/eliza-app"; +import { + elizaAppSessionService, + elizaAppUserService, +} from "@/lib/services/eliza-app"; import { logger } from "@/lib/utils/logger"; /** @@ -79,7 +82,10 @@ async function handleGetUser( logger.warn("[ElizaApp UserMe] User not found", { userId: session.userId, }); - return NextResponse.json({ error: "User not found", code: "USER_NOT_FOUND" }, { status: 404 }); + return NextResponse.json( + { error: "User not found", code: "USER_NOT_FOUND" }, + { status: 404 }, + ); } // Get organization details diff --git a/app/api/eliza-app/user/phone/route.ts b/app/api/eliza-app/user/phone/route.ts index ef83a020f..994f19ba5 100644 --- a/app/api/eliza-app/user/phone/route.ts +++ b/app/api/eliza-app/user/phone/route.ts @@ -13,9 +13,15 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { elizaAppSessionService, elizaAppUserService } from "@/lib/services/eliza-app"; +import { + elizaAppSessionService, + elizaAppUserService, +} from "@/lib/services/eliza-app"; import { logger } from "@/lib/utils/logger"; -import { isValidE164, normalizePhoneNumber } from "@/lib/utils/phone-normalization"; +import { + isValidE164, + normalizePhoneNumber, +} from "@/lib/utils/phone-normalization"; /** * E.164 phone number validation (after normalization) @@ -28,7 +34,8 @@ const phoneNumberSchema = z if (!isValidE164(normalized)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Invalid phone number format. Please use international format (e.g., +1234567890)", + message: + "Invalid phone number format. Please use international format (e.g., +1234567890)", }); return z.NEVER; } @@ -67,7 +74,11 @@ async function handleLinkPhone( if (!authHeader) { return NextResponse.json( - { success: false, error: "Authorization header required", code: "UNAUTHORIZED" }, + { + success: false, + error: "Authorization header required", + code: "UNAUTHORIZED", + }, { status: 401 }, ); } @@ -77,7 +88,11 @@ async function handleLinkPhone( if (!session) { return NextResponse.json( - { success: false, error: "Invalid or expired session", code: "INVALID_SESSION" }, + { + success: false, + error: "Invalid or expired session", + code: "INVALID_SESSION", + }, { status: 401 }, ); } @@ -129,7 +144,10 @@ async function handleLinkPhone( } // Link the phone number - const result = await elizaAppUserService.linkPhoneToUser(session.userId, phoneNumber); + const result = await elizaAppUserService.linkPhoneToUser( + session.userId, + phoneNumber, + ); if (!result.success) { logger.warn("[ElizaApp LinkPhone] Phone linking failed", { diff --git a/app/api/eliza-app/webhook/blooio/route.ts b/app/api/eliza-app/webhook/blooio/route.ts index 95da853f7..e7fd5f939 100644 --- a/app/api/eliza-app/webhook/blooio/route.ts +++ b/app/api/eliza-app/webhook/blooio/route.ts @@ -25,7 +25,10 @@ import { runtimeFactory } from "@/lib/eliza/runtime-factory"; import { userContextService } from "@/lib/eliza/user-context"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { roomsService } from "@/lib/services/agents/rooms"; -import { connectionEnforcementService, elizaAppUserService } from "@/lib/services/eliza-app"; +import { + connectionEnforcementService, + elizaAppUserService, +} from "@/lib/services/eliza-app"; import { elizaAppConfig } from "@/lib/services/eliza-app/config"; import { type BlooioSendMessageResponse, @@ -37,10 +40,17 @@ import { verifyBlooioSignature, } from "@/lib/utils/blooio-api"; import { generateElizaAppRoomId } from "@/lib/utils/deterministic-uuid"; -import { isValidEmail, maskEmailForLogging, normalizeEmail } from "@/lib/utils/email-validation"; +import { + isValidEmail, + maskEmailForLogging, + normalizeEmail, +} from "@/lib/utils/email-validation"; import { isAlreadyProcessed, markAsProcessed } from "@/lib/utils/idempotency"; import { logger } from "@/lib/utils/logger"; -import { isValidE164, normalizePhoneNumber } from "@/lib/utils/phone-normalization"; +import { + isValidE164, + normalizePhoneNumber, +} from "@/lib/utils/phone-normalization"; export const dynamic = "force-dynamic"; export const maxDuration = 120; // Extended for ASSISTANT mode multi-step execution @@ -87,17 +97,20 @@ async function sendBlooioMessage( } } -async function handleIncomingMessage(event: BlooioWebhookEvent): Promise { +async function handleIncomingMessage( + event: BlooioWebhookEvent, +): Promise { if (!event.sender) return true; // Not applicable, mark as processed if (event.is_group) return true; // Not applicable, mark as processed const { apiKey, phoneNumber } = getBlooioConfig(); // Mark the chat as read immediately for better UX (sends read receipt) - markChatAsRead(apiKey, event.sender, { fromNumber: phoneNumber }).catch((err) => - logger.warn("[ElizaApp BlooioWebhook] Failed to mark chat as read", { - sender: event.sender, - error: err instanceof Error ? err.message : String(err), - }), + markChatAsRead(apiKey, event.sender, { fromNumber: phoneNumber }).catch( + (err) => + logger.warn("[ElizaApp BlooioWebhook] Failed to mark chat as read", { + sender: event.sender, + error: err instanceof Error ? err.message : String(err), + }), ); const text = event.text?.trim(); @@ -108,7 +121,9 @@ async function handleIncomingMessage(event: BlooioWebhookEvent): Promise>["user"]; + let userWithOrg: Awaited< + ReturnType + >["user"]; let organization: Awaited< ReturnType >["organization"]; @@ -168,10 +183,11 @@ async function handleIncomingMessage(event: BlooioWebhookEvent): Promise { +async function handleBlooioWebhook( + request: NextRequest, +): Promise { const webhookSecret = getBlooioConfig().webhookSecret; const rawBody = await request.text(); const skipVerification = - process.env.SKIP_WEBHOOK_VERIFICATION === "true" && process.env.NODE_ENV !== "production"; + process.env.SKIP_WEBHOOK_VERIFICATION === "true" && + process.env.NODE_ENV !== "production"; // Fail closed: require webhook secret unless explicitly skipped in dev if (!webhookSecret) { if (skipVerification) { - logger.warn("[ElizaApp BlooioWebhook] Signature verification skipped (dev mode)"); + logger.warn( + "[ElizaApp BlooioWebhook] Signature verification skipped (dev mode)", + ); } else { logger.error("[ElizaApp BlooioWebhook] WEBHOOK_SECRET is required"); - return NextResponse.json({ error: "Webhook not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Webhook not configured" }, + { status: 500 }, + ); } } else { const signatureHeader = request.headers.get("X-Blooio-Signature") || ""; - const isValid = await verifyBlooioSignature(webhookSecret, signatureHeader, rawBody); + const isValid = await verifyBlooioSignature( + webhookSecret, + signatureHeader, + rawBody, + ); if (!isValid) { logger.warn("[ElizaApp BlooioWebhook] Invalid signature"); @@ -376,7 +414,10 @@ async function handleBlooioWebhook(request: NextRequest): Promise // Only mark as processed if handler succeeded (prevents lost messages on lock failure) if (processed && payload.message_id) { - await markAsProcessed(`blooio:eliza-app:${payload.message_id}`, "blooio-eliza-app"); + await markAsProcessed( + `blooio:eliza-app:${payload.message_id}`, + "blooio-eliza-app", + ); } // Return 503 on lock failure to trigger webhook retry from Blooio @@ -390,7 +431,10 @@ async function handleBlooioWebhook(request: NextRequest): Promise return NextResponse.json({ success: true }); } -export const POST = withRateLimit(handleBlooioWebhook, RateLimitPresets.AGGRESSIVE); +export const POST = withRateLimit( + handleBlooioWebhook, + RateLimitPresets.AGGRESSIVE, +); export async function GET(): Promise { return NextResponse.json({ diff --git a/app/api/eliza-app/webhook/discord/route.ts b/app/api/eliza-app/webhook/discord/route.ts index 0898eae58..98e2cb0f4 100644 --- a/app/api/eliza-app/webhook/discord/route.ts +++ b/app/api/eliza-app/webhook/discord/route.ts @@ -20,10 +20,16 @@ import { mergeModelPreferences } from "@/lib/eliza/model-preferences"; import { runtimeFactory } from "@/lib/eliza/runtime-factory"; import { userContextService } from "@/lib/eliza/user-context"; import { roomsService } from "@/lib/services/agents/rooms"; -import { connectionEnforcementService, elizaAppUserService } from "@/lib/services/eliza-app"; +import { + connectionEnforcementService, + elizaAppUserService, +} from "@/lib/services/eliza-app"; import { elizaAppConfig } from "@/lib/services/eliza-app/config"; import { generateElizaAppRoomId } from "@/lib/utils/deterministic-uuid"; -import { releaseProcessingClaim, tryClaimForProcessing } from "@/lib/utils/idempotency"; +import { + releaseProcessingClaim, + tryClaimForProcessing, +} from "@/lib/utils/idempotency"; import { logger } from "@/lib/utils/logger"; export const dynamic = "force-dynamic"; @@ -87,7 +93,9 @@ async function sendDiscordMessage( const { botToken } = elizaAppConfig.discord; if (!botToken) { - logger.error("[ElizaApp DiscordWebhook] Cannot send message - bot token not configured"); + logger.error( + "[ElizaApp DiscordWebhook] Cannot send message - bot token not configured", + ); return false; } @@ -111,7 +119,9 @@ async function sendDiscordMessage( }, body: JSON.stringify({ content: truncatedContent, - message_reference: replyToMessageId ? { message_id: replyToMessageId } : undefined, + message_reference: replyToMessageId + ? { message_id: replyToMessageId } + : undefined, }), }); }; @@ -179,7 +189,9 @@ function processDiscordAttachments(data: DiscordMessageData): Media[] { attachments.push({ id: att.id || att.url, url: att.url, - contentType: att.content_type ? getContentTypeFromMimeType(att.content_type) : undefined, + contentType: att.content_type + ? getContentTypeFromMimeType(att.content_type) + : undefined, title: att.filename, }); } @@ -191,7 +203,9 @@ function processDiscordAttachments(data: DiscordMessageData): Media[] { attachments.push({ id: va.url, url: va.url, - contentType: va.content_type ? getContentTypeFromMimeType(va.content_type) : undefined, + contentType: va.content_type + ? getContentTypeFromMimeType(va.content_type) + : undefined, title: va.filename, }); } @@ -235,7 +249,9 @@ function startDiscordTypingIndicator(channelId: string): () => void { return () => clearInterval(interval); } -async function handleDiscordWebhook(request: NextRequest): Promise { +async function handleDiscordWebhook( + request: NextRequest, +): Promise { const payload: DiscordEventPayload = await request.json(); // Only handle MESSAGE_CREATE events @@ -256,7 +272,11 @@ async function handleDiscordWebhook(request: NextRequest): Promise } // Skip empty messages (unless they have attachments) - if (!data.content.trim() && !data.attachments?.length && !data.voice_attachments?.length) { + if ( + !data.content.trim() && + !data.attachments?.length && + !data.voice_attachments?.length + ) { return NextResponse.json({ ok: true, status: "empty_message_skipped" }); } @@ -269,19 +289,28 @@ async function handleDiscordWebhook(request: NextRequest): Promise // Validate Discord user data if (!discordUserId?.trim()) { logger.warn("[ElizaApp DiscordWebhook] Missing Discord user ID"); - return NextResponse.json({ ok: false, error: "Invalid user data" }, { status: 400 }); + return NextResponse.json( + { ok: false, error: "Invalid user data" }, + { status: 400 }, + ); } if (!discordUsername?.trim()) { logger.warn("[ElizaApp DiscordWebhook] Missing Discord username", { discordUserId, }); - return NextResponse.json({ ok: false, error: "Invalid username" }, { status: 400 }); + return NextResponse.json( + { ok: false, error: "Invalid username" }, + { status: 400 }, + ); } // Atomic idempotency claim - prevents duplicate processing from concurrent requests. // Uses INSERT ... ON CONFLICT DO NOTHING so only one caller wins the race. const idempotencyKey = `discord:eliza-app:${payload.event_id}`; - const claimed = await tryClaimForProcessing(idempotencyKey, "discord-eliza-app"); + const claimed = await tryClaimForProcessing( + idempotencyKey, + "discord-eliza-app", + ); if (!claimed) { logger.info("[ElizaApp DiscordWebhook] Duplicate event skipped", { eventId: payload.event_id, @@ -306,23 +335,29 @@ async function handleDiscordWebhook(request: NextRequest): Promise } const { organization } = userWithOrg; - const hasRequiredConnection = await connectionEnforcementService.hasRequiredConnection( - organization.id, - userWithOrg.id, - ); + const hasRequiredConnection = + await connectionEnforcementService.hasRequiredConnection( + organization.id, + userWithOrg.id, + ); if (!hasRequiredConnection) { - const nudgeText = await connectionEnforcementService.generateNudgeResponse({ - userMessage: text, - platform: "discord", - organizationId: organization.id, - userId: userWithOrg.id, - }); + const nudgeText = + await connectionEnforcementService.generateNudgeResponse({ + userMessage: text, + platform: "discord", + organizationId: organization.id, + userId: userWithOrg.id, + }); await sendDiscordMessage(data.channel_id, nudgeText, data.id); return NextResponse.json({ ok: true }); } // Generate room ID (deterministic) - const roomId = generateElizaAppRoomId("discord", DEFAULT_AGENT_ID, discordUserId); + const roomId = generateElizaAppRoomId( + "discord", + DEFAULT_AGENT_ID, + discordUserId, + ); const entityId = userWithOrg.id; // Use userId as entityId for unified memory // Create room with participant atomically (prevents race condition) @@ -354,17 +389,24 @@ async function handleDiscordWebhook(request: NextRequest): Promise await releaseProcessingClaim(idempotencyKey); throw error; } - logger.debug("[ElizaApp DiscordWebhook] Room already exists (concurrent creation)", { - roomId, - }); + logger.debug( + "[ElizaApp DiscordWebhook] Room already exists (concurrent creation)", + { + roomId, + }, + ); } } - const lock = await distributedLocks.acquireRoomLockWithRetry(roomId, ROOM_LOCK_TTL_MS, { - maxRetries: 10, - initialDelayMs: 100, - maxDelayMs: 2000, - }); + const lock = await distributedLocks.acquireRoomLockWithRetry( + roomId, + ROOM_LOCK_TTL_MS, + { + maxRetries: 10, + initialDelayMs: 100, + maxDelayMs: 2000, + }, + ); if (!lock) { logger.error("[ElizaApp DiscordWebhook] Failed to acquire room lock", { @@ -411,7 +453,9 @@ async function handleDiscordWebhook(request: NextRequest): Promise const responseContent = result.message.content; const responseText = - typeof responseContent === "string" ? responseContent : responseContent?.text || ""; + typeof responseContent === "string" + ? responseContent + : responseContent?.text || ""; if (responseText) { await sendDiscordMessage(data.channel_id, responseText, data.id); @@ -430,7 +474,10 @@ async function handleDiscordWebhook(request: NextRequest): Promise roomId, }); await releaseProcessingClaim(idempotencyKey); - return NextResponse.json({ ok: false, error: "Agent processing failed" }, { status: 500 }); + return NextResponse.json( + { ok: false, error: "Agent processing failed" }, + { status: 500 }, + ); } finally { stopTyping(); await lock.release(); diff --git a/app/api/eliza-app/webhook/telegram/route.ts b/app/api/eliza-app/webhook/telegram/route.ts index 12437b627..43d87103e 100644 --- a/app/api/eliza-app/webhook/telegram/route.ts +++ b/app/api/eliza-app/webhook/telegram/route.ts @@ -47,28 +47,37 @@ function getBlooioPhoneNumber() { return elizaAppConfig.blooio.phoneNumber; } -async function callSendMessage(payload: Record): Promise { - return fetch(`https://api.telegram.org/bot${getTelegramConfig().botToken}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); +async function callSendMessage( + payload: Record, +): Promise { + return fetch( + `https://api.telegram.org/bot${getTelegramConfig().botToken}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }, + ); } function cleanUrlMarkdown(text: string): string { return text.replace( /(^|[\s(])([*_]{1,2})(https?:\/\/[^\s]+?)\2(?=$|[\s),.!?:;])/g, - (_match, prefix: string, _delimiter: string, url: string) => `${prefix}${url}`, + (_match, prefix: string, _delimiter: string, url: string) => + `${prefix}${url}`, ); } async function sendTypingIndicator(chatId: number): Promise { try { - await fetch(`https://api.telegram.org/bot${getTelegramConfig().botToken}/sendChatAction`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, action: "typing" }), - }); + await fetch( + `https://api.telegram.org/bot${getTelegramConfig().botToken}/sendChatAction`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ chat_id: chatId, action: "typing" }), + }, + ); } catch (error) { logger.debug("[ElizaApp TelegramWebhook] Typing indicator failed", { chatId, @@ -77,16 +86,21 @@ async function sendTypingIndicator(chatId: number): Promise { } } -async function sendWithMarkdownFallback(payload: Record): Promise { +async function sendWithMarkdownFallback( + payload: Record, +): Promise { try { const response = await callSendMessage(payload); if (response.ok) return true; const firstError = await response.text(); if (firstError.includes("can't parse entities")) { - logger.warn("[ElizaApp TelegramWebhook] Markdown parse failed, retrying as plain text", { - chatId: payload.chat_id, - }); + logger.warn( + "[ElizaApp TelegramWebhook] Markdown parse failed, retrying as plain text", + { + chatId: payload.chat_id, + }, + ); const { parse_mode: _, ...plain } = payload; const retryResponse = await callSendMessage(plain); if (retryResponse.ok) return true; @@ -134,7 +148,9 @@ async function sendTelegramMessage( chat_id: chatId, text: cleanUrlMarkdown(chunk), reply_to_message_id: i === 0 ? replyToMessageId : undefined, - ...(!hasLongUrl && !hasUrlWithUnderscores ? { parse_mode: "Markdown" } : {}), + ...(!hasLongUrl && !hasUrlWithUnderscores + ? { parse_mode: "Markdown" } + : {}), }); if (!ok) return false; } @@ -162,7 +178,8 @@ async function handleMessage(message: Message): Promise { try { perfTrace.mark("user-lookup"); - const userWithOrg = await elizaAppUserService.getByTelegramId(telegramUserId); + const userWithOrg = + await elizaAppUserService.getByTelegramId(telegramUserId); if (!userWithOrg?.organization) { await sendTelegramMessage( message.chat.id, @@ -172,24 +189,30 @@ async function handleMessage(message: Message): Promise { } const { organization } = userWithOrg; - const hasRequiredConnection = await connectionEnforcementService.hasRequiredConnection( - organization.id, - userWithOrg.id, - ); + const hasRequiredConnection = + await connectionEnforcementService.hasRequiredConnection( + organization.id, + userWithOrg.id, + ); if (!hasRequiredConnection) { - const nudgeText = await connectionEnforcementService.generateNudgeResponse({ - userMessage: text, - platform: "telegram", - organizationId: organization.id, - userId: userWithOrg.id, - }); + const nudgeText = + await connectionEnforcementService.generateNudgeResponse({ + userMessage: text, + platform: "telegram", + organizationId: organization.id, + userId: userWithOrg.id, + }); await sendTelegramMessage(message.chat.id, nudgeText, message.message_id); return; } const defaultAgentId = getDefaultAgentId(); - const roomId = generateElizaAppRoomId("telegram", defaultAgentId, telegramUserId); + const roomId = generateElizaAppRoomId( + "telegram", + defaultAgentId, + telegramUserId, + ); const entityId = userWithOrg.id; perfTrace.mark("room-setup"); @@ -215,23 +238,34 @@ async function handleMessage(message: Message): Promise { await roomsService.addParticipant(roomId, entityId, defaultAgentId); } catch (error) { const msg = error instanceof Error ? error.message : String(error); - if (!msg.includes("already") && !msg.includes("duplicate") && !msg.includes("exists")) { + if ( + !msg.includes("already") && + !msg.includes("duplicate") && + !msg.includes("exists") + ) { throw error; } } perfTrace.mark("acquire-lock"); - const lock = await distributedLocks.acquireRoomLockWithRetry(roomId, 120000, { - maxRetries: 10, - initialDelayMs: 100, - maxDelayMs: 2000, - }); + const lock = await distributedLocks.acquireRoomLockWithRetry( + roomId, + 120000, + { + maxRetries: 10, + initialDelayMs: 100, + maxDelayMs: 2000, + }, + ); if (!lock) { - logger.warn("[ElizaApp TelegramWebhook] Room locked - message already being processed", { - roomId, - lockServiceEnabled: distributedLocks.isEnabled(), - }); + logger.warn( + "[ElizaApp TelegramWebhook] Room locked - message already being processed", + { + roomId, + lockServiceEnabled: distributedLocks.isEnabled(), + }, + ); return; } @@ -263,7 +297,8 @@ async function handleMessage(message: Message): Promise { elizaAppConfig.modelPreferences, ); - const { name, description, ...promptConfig } = elizaAppConfig.promptPreset; + const { name, description, ...promptConfig } = + elizaAppConfig.promptPreset; userContext.appPromptConfig = promptConfig; logger.info("[ElizaApp TelegramWebhook] Processing message", { @@ -289,7 +324,8 @@ async function handleMessage(message: Message): Promise { const originalSystemPrompt = runtime.character?.system; if (runtime.character) { - runtime.character.system = (runtime.character.system || "") + telegramChannelContext; + runtime.character.system = + (runtime.character.system || "") + telegramChannelContext; } else { logger.warn( "[ElizaApp TelegramWebhook] runtime.character is null - channel context not injected", @@ -309,20 +345,29 @@ async function handleMessage(message: Message): Promise { const responseContent = result.message.content; const responseText = - typeof responseContent === "string" ? responseContent : responseContent?.text || ""; + typeof responseContent === "string" + ? responseContent + : responseContent?.text || ""; perfTrace.mark("send-response"); if (!responseText) { - logger.warn("[ElizaApp TelegramWebhook] Agent returned empty response", { - roomId, - }); + logger.warn( + "[ElizaApp TelegramWebhook] Agent returned empty response", + { + roomId, + }, + ); await sendTelegramMessage( message.chat.id, "I processed your message but didn't have a response. Could you try rephrasing?", message.message_id, ); } else { - await sendTelegramMessage(message.chat.id, responseText, message.message_id); + await sendTelegramMessage( + message.chat.id, + responseText, + message.message_id, + ); } } catch (error) { logger.error("[ElizaApp TelegramWebhook] Agent failed", { @@ -336,10 +381,16 @@ async function handleMessage(message: Message): Promise { message.message_id, ); } catch (sendError) { - logger.error("[ElizaApp TelegramWebhook] Failed to send error message to user", { - chatId: message.chat.id, - error: sendError instanceof Error ? sendError.message : String(sendError), - }); + logger.error( + "[ElizaApp TelegramWebhook] Failed to send error message to user", + { + chatId: message.chat.id, + error: + sendError instanceof Error + ? sendError.message + : String(sendError), + }, + ); } } finally { if (runtime.character) { @@ -369,7 +420,9 @@ async function handleMessage(message: Message): Promise { } } -async function handleCommand(message: Message & { text: string }): Promise { +async function handleCommand( + message: Message & { text: string }, +): Promise { const command = message.text.trim().split(" ")[0].toLowerCase(); const chatId = message.chat.id; @@ -396,10 +449,11 @@ async function handleCommand(message: Message & { text: string }): Promise if (user?.organization) { const creditBalance = user.organization.credit_balance || "0.00"; - const hasRequiredConnection = await connectionEnforcementService.hasRequiredConnection( - user.organization.id, - user.id, - ); + const hasRequiredConnection = + await connectionEnforcementService.hasRequiredConnection( + user.organization.id, + user.id, + ); const connectionStatus = hasRequiredConnection ? "✅ Data integration connected" : "⚠️ No data integration yet. Connect Google, Microsoft, or X."; @@ -431,18 +485,26 @@ async function handleCommand(message: Message & { text: string }): Promise } } -async function handleTelegramWebhook(request: NextRequest): Promise { +async function handleTelegramWebhook( + request: NextRequest, +): Promise { const webhookSecret = getTelegramConfig().webhookSecret; // Fail closed: require webhook secret unless explicitly skipped in dev const skipVerification = - process.env.SKIP_WEBHOOK_VERIFICATION === "true" && process.env.NODE_ENV !== "production"; + process.env.SKIP_WEBHOOK_VERIFICATION === "true" && + process.env.NODE_ENV !== "production"; if (!webhookSecret) { if (skipVerification) { - logger.warn("[ElizaApp TelegramWebhook] Signature verification skipped (dev mode)"); + logger.warn( + "[ElizaApp TelegramWebhook] Signature verification skipped (dev mode)", + ); } else { logger.error("[ElizaApp TelegramWebhook] WEBHOOK_SECRET is required"); - return NextResponse.json({ error: "Webhook not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Webhook not configured" }, + { status: 500 }, + ); } } else { const secretToken = request.headers.get("x-telegram-bot-api-secret-token"); @@ -471,11 +533,17 @@ async function handleTelegramWebhook(request: NextRequest): Promise { return NextResponse.json({ diff --git a/app/api/eliza-app/webhook/whatsapp/route.ts b/app/api/eliza-app/webhook/whatsapp/route.ts index 300df1ff1..c51d1c1a0 100644 --- a/app/api/eliza-app/webhook/whatsapp/route.ts +++ b/app/api/eliza-app/webhook/whatsapp/route.ts @@ -27,7 +27,10 @@ import { elizaAppConfig } from "@/lib/services/eliza-app/config"; import { whatsAppAuthService } from "@/lib/services/eliza-app/whatsapp-auth"; import type { UserWithOrganization } from "@/lib/types"; import { generateElizaAppRoomId } from "@/lib/utils/deterministic-uuid"; -import { releaseProcessingClaim, tryClaimForProcessing } from "@/lib/utils/idempotency"; +import { + releaseProcessingClaim, + tryClaimForProcessing, +} from "@/lib/utils/idempotency"; import { logger } from "@/lib/utils/logger"; import { createPerfTrace } from "@/lib/utils/perf-trace"; import { @@ -53,11 +56,19 @@ function getWhatsAppConfig() { return elizaAppConfig.whatsapp; } -async function sendWhatsAppResponse(to: string, text: string): Promise { +async function sendWhatsAppResponse( + to: string, + text: string, +): Promise { const { accessToken, phoneNumberId } = getWhatsAppConfig(); try { - const response = await sendWhatsAppMessage(accessToken, phoneNumberId, to, text); + const response = await sendWhatsAppMessage( + accessToken, + phoneNumberId, + to, + text, + ); logger.info("[ElizaApp WhatsAppWebhook] Message sent", { to, @@ -74,33 +85,47 @@ async function sendWhatsAppResponse(to: string, text: string): Promise } } -async function handleIncomingMessage(msg: WhatsAppIncomingMessage): Promise { +async function handleIncomingMessage( + msg: WhatsAppIncomingMessage, +): Promise { const text = msg.text?.trim(); if (!text) return true; const { accessToken, phoneNumberId } = getWhatsAppConfig(); if (!isValidWhatsAppId(msg.from)) { - logger.warn("[ElizaApp WhatsAppWebhook] Invalid WhatsApp ID format, skipping", { - from: msg.from, - messageId: msg.messageId, - }); + logger.warn( + "[ElizaApp WhatsAppWebhook] Invalid WhatsApp ID format, skipping", + { + from: msg.from, + messageId: msg.messageId, + }, + ); return true; } const markRead = async (retries = 2) => { for (let attempt = 0; attempt <= retries; attempt++) { try { - await markWhatsAppMessageAsRead(accessToken, phoneNumberId, msg.messageId); + await markWhatsAppMessageAsRead( + accessToken, + phoneNumberId, + msg.messageId, + ); return; } catch (err) { if (attempt < retries) { - await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1))); + await new Promise((resolve) => + setTimeout(resolve, 500 * (attempt + 1)), + ); } else { - logger.warn("[ElizaApp WhatsAppWebhook] Failed to mark as read after retries", { - messageId: msg.messageId, - error: err instanceof Error ? err.message : String(err), - }); + logger.warn( + "[ElizaApp WhatsAppWebhook] Failed to mark as read after retries", + { + messageId: msg.messageId, + error: err instanceof Error ? err.message : String(err), + }, + ); } } } @@ -108,7 +133,11 @@ async function handleIncomingMessage(msg: WhatsAppIncomingMessage): Promise { +async function handleWhatsAppWebhookPost( + request: NextRequest, +): Promise { const rawBody = await request.text(); const skipVerification = - process.env.SKIP_WEBHOOK_VERIFICATION === "true" && process.env.NODE_ENV !== "production"; + process.env.SKIP_WEBHOOK_VERIFICATION === "true" && + process.env.NODE_ENV !== "production"; const signatureHeader = request.headers.get("x-hub-signature-256") || ""; if (!skipVerification) { - const isValid = whatsAppAuthService.verifyWebhookSignature(signatureHeader, rawBody); + const isValid = whatsAppAuthService.verifyWebhookSignature( + signatureHeader, + rawBody, + ); if (!isValid) { logger.warn("[ElizaApp WhatsAppWebhook] Invalid signature"); return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); } } else { - logger.warn("[ElizaApp WhatsAppWebhook] Signature verification skipped (dev mode)"); + logger.warn( + "[ElizaApp WhatsAppWebhook] Signature verification skipped (dev mode)", + ); } let payload; @@ -294,7 +349,10 @@ async function handleWhatsAppWebhookPost(request: NextRequest): Promise { +async function handleWhatsAppWebhookGet( + request: NextRequest, +): Promise { const searchParams = request.nextUrl.searchParams; const mode = searchParams.get("hub.mode"); const verifyToken = searchParams.get("hub.verify_token"); const challenge = searchParams.get("hub.challenge"); - const result = whatsAppAuthService.verifyWebhookSubscription(mode, verifyToken, challenge); + const result = whatsAppAuthService.verifyWebhookSubscription( + mode, + verifyToken, + challenge, + ); if (result) { return new NextResponse(result, { @@ -347,5 +411,11 @@ async function handleWhatsAppWebhookGet(request: NextRequest): Promise }) { +export async function POST( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { try { // Support both authenticated and anonymous users let user: UserWithOrganization; @@ -45,7 +52,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId let anonData = await getAnonymousUser(); if (!anonData) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } user = anonData.user; @@ -65,7 +75,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId if (!roomId) { logger.error("[Eliza Messages API] Missing roomId"); - return NextResponse.json({ error: "roomId is required" }, { status: 400 }); + return NextResponse.json( + { error: "roomId is required" }, + { status: 400 }, + ); } if (!text || typeof text !== "string" || text.trim().length === 0) { @@ -78,9 +91,12 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // Check if user is blocked due to moderation violations if (await contentModerationService.shouldBlockUser(user.id)) { - logger.warn("[Eliza Messages API] User blocked due to moderation violations", { - userId: user.id, - }); + logger.warn( + "[Eliza Messages API] User blocked due to moderation violations", + { + userId: user.id, + }, + ); return NextResponse.json( { error: @@ -91,18 +107,28 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId } // Start async content moderation (runs in background, doesn't block) - contentModerationService.moderateInBackground(text, user.id, roomId, (result) => { - logger.warn("[Eliza Messages API] Async moderation detected violation", { - userId: user.id, - roomId, - categories: result.flaggedCategories, - action: result.action, - }); - }); + contentModerationService.moderateInBackground( + text, + user.id, + roomId, + (result) => { + logger.warn( + "[Eliza Messages API] Async moderation detected violation", + { + userId: user.id, + roomId, + categories: result.flaggedCategories, + action: result.action, + }, + ); + }, + ); // Handle anonymous user rate limiting if (isAnonymous && anonymousSession) { - const limitCheck = await checkAnonymousLimit(anonymousSession.session_token); + const limitCheck = await checkAnonymousLimit( + anonymousSession.session_token, + ); if (!limitCheck.allowed) { const errorMessage = @@ -140,10 +166,16 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId let reservation: CreditReservation | null = null; if (!isAnonymous) { if (!user.organization_id || !user.organization) { - logger.error("[Eliza Messages API] User has no organization - cannot proceed", { - userId: user.id, - }); - return NextResponse.json({ error: "User has no organization" }, { status: 400 }); + logger.error( + "[Eliza Messages API] User has no organization - cannot proceed", + { + userId: user.id, + }, + ); + return NextResponse.json( + { error: "User has no organization" }, + { status: 400 }, + ); } try { @@ -193,7 +225,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId isAnonymous: false, } : (() => { - const ctx = userContextService.createSystemContext(AgentMode.CHAT); + const ctx = userContextService.createSystemContext( + AgentMode.CHAT, + ); if (characterId) ctx.characterId = characterId; return ctx; })(); @@ -263,7 +297,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // It will automatically generate a title after 4+ messages // Send to Discord thread if configured - const discordThreadId = room?.metadata?.discordThreadId as string | undefined; + const discordThreadId = room?.metadata?.discordThreadId as + | string + | undefined; if (discordThreadId) { try { const userMessage = `**${user.name || user.email || user.id}:** ${text}`; @@ -289,7 +325,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId threadId: discordThreadId, }); } catch (error) { - logger.error("[Eliza Messages API] Failed to send to Discord thread:", error); + logger.error( + "[Eliza Messages API] Failed to send to Discord thread:", + error, + ); } } @@ -301,7 +340,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId logger.error("[Eliza Messages API] Error:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to process message", + error: + error instanceof Error ? error.message : "Failed to process message", }, { status: 500 }, ); diff --git a/app/api/eliza/rooms/[roomId]/messages/stream/route.ts b/app/api/eliza/rooms/[roomId]/messages/stream/route.ts index 59b7f8df7..bcb62cef0 100644 --- a/app/api/eliza/rooms/[roomId]/messages/stream/route.ts +++ b/app/api/eliza/rooms/[roomId]/messages/stream/route.ts @@ -10,10 +10,19 @@ import { getOrCreateAnonymousUser, } from "@/lib/auth-anonymous"; import type { AgentModeConfig } from "@/lib/eliza/agent-mode-types"; -import { AgentMode, isValidAgentModeConfig } from "@/lib/eliza/agent-mode-types"; +import { + AgentMode, + isValidAgentModeConfig, +} from "@/lib/eliza/agent-mode-types"; import { createMessageHandler } from "@/lib/eliza/message-handler"; -import { mergeModelPreferences, sanitizeModelPreferences } from "@/lib/eliza/model-preferences"; -import { DEFAULT_AGENT_ID_STRING, runtimeFactory } from "@/lib/eliza/runtime-factory"; +import { + mergeModelPreferences, + sanitizeModelPreferences, +} from "@/lib/eliza/model-preferences"; +import { + DEFAULT_AGENT_ID_STRING, + runtimeFactory, +} from "@/lib/eliza/runtime-factory"; import { clientCharacterStateSchema, validateAppId, @@ -49,7 +58,8 @@ type RunWithRequestContext = ( ) => Promise; const runWithRequestContext: RunWithRequestContext = - "runWithRequestContext" in elizaCore && typeof elizaCore.runWithRequestContext === "function" + "runWithRequestContext" in elizaCore && + typeof elizaCore.runWithRequestContext === "function" ? (elizaCore.runWithRequestContext as RunWithRequestContext) : async (_context, operation) => await operation(); @@ -64,7 +74,10 @@ const runWithRequestContext: RunWithRequestContext = * * Security: entityId is derived from authenticated user, not client-supplied */ -export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId: string }> }) { +export async function POST( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { const encoder = new TextEncoder(); try { @@ -103,10 +116,13 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId const appId = appIdResult.appId; if (!roomId || !text?.trim()) { - return new Response(JSON.stringify({ error: "Missing required fields" }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ error: "Missing required fields" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } // Validate appPromptConfig if provided @@ -131,10 +147,13 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // Validate explicit agentMode if provided if (agentMode && !isValidAgentModeConfig(agentMode)) { - return new Response(JSON.stringify({ error: "Invalid agent mode configuration" }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ error: "Invalid agent mode configuration" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } // Determine agent mode: explicit > features toggle > default @@ -150,12 +169,16 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // Step 2: Authentication & Context Building perfTrace.mark("auth"); - const userContext = await authenticateAndBuildContext(request, agentModeConfig.mode, { - sessionToken, - appId, - appPromptConfig, - webSearchEnabled, - }); + const userContext = await authenticateAndBuildContext( + request, + agentModeConfig.mode, + { + sessionToken, + appId, + appPromptConfig, + webSearchEnabled, + }, + ); // Set webSearchEnabled on context (defaults to true) userContext.webSearchEnabled = effectiveWebSearchEnabled; @@ -192,20 +215,21 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId } // Run all independent checks in parallel - const [isBlocked, room, anonymousLimitResult, monetizationSettingsResult] = await Promise.all([ - // Check if user is blocked due to moderation violations - contentModerationService.shouldBlockUser(userContext.userId), - // Get room data (needed for character assignment) - roomsRepository.findById(roomId), - // Check anonymous rate limit (conditional but safe to always check) - userContext.isAnonymous && userContext.sessionToken - ? checkAnonymousLimit(userContext.sessionToken) - : Promise.resolve(null), - // Check app monetization settings (conditional but safe to always check) - userContext.appId - ? appCreditsService.getMonetizationSettings(userContext.appId) - : Promise.resolve(null), - ]); + const [isBlocked, room, anonymousLimitResult, monetizationSettingsResult] = + await Promise.all([ + // Check if user is blocked due to moderation violations + contentModerationService.shouldBlockUser(userContext.userId), + // Get room data (needed for character assignment) + roomsRepository.findById(roomId), + // Check anonymous rate limit (conditional but safe to always check) + userContext.isAnonymous && userContext.sessionToken + ? checkAnonymousLimit(userContext.sessionToken) + : Promise.resolve(null), + // Check app monetization settings (conditional but safe to always check) + userContext.appId + ? appCreditsService.getMonetizationSettings(userContext.appId) + : Promise.resolve(null), + ]); // Process parallel results: user block check if (isBlocked) { @@ -297,7 +321,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // users should no longer be able to send messages const isOwner = character.user_id === userContext.userId; const isPublic = character.is_public === true; - const claimCheck = await charactersService.isClaimableAffiliateCharacter(characterId); + const claimCheck = + await charactersService.isClaimableAffiliateCharacter(characterId); const isClaimableAffiliate = claimCheck.claimable; if (!isPublic && !isOwner && !isClaimableAffiliate) { @@ -309,7 +334,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId }); return new Response( JSON.stringify({ - error: "This agent is private. Only the owner can chat with it.", + error: + "This agent is private. Only the owner can chat with it.", accessDenied: true, }), { status: 403, headers: { "Content-Type": "application/json" } }, @@ -317,13 +343,17 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId } // Check legacy location: character_data.affiliate - const characterData = character.character_data as Record | undefined; + const characterData = character.character_data as + | Record + | undefined; const legacyAffiliateData = characterData?.affiliate as | Record | undefined; // Check new location: settings.affiliateData (used by miniapp) - const settings = character.settings as Record | undefined; + const settings = character.settings as + | Record + | undefined; const settingsAffiliateData = settings?.affiliateData as | Record | undefined; @@ -346,15 +376,23 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId } } } catch (error) { - logger.error("[Stream] Failed to check character access/affiliate status:", error); + logger.error( + "[Stream] Failed to check character access/affiliate status:", + error, + ); } } // For BUILD mode, use the targetCharacterId from agent mode metadata // This ensures we're editing the correct character, not the default - if (agentModeConfig.mode === AgentMode.BUILD && agentModeConfig.metadata?.targetCharacterId) { + if ( + agentModeConfig.mode === AgentMode.BUILD && + agentModeConfig.metadata?.targetCharacterId + ) { characterId = String(agentModeConfig.metadata.targetCharacterId); - logger.info(`[Stream] BUILD mode - Using character from metadata: ${characterId}`); + logger.info( + `[Stream] BUILD mode - Using character from metadata: ${characterId}`, + ); // Update room agentId for build mode (proper column, not metadata) if (characterId && room && room.agentId !== characterId) { @@ -364,18 +402,24 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId `[Stream] BUILD mode - Updated room agentId: room ${roomId} → agent ${characterId}`, ); } catch (error) { - logger.error("[Stream] BUILD mode - Failed to update room agentId:", error); + logger.error( + "[Stream] BUILD mode - Failed to update room agentId:", + error, + ); } } } logger.info( `[Stream] Room ${roomId} - Character lookup:`, - characterId ? `Using character ${characterId}` : "Using default character", + characterId + ? `Using character ${characterId}` + : "Using default character", ); // Step 5: Apply model preferences if provided - const requestModelPreferences = sanitizeModelPreferences(bodyModelPreferences); + const requestModelPreferences = + sanitizeModelPreferences(bodyModelPreferences); if (model || requestModelPreferences) { userContext.modelPreferences = mergeModelPreferences( @@ -459,7 +503,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId agentIdForSettings, userContext.organizationId, ), - runWithRequestContext(requestContext, () => runtimeFactory.createRuntimeForUser(userContext)), + runWithRequestContext(requestContext, () => + runtimeFactory.createRuntimeForUser(userContext), + ), ]); // Inject prefetched entity settings into the shared Map @@ -510,7 +556,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId ); } - const clientCharacterState = JSON.parse(JSON.stringify(validatedState.data)); + const clientCharacterState = JSON.parse( + JSON.stringify(validatedState.data), + ); runtime.character.settings = { ...runtime.character.settings, @@ -655,7 +703,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // Extract content - the full Content object is now stored in memory const messageContent = result.message.content; const responseText = - typeof messageContent === "string" ? messageContent : messageContent?.text || ""; + typeof messageContent === "string" + ? messageContent + : messageContent?.text || ""; // Build response content, preserving all Content fields const responseContentPayload: Record = { @@ -697,7 +747,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId // Check if we should send low credit warning if (result.usage && !userContext.isAnonymous) { // This is just for the warning event, actual credit deduction happened in MessageHandler - const remainingCredits = await checkUserCredits(userContext.organizationId); + const remainingCredits = await checkUserCredits( + userContext.organizationId, + ); if (remainingCredits < 1.0) { await sendEvent("warning", { message: "Low credits - please top up to continue", @@ -772,14 +824,17 @@ async function authenticateAndBuildContext( webSearchEnabled?: boolean; }, ) { - const anonymousSessionToken = request.headers.get("X-Anonymous-Session") || body?.sessionToken; + const anonymousSessionToken = + request.headers.get("X-Anonymous-Session") || body?.sessionToken; // Try Privy/API key auth first (ensures authenticated users aren't treated as anonymous) try { const authResult = await requireAuthOrApiKey(request); if (authResult.user.is_anonymous) { - logger.warn("[Stream] User authenticated but marked anonymous - possible migration issue"); + logger.warn( + "[Stream] User authenticated but marked anonymous - possible migration issue", + ); } return await userContextService.buildContext({ @@ -795,7 +850,9 @@ async function authenticateAndBuildContext( // Try provided session token if (anonymousSessionToken) { - const session = await anonymousSessionsService.getByToken(anonymousSessionToken); + const session = await anonymousSessionsService.getByToken( + anonymousSessionToken, + ); if (session && !session.converted_at && session.is_active) { const user = await usersService.getById(session.user_id); diff --git a/app/api/eliza/rooms/[roomId]/route.ts b/app/api/eliza/rooms/[roomId]/route.ts index 1a704b9bc..dc9115293 100644 --- a/app/api/eliza/rooms/[roomId]/route.ts +++ b/app/api/eliza/rooms/[roomId]/route.ts @@ -16,7 +16,10 @@ import { logger } from "@/lib/utils/logger"; * Uses agentsService to get agent display info * Requires the authenticated user to be a participant of the room */ -export async function GET(request: NextRequest, ctx: { params: Promise<{ roomId: string }> }) { +export async function GET( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { // Get authenticated user ID let userId: string; @@ -27,7 +30,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ roomId: // Fallback to anonymous user const anonData = await getAnonymousUser(); if (!anonData) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } userId = anonData.user.id; } @@ -54,7 +60,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ roomId: // Use rooms service to get room with messages (pure DB query) // Service handles filtering (hidden/action_result) and deduplication - const roomData = await roomsService.getRoomWithMessages(roomId, limit ? parseInt(limit) : 50); + const roomData = await roomsService.getRoomWithMessages( + roomId, + limit ? parseInt(limit) : 50, + ); // If room doesn't exist in Eliza tables, check if it's a conversation // that hasn't had any messages yet (room is created on first message) @@ -121,7 +130,9 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ roomId: }; }); - logger.info(`[Eliza Room API] ✅ Returning ${messages.length} messages for room ${roomId}`); + logger.info( + `[Eliza Room API] ✅ Returning ${messages.length} messages for room ${roomId}`, + ); // Get agent display info from database (no runtime needed!) // PERFORMANCE: Try character ID first, fallback to room's agentId @@ -158,7 +169,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ roomId: * Pure database operation - no runtime needed * Requires the authenticated user to be a participant of the room */ -export async function PATCH(request: NextRequest, ctx: { params: Promise<{ roomId: string }> }) { +export async function PATCH( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { // Get authenticated user ID let userId: string; @@ -169,7 +183,10 @@ export async function PATCH(request: NextRequest, ctx: { params: Promise<{ roomI // Fallback to anonymous user const anonData = await getAnonymousUser(); if (!anonData) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } userId = anonData.user.id; } @@ -198,11 +215,17 @@ export async function PATCH(request: NextRequest, ctx: { params: Promise<{ roomI }; if (!body.metadata && !body.name) { - return NextResponse.json({ error: "metadata or name is required" }, { status: 400 }); + return NextResponse.json( + { error: "metadata or name is required" }, + { status: 400 }, + ); } if (body.metadata && typeof body.metadata !== "object") { - return NextResponse.json({ error: "metadata must be an object" }, { status: 400 }); + return NextResponse.json( + { error: "metadata must be an object" }, + { status: 400 }, + ); } if (body.metadata) { @@ -213,7 +236,10 @@ export async function PATCH(request: NextRequest, ctx: { params: Promise<{ roomI await roomsRepository.update(roomId, { name: body.name }); } - const updatedFields = [body.metadata && "metadata", body.name && "name"].filter(Boolean); + const updatedFields = [ + body.metadata && "metadata", + body.name && "name", + ].filter(Boolean); logger.info("[Eliza Room API] ✓ Room updated successfully:", roomId); @@ -230,7 +256,10 @@ export async function PATCH(request: NextRequest, ctx: { params: Promise<{ roomI * Pure database operation - no runtime needed * Requires the authenticated user to be a participant of the room */ -export async function DELETE(request: NextRequest, ctx: { params: Promise<{ roomId: string }> }) { +export async function DELETE( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { // Get authenticated user ID let userId: string; @@ -241,7 +270,10 @@ export async function DELETE(request: NextRequest, ctx: { params: Promise<{ room // Fallback to anonymous user const anonData = await getAnonymousUser(); if (!anonData) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } userId = anonData.user.id; } diff --git a/app/api/eliza/rooms/[roomId]/welcome/route.ts b/app/api/eliza/rooms/[roomId]/welcome/route.ts index a1f113590..28da6f2fd 100644 --- a/app/api/eliza/rooms/[roomId]/welcome/route.ts +++ b/app/api/eliza/rooms/[roomId]/welcome/route.ts @@ -17,7 +17,10 @@ const DEFAULT_AGENT_ID = "b850bc30-45f8-0041-a00a-83df46d8555d"; * * Body: { text: string } */ -export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId: string }> }) { +export async function POST( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { const { roomId } = await ctx.params; const body = await request.json(); const { text } = body; @@ -35,7 +38,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId } catch { const anonData = await getAnonymousUser(); if (!anonData) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } userId = anonData.user.id; } @@ -68,7 +74,9 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId }, }); - logger.info(`[Welcome API] Stored welcome message: ${messageId} in room ${roomId}`); + logger.info( + `[Welcome API] Stored welcome message: ${messageId} in room ${roomId}`, + ); return NextResponse.json({ success: true, @@ -81,7 +89,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ roomId * * Clears all messages from a room (used to reset edit mode rooms). */ -export async function DELETE(request: NextRequest, ctx: { params: Promise<{ roomId: string }> }) { +export async function DELETE( + request: NextRequest, + ctx: { params: Promise<{ roomId: string }> }, +) { const { roomId } = await ctx.params; // Support both authenticated and anonymous users @@ -93,7 +104,10 @@ export async function DELETE(request: NextRequest, ctx: { params: Promise<{ room } catch { const anonData = await getAnonymousUser(); if (!anonData) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } userId = anonData.user.id; } diff --git a/app/api/eliza/rooms/route.ts b/app/api/eliza/rooms/route.ts index a696eb507..75dcb57c6 100644 --- a/app/api/eliza/rooms/route.ts +++ b/app/api/eliza/rooms/route.ts @@ -1,7 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { requireAuthOrApiKey } from "@/lib/auth"; -import { getAnonymousUser, getOrCreateAnonymousUser } from "@/lib/auth-anonymous"; +import { + getAnonymousUser, + getOrCreateAnonymousUser, +} from "@/lib/auth-anonymous"; import { agentsService } from "@/lib/services/agents/agents"; import { roomsService } from "@/lib/services/agents/rooms"; import { anonymousSessionsService } from "@/lib/services/anonymous-sessions"; @@ -105,12 +108,16 @@ export async function POST(request: NextRequest) { "[Eliza Rooms API POST] Checking provided session token:", providedSessionToken.slice(0, 8) + "...", ); - const session = await anonymousSessionsService.getByToken(providedSessionToken); + const session = + await anonymousSessionsService.getByToken(providedSessionToken); if (session) { const sessionUser = await usersService.getById(session.user_id); if (sessionUser && sessionUser.is_anonymous) { userId = sessionUser.id; - logger.info("[Eliza Rooms API POST] Anonymous auth via provided token:", userId); + logger.info( + "[Eliza Rooms API POST] Anonymous auth via provided token:", + userId, + ); } } } @@ -121,26 +128,40 @@ export async function POST(request: NextRequest) { if (anonData) { userId = anonData.user.id; - logger.info("[Eliza Rooms API POST] Anonymous auth via cookie:", userId); + logger.info( + "[Eliza Rooms API POST] Anonymous auth via cookie:", + userId, + ); } else { // No cookie found - create a new anonymous session - logger.info("[Eliza Rooms API POST] No session cookie - creating new anonymous session"); + logger.info( + "[Eliza Rooms API POST] No session cookie - creating new anonymous session", + ); try { const newAnonData = await getOrCreateAnonymousUser(); userId = newAnonData.user.id; - logger.info("[Eliza Rooms API POST] Created new anonymous session:", userId); + logger.info( + "[Eliza Rooms API POST] Created new anonymous session:", + userId, + ); } catch (error) { logger.warn("[Eliza Rooms API POST] Anonymous fallback unavailable", { error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } } } } if (!userId) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } logger.info( @@ -152,8 +173,14 @@ export async function POST(request: NextRequest) { // Validate characterId if provided if (characterId && typeof characterId !== "string") { - logger.error("[Eliza Rooms API POST] Invalid characterId type:", typeof characterId); - return NextResponse.json({ error: "characterId must be a string" }, { status: 400 }); + logger.error( + "[Eliza Rooms API POST] Invalid characterId type:", + typeof characterId, + ); + return NextResponse.json( + { error: "characterId must be a string" }, + { status: 400 }, + ); } // ACCESS CONTROL: Check if user has permission to chat with this character @@ -163,7 +190,10 @@ export async function POST(request: NextRequest) { if (!character) { logger.warn("[Eliza Rooms API POST] Character not found:", characterId); - return NextResponse.json({ error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { error: "Character not found" }, + { status: 404 }, + ); } // Check access permissions @@ -171,27 +201,35 @@ export async function POST(request: NextRequest) { const isPublic = character.is_public === true; // Check if this is a claimable affiliate character - const claimCheck = await charactersService.isClaimableAffiliateCharacter(characterId); + const claimCheck = + await charactersService.isClaimableAffiliateCharacter(characterId); const isClaimableAffiliate = claimCheck.claimable; if (!isPublic && !isOwner && !isClaimableAffiliate) { - logger.warn("[Eliza Rooms API POST] Access denied to private character:", { - characterId, - userId, - characterOwnerId: character.user_id, - isPublic: character.is_public, - }); + logger.warn( + "[Eliza Rooms API POST] Access denied to private character:", + { + characterId, + userId, + characterOwnerId: character.user_id, + isPublic: character.is_public, + }, + ); return NextResponse.json( { error: "Access denied - this character is private" }, { status: 403 }, ); } - logger.info("[Eliza Rooms API POST] Access granted to character:", characterId, { - isPublic, - isOwner, - isClaimableAffiliate, - }); + logger.info( + "[Eliza Rooms API POST] Access granted to character:", + characterId, + { + isPublic, + isOwner, + isClaimableAffiliate, + }, + ); } // Determine the agent ID - use provided characterId or default diff --git a/app/api/internal/auth/refresh/route.ts b/app/api/internal/auth/refresh/route.ts index cb3ec485f..ba96b51c1 100644 --- a/app/api/internal/auth/refresh/route.ts +++ b/app/api/internal/auth/refresh/route.ts @@ -36,7 +36,10 @@ export async function POST(request: NextRequest) { const token = extractBearerToken(authHeader); if (!token) { - return NextResponse.json({ error: "Missing or invalid Authorization header" }, { status: 401 }); + return NextResponse.json( + { error: "Missing or invalid Authorization header" }, + { status: 401 }, + ); } // Verify the current token @@ -48,7 +51,10 @@ export async function POST(request: NextRequest) { logger.warn("[Token Refresh] Invalid token", { error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ error: "Invalid or expired token" }, { status: 401 }); + return NextResponse.json( + { error: "Invalid or expired token" }, + { status: 401 }, + ); } // Issue a new token with the same subject and service diff --git a/app/api/internal/auth/token/route.ts b/app/api/internal/auth/token/route.ts index 22c1700d3..6e40a418f 100644 --- a/app/api/internal/auth/token/route.ts +++ b/app/api/internal/auth/token/route.ts @@ -14,7 +14,10 @@ import { timingSafeEqual } from "crypto"; import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { isJWKSConfigured } from "@/lib/auth/jwks"; -import { signInternalToken, TOKEN_LIFETIME_SECONDS } from "@/lib/auth/jwt-internal"; +import { + signInternalToken, + TOKEN_LIFETIME_SECONDS, +} from "@/lib/auth/jwt-internal"; import { logger } from "@/lib/utils/logger"; export const dynamic = "force-dynamic"; @@ -23,7 +26,9 @@ const GATEWAY_BOOTSTRAP_SECRET = process.env.GATEWAY_BOOTSTRAP_SECRET; // Log configuration issues once at startup if (!GATEWAY_BOOTSTRAP_SECRET && process.env.NODE_ENV !== "test") { - console.error("[CRITICAL] GATEWAY_BOOTSTRAP_SECRET not configured - token issuance will fail"); + console.error( + "[CRITICAL] GATEWAY_BOOTSTRAP_SECRET not configured - token issuance will fail", + ); } /** @@ -84,7 +89,10 @@ export async function POST(request: NextRequest) { const parsed = TokenRequestSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( - { error: "Invalid request", details: parsed.error.issues.map((e) => e.message).join(", ") }, + { + error: "Invalid request", + details: parsed.error.issues.map((e) => e.message).join(", "), + }, { status: 400 }, ); } diff --git a/app/api/internal/discord/events/route.ts b/app/api/internal/discord/events/route.ts index a3c125b28..382c8da10 100644 --- a/app/api/internal/discord/events/route.ts +++ b/app/api/internal/discord/events/route.ts @@ -22,12 +22,17 @@ export const dynamic = "force-dynamic"; */ function validatePayload( body: unknown, -): { success: true; data: DiscordEventPayload } | { success: false; error: string } { +): + | { success: true; data: DiscordEventPayload } + | { success: false; error: string } { const parsed = DiscordEventPayloadSchema.safeParse(body); if (parsed.success) { return { success: true, data: parsed.data }; } - return { success: false, error: parsed.error.issues.map((e) => e.message).join(", ") }; + return { + success: false, + error: parsed.error.issues.map((e) => e.message).join(", "), + }; } export const POST = withInternalAuth(async (request: NextRequest) => { @@ -40,7 +45,9 @@ export const POST = withInternalAuth(async (request: NextRequest) => { const validation = validatePayload(body); if (!validation.success) { - logger.warn("[Discord Events] Invalid payload", { error: validation.error }); + logger.warn("[Discord Events] Invalid payload", { + error: validation.error, + }); return NextResponse.json( { error: "Invalid payload", details: validation.error }, { status: 400 }, diff --git a/app/api/internal/discord/gateway/assignments/route.ts b/app/api/internal/discord/gateway/assignments/route.ts index 16a6b2269..2f939c941 100644 --- a/app/api/internal/discord/gateway/assignments/route.ts +++ b/app/api/internal/discord/gateway/assignments/route.ts @@ -21,8 +21,14 @@ export const GET = withInternalAuth(async (request: NextRequest) => { // Current and max connection counts to prevent over-claiming // Validate to prevent NaN causing silent failures (NaN < NaN is always false) - const currentCountRaw = parseInt(request.nextUrl.searchParams.get("current") ?? "0", 10); - const maxCountRaw = parseInt(request.nextUrl.searchParams.get("max") ?? "100", 10); + const currentCountRaw = parseInt( + request.nextUrl.searchParams.get("current") ?? "0", + 10, + ); + const maxCountRaw = parseInt( + request.nextUrl.searchParams.get("max") ?? "100", + 10, + ); // Use safe defaults if values are invalid const currentCount = Number.isNaN(currentCountRaw) ? 0 : currentCountRaw; diff --git a/app/api/internal/discord/gateway/failover/route.ts b/app/api/internal/discord/gateway/failover/route.ts index d4cf6370b..ad9b2247c 100644 --- a/app/api/internal/discord/gateway/failover/route.ts +++ b/app/api/internal/discord/gateway/failover/route.ts @@ -39,7 +39,10 @@ export const POST = withInternalAuth(async (request: NextRequest) => { logger.warn("[Gateway Failover] Rejected self-claim attempt", { pod: claiming_pod, }); - return NextResponse.json({ error: "Cannot claim connections from self" }, { status: 400 }); + return NextResponse.json( + { error: "Cannot claim connections from self" }, + { status: 400 }, + ); } // Quick check - reject if pod has any recent heartbeat (optimization) @@ -49,11 +52,17 @@ export const POST = withInternalAuth(async (request: NextRequest) => { ); if (recentHeartbeat) { - logger.warn("[Gateway Failover] Rejected claim - pod has recent heartbeat", { - claimingPod: claiming_pod, - deadPod: dead_pod, - }); - return NextResponse.json({ error: "Pod is not dead - has recent heartbeat" }, { status: 400 }); + logger.warn( + "[Gateway Failover] Rejected claim - pod has recent heartbeat", + { + claimingPod: claiming_pod, + deadPod: dead_pod, + }, + ); + return NextResponse.json( + { error: "Pod is not dead - has recent heartbeat" }, + { status: 400 }, + ); } logger.warn("[Gateway Failover] Processing failover request", { diff --git a/app/api/internal/discord/gateway/heartbeat/route.ts b/app/api/internal/discord/gateway/heartbeat/route.ts index 20b559e8a..0713cff7c 100644 --- a/app/api/internal/discord/gateway/heartbeat/route.ts +++ b/app/api/internal/discord/gateway/heartbeat/route.ts @@ -82,6 +82,9 @@ export const POST = withInternalAuth(async (request: NextRequest) => { podName: pod_name, error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ error: "Failed to update heartbeats" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to update heartbeats" }, + { status: 500 }, + ); } }); diff --git a/app/api/internal/discord/gateway/shutdown/route.ts b/app/api/internal/discord/gateway/shutdown/route.ts index 336712959..a33781611 100644 --- a/app/api/internal/discord/gateway/shutdown/route.ts +++ b/app/api/internal/discord/gateway/shutdown/route.ts @@ -48,7 +48,8 @@ export const POST = withInternalAuth(async (request: NextRequest) => { podName: pod_name, }); - const released = await discordConnectionsRepository.clearPodAssignments(pod_name); + const released = + await discordConnectionsRepository.clearPodAssignments(pod_name); logger.info("[Gateway Shutdown] Released connections", { podName: pod_name, diff --git a/app/api/internal/discord/gateway/status/route.ts b/app/api/internal/discord/gateway/status/route.ts index ffc9797fb..db41b7f68 100644 --- a/app/api/internal/discord/gateway/status/route.ts +++ b/app/api/internal/discord/gateway/status/route.ts @@ -31,7 +31,8 @@ export const POST = withInternalAuth(async (request: NextRequest) => { ); } - const { connection_id, pod_name, status, error_message, bot_user_id } = parsed.data; + const { connection_id, pod_name, status, error_message, bot_user_id } = + parsed.data; logger.info("[Gateway Status] Updating connection status", { connectionId: connection_id, @@ -50,7 +51,10 @@ export const POST = withInternalAuth(async (request: NextRequest) => { ); if (!updated) { - return NextResponse.json({ error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { error: "Connection not found" }, + { status: 404 }, + ); } return NextResponse.json({ success: true }); diff --git a/app/api/internal/identity/resolve/route.ts b/app/api/internal/identity/resolve/route.ts index aafc0a26d..7360e768e 100644 --- a/app/api/internal/identity/resolve/route.ts +++ b/app/api/internal/identity/resolve/route.ts @@ -1,5 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import { type UserWithOrganization, usersRepository } from "@/db/repositories/users"; +import { + type UserWithOrganization, + usersRepository, +} from "@/db/repositories/users"; import { withInternalAuth } from "@/lib/auth/internal-api"; import { CacheKeys, CacheTTL } from "@/lib/cache/keys"; import { withCache } from "@/lib/cache/service-cache"; @@ -9,7 +12,13 @@ import { logger } from "@/lib/utils/logger"; export const dynamic = "force-dynamic"; -const SUPPORTED_PLATFORMS = ["discord", "telegram", "twilio", "blooio", "whatsapp"] as const; +const SUPPORTED_PLATFORMS = [ + "discord", + "telegram", + "twilio", + "blooio", + "whatsapp", +] as const; type Platform = (typeof SUPPORTED_PLATFORMS)[number]; function lookupUser(platform: Platform, platformId: string) { @@ -23,11 +32,17 @@ function lookupUser(platform: Platform, platformId: string) { return usersRepository.findByPhoneNumberWithOrganization(platformId); case "whatsapp": // WhatsApp ID is digits only (e.g., "14245074963"), derive E.164 for phone lookup - return usersRepository.findByPhoneNumberWithOrganization(`+${platformId.replace(/\D/g, "")}`); + return usersRepository.findByPhoneNumberWithOrganization( + `+${platformId.replace(/\D/g, "")}`, + ); } } -async function autoCreateUser(platform: Platform, platformId: string, platformName?: string) { +async function autoCreateUser( + platform: Platform, + platformId: string, + platformName?: string, +) { switch (platform) { case "discord": return elizaAppUserService.findOrCreateByDiscordId(platformId, { @@ -51,7 +66,9 @@ async function autoCreateUser(platform: Platform, platformId: string, platformNa return elizaAppUserService.findOrCreateByPhone(platformId); case "whatsapp": // WhatsApp ID is digits only — derive E.164 phone number - return elizaAppUserService.findOrCreateByPhone(`+${platformId.replace(/\D/g, "")}`); + return elizaAppUserService.findOrCreateByPhone( + `+${platformId.replace(/\D/g, "")}`, + ); } } @@ -96,41 +113,52 @@ export const GET = withInternalAuth(async (request: NextRequest) => { const typedPlatform = platform as Platform; const cacheKey = CacheKeys.identity.resolve(typedPlatform, platformId); - const result = await withCache(cacheKey, CacheTTL.identity.resolve, async () => { - let user = await lookupUser(typedPlatform, platformId); + const result = await withCache( + cacheKey, + CacheTTL.identity.resolve, + async () => { + let user = await lookupUser(typedPlatform, platformId); - if (!user) { - logger.info("[Identity] User not found, auto-creating", { - platform, - platformId, - }); - try { - const created = await autoCreateUser(typedPlatform, platformId, platformName); - // findOrCreate returns { user: User, organization }, build UserWithOrganization shape - user = { - ...created.user, - organization: created.organization, - } as UserWithOrganization; - } catch (err) { - logger.error("[Identity] Auto-create failed", { + if (!user) { + logger.info("[Identity] User not found, auto-creating", { platform, platformId, - error: err instanceof Error ? err.message : String(err), }); - return null; + try { + const created = await autoCreateUser( + typedPlatform, + platformId, + platformName, + ); + // findOrCreate returns { user: User, organization }, build UserWithOrganization shape + user = { + ...created.user, + organization: created.organization, + } as UserWithOrganization; + } catch (err) { + logger.error("[Identity] Auto-create failed", { + platform, + platformId, + error: err instanceof Error ? err.message : String(err), + }); + return null; + } } - } - return { - userId: user.id, - organizationId: user.organization_id, - agentId: elizaAppConfig.defaultAgentId, - platformData: extractPlatformData(typedPlatform, user), - }; - }); + return { + userId: user.id, + organizationId: user.organization_id, + agentId: elizaAppConfig.defaultAgentId, + platformData: extractPlatformData(typedPlatform, user), + }; + }, + ); if (!result) { - return NextResponse.json({ error: "Failed to resolve identity" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to resolve identity" }, + { status: 500 }, + ); } return NextResponse.json(result); diff --git a/app/api/internal/webhook/config/route.ts b/app/api/internal/webhook/config/route.ts index 876f217a1..6f911cdaa 100644 --- a/app/api/internal/webhook/config/route.ts +++ b/app/api/internal/webhook/config/route.ts @@ -7,7 +7,12 @@ import { logger } from "@/lib/utils/logger"; export const dynamic = "force-dynamic"; -const SUPPORTED_PLATFORMS = ["telegram", "blooio", "twilio", "whatsapp"] as const; +const SUPPORTED_PLATFORMS = [ + "telegram", + "blooio", + "twilio", + "whatsapp", +] as const; type Platform = (typeof SUPPORTED_PLATFORMS)[number]; const CACHE_TTL_SECONDS = 300; @@ -47,13 +52,14 @@ async function fetchAgentTwilioConfig(orgId: string, agentId: string) { } async function fetchAgentWhatsAppConfig(orgId: string, agentId: string) { - const [accessToken, phoneNumberId, appSecret, verifyToken, businessPhone] = await Promise.all([ - secretsService.get(orgId, "WHATSAPP_ACCESS_TOKEN", agentId), - secretsService.get(orgId, "WHATSAPP_PHONE_NUMBER_ID", agentId), - secretsService.get(orgId, "WHATSAPP_APP_SECRET", agentId), - secretsService.get(orgId, "WHATSAPP_VERIFY_TOKEN", agentId), - secretsService.get(orgId, "WHATSAPP_PHONE_NUMBER", agentId), - ]); + const [accessToken, phoneNumberId, appSecret, verifyToken, businessPhone] = + await Promise.all([ + secretsService.get(orgId, "WHATSAPP_ACCESS_TOKEN", agentId), + secretsService.get(orgId, "WHATSAPP_PHONE_NUMBER_ID", agentId), + secretsService.get(orgId, "WHATSAPP_APP_SECRET", agentId), + secretsService.get(orgId, "WHATSAPP_VERIFY_TOKEN", agentId), + secretsService.get(orgId, "WHATSAPP_PHONE_NUMBER", agentId), + ]); if (!accessToken || !phoneNumberId) return null; return { agentId, @@ -93,11 +99,17 @@ export const GET = withInternalAuth(async (request: NextRequest) => { const platform = searchParams.get("platform"); if (!agentId || !platform) { - return NextResponse.json({ error: "agentId and platform required" }, { status: 400 }); + return NextResponse.json( + { error: "agentId and platform required" }, + { status: 400 }, + ); } if (!SUPPORTED_PLATFORMS.includes(platform as Platform)) { - return NextResponse.json({ error: `Unsupported platform: ${platform}` }, { status: 400 }); + return NextResponse.json( + { error: `Unsupported platform: ${platform}` }, + { status: 400 }, + ); } const cacheKey = `webhook-config:${platform}:agent:${agentId}`; @@ -107,7 +119,10 @@ export const GET = withInternalAuth(async (request: NextRequest) => { ); if (!config) { - return NextResponse.json({ error: "Platform not configured" }, { status: 404 }); + return NextResponse.json( + { error: "Platform not configured" }, + { status: 404 }, + ); } return NextResponse.json(config); diff --git a/app/api/invites/accept/route.ts b/app/api/invites/accept/route.ts index ed8ad1c89..a37602473 100644 --- a/app/api/invites/accept/route.ts +++ b/app/api/invites/accept/route.ts @@ -24,7 +24,10 @@ async function handlePOST(request: NextRequest) { const body = await request.json(); const validated = acceptInviteSchema.parse(body); - const acceptedInvite = await invitesService.acceptInvite(validated.token, user.id); + const acceptedInvite = await invitesService.acceptInvite( + validated.token, + user.id, + ); revalidateTag("user-auth", {}); @@ -51,7 +54,8 @@ async function handlePOST(request: NextRequest) { ); } - const errorMessage = error instanceof Error ? error.message : "Failed to accept invitation"; + const errorMessage = + error instanceof Error ? error.message : "Failed to accept invitation"; return NextResponse.json( { @@ -60,9 +64,11 @@ async function handlePOST(request: NextRequest) { }, { status: - errorMessage.includes("sign in with") || errorMessage.includes("already a member") + errorMessage.includes("sign in with") || + errorMessage.includes("already a member") ? 409 - : errorMessage.includes("Invalid invite") || errorMessage.includes("expired") + : errorMessage.includes("Invalid invite") || + errorMessage.includes("expired") ? 400 : 500, }, diff --git a/app/api/invites/validate/route.ts b/app/api/invites/validate/route.ts index 25aa27d4d..331aeceac 100644 --- a/app/api/invites/validate/route.ts +++ b/app/api/invites/validate/route.ts @@ -55,7 +55,10 @@ async function handleGET(request: NextRequest) { { success: false, valid: false, - error: error instanceof Error ? error.message : "Failed to validate invitation", + error: + error instanceof Error + ? error.message + : "Failed to validate invitation", }, { status: 500 }, ); diff --git a/app/api/invoices/[id]/route.ts b/app/api/invoices/[id]/route.ts index 9f084ff8f..40c5a9e09 100644 --- a/app/api/invoices/[id]/route.ts +++ b/app/api/invoices/[id]/route.ts @@ -14,19 +14,28 @@ import { logger } from "@/lib/utils/logger"; * @param context - Route context containing the invoice ID parameter. * @returns Formatted invoice details or error if not found/unauthorized. */ -async function handleGetInvoice(req: NextRequest, context: { params: Promise<{ id: string }> }) { +async function handleGetInvoice( + req: NextRequest, + context: { params: Promise<{ id: string }> }, +) { const { params } = context; try { const { user } = await requireAuthOrApiKeyWithOrg(req); if (!user.organization_id) { - return NextResponse.json({ error: "No organization found" }, { status: 404 }); + return NextResponse.json( + { error: "No organization found" }, + { status: 404 }, + ); } const { id } = await params; if (!id) { - return NextResponse.json({ error: "Invoice ID is required" }, { status: 400 }); + return NextResponse.json( + { error: "Invoice ID is required" }, + { status: 400 }, + ); } const invoice = await invoicesService.getById(id); @@ -36,7 +45,10 @@ async function handleGetInvoice(req: NextRequest, context: { params: Promise<{ i } if (invoice.organization_id !== user.organization_id) { - return NextResponse.json({ error: "Unauthorized access to invoice" }, { status: 403 }); + return NextResponse.json( + { error: "Unauthorized access to invoice" }, + { status: 403 }, + ); } const formattedInvoice = { @@ -52,7 +64,9 @@ async function handleGetInvoice(req: NextRequest, context: { params: Promise<{ i invoiceNumber: invoice.invoice_number, invoicePdf: invoice.invoice_pdf, hostedInvoiceUrl: invoice.hosted_invoice_url, - creditsAdded: invoice.credits_added ? Number(invoice.credits_added) : undefined, + creditsAdded: invoice.credits_added + ? Number(invoice.credits_added) + : undefined, metadata: invoice.metadata, createdAt: invoice.created_at.toISOString(), updatedAt: invoice.updated_at.toISOString(), @@ -68,7 +82,10 @@ async function handleGetInvoice(req: NextRequest, context: { params: Promise<{ i return NextResponse.json({ error: error.message }, { status: 500 }); } - return NextResponse.json({ error: "Failed to fetch invoice" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch invoice" }, + { status: 500 }, + ); } } diff --git a/app/api/invoices/list/route.ts b/app/api/invoices/list/route.ts index 12e7fec87..1d9387146 100644 --- a/app/api/invoices/list/route.ts +++ b/app/api/invoices/list/route.ts @@ -17,10 +17,15 @@ async function handleListInvoices(req: NextRequest) { const { user } = await requireAuthOrApiKeyWithOrg(req); if (!user.organization_id) { - return NextResponse.json({ error: "No organization found" }, { status: 404 }); + return NextResponse.json( + { error: "No organization found" }, + { status: 404 }, + ); } - const invoices = await invoicesService.listByOrganization(user.organization_id); + const invoices = await invoicesService.listByOrganization( + user.organization_id, + ); const formattedInvoices = invoices.map((invoice) => ({ id: invoice.id, @@ -38,7 +43,9 @@ async function handleListInvoices(req: NextRequest) { invoiceUrl: invoice.hosted_invoice_url || "", invoicePdf: invoice.invoice_pdf || "", type: invoice.invoice_type, - creditsAdded: invoice.credits_added ? Number(invoice.credits_added) : undefined, + creditsAdded: invoice.credits_added + ? Number(invoice.credits_added) + : undefined, })); return NextResponse.json({ @@ -52,7 +59,10 @@ async function handleListInvoices(req: NextRequest) { return NextResponse.json({ error: error.message }, { status: 500 }); } - return NextResponse.json({ error: "Failed to list invoices" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to list invoices" }, + { status: 500 }, + ); } } diff --git a/app/api/mcp/info/route.ts b/app/api/mcp/info/route.ts index d5e9261b0..dc8b77b3d 100644 --- a/app/api/mcp/info/route.ts +++ b/app/api/mcp/info/route.ts @@ -143,7 +143,8 @@ export async function GET() { authentication: { type: "Bearer", header: "Authorization", - description: "Requires API key in Authorization header: Bearer YOUR_API_KEY", + description: + "Requires API key in Authorization header: Bearer YOUR_API_KEY", }, status: "live", }); diff --git a/app/api/mcp/lib/handler.ts b/app/api/mcp/lib/handler.ts index 6cfd09ad3..f82879236 100644 --- a/app/api/mcp/lib/handler.ts +++ b/app/api/mcp/lib/handler.ts @@ -1,4 +1,5 @@ -let mcpHandlerPromise: Promise<(req: Request) => Promise> | null = null; +let mcpHandlerPromise: Promise<(req: Request) => Promise> | null = + null; export async function getMcpHandler() { if (!mcpHandlerPromise) { diff --git a/app/api/mcp/list/route.ts b/app/api/mcp/list/route.ts index b95869e92..f8fabcc2f 100644 --- a/app/api/mcp/list/route.ts +++ b/app/api/mcp/list/route.ts @@ -16,7 +16,8 @@ const mcpDefinitions = [ tools: [ { name: "check_credits", - description: "Check credit balance and recent transactions for your organization", + description: + "Check credit balance and recent transactions for your organization", parameters: { includeTransactions: { type: "boolean", @@ -36,7 +37,8 @@ const mcpDefinitions = [ }, { name: "get_recent_usage", - description: "Get recent API usage statistics including models used, costs, and tokens", + description: + "Get recent API usage statistics including models used, costs, and tokens", parameters: { limit: { type: "number", @@ -185,7 +187,8 @@ const mcpDefinitions = [ }, { name: "list_agents", - description: "List all available agents, characters, and deployed elizaOS instances.", + description: + "List all available agents, characters, and deployed elizaOS instances.", parameters: { filters: { type: "object", @@ -326,12 +329,14 @@ const mcpDefinitions = [ }, { name: "get_market_data", - description: "Get comprehensive market data including price, volume, supply, ATH/ATL", + description: + "Get comprehensive market data including price, volume, supply, ATH/ATL", cost: "Free", }, { name: "list_trending", - description: "Get list of trending cryptocurrencies by search popularity", + description: + "Get list of trending cryptocurrencies by search popularity", cost: "Free", }, ], diff --git a/app/api/mcp/proxy/[mcpId]/route.ts b/app/api/mcp/proxy/[mcpId]/route.ts index bf9da40c1..3fa511c28 100644 --- a/app/api/mcp/proxy/[mcpId]/route.ts +++ b/app/api/mcp/proxy/[mcpId]/route.ts @@ -38,7 +38,10 @@ export const maxDuration = 60; * GET /api/mcp/proxy/[mcpId] * Get MCP info and endpoint details */ -export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function GET( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const { mcpId } = await ctx.params; const mcp = await userMcpsService.getById(mcpId); @@ -48,10 +51,14 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: } if (mcp.status !== "live") { - return NextResponse.json({ error: "MCP is not available" }, { status: 404 }); + return NextResponse.json( + { error: "MCP is not available" }, + { status: 404 }, + ); } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; return NextResponse.json({ id: mcp.id, @@ -73,16 +80,24 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: * POST /api/mcp/proxy/[mcpId] * Proxy request to user MCP */ -export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function POST( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const startTime = Date.now(); const { mcpId } = await ctx.params; // Authenticate - x402 is NOT supported for direct payment here // Users must top up credits first via /api/v1/credits/topup - const authResult = await requireAuthOrApiKeyWithOrg(request).catch(() => null); + const authResult = await requireAuthOrApiKeyWithOrg(request).catch( + () => null, + ); if (!authResult) { - return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); } // Look up MCP @@ -93,7 +108,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: } if (mcp.status !== "live") { - return NextResponse.json({ error: "MCP is not available" }, { status: 404 }); + return NextResponse.json( + { error: "MCP is not available" }, + { status: 404 }, + ); } // Payment type is always credits for this proxy endpoint @@ -113,7 +131,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: if (referrer) { affiliateOwnerId = referrer.user_id; affiliateCodeId = referrer.id; - affiliateFeeCredits = creditsRequired * (Number(referrer.markup_percent) / 100); + affiliateFeeCredits = + creditsRequired * (Number(referrer.markup_percent) / 100); platformFeeCredits = creditsRequired * 0.2; } } catch (error) { @@ -124,7 +143,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: }); } - const totalCreditsRequired = creditsRequired + affiliateFeeCredits + platformFeeCredits; + const totalCreditsRequired = + creditsRequired + affiliateFeeCredits + platformFeeCredits; const preChargeResult = await creditsService.reserveAndDeductCredits({ organizationId: authResult.user.organization_id, @@ -159,23 +179,37 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: if (mcp.endpoint_type === "external" && mcp.external_endpoint) { try { - targetUrl = (await assertSafeOutboundUrl(mcp.external_endpoint)).toString(); + targetUrl = ( + await assertSafeOutboundUrl(mcp.external_endpoint) + ).toString(); } catch (error) { logger.warn("[MCP Proxy] Blocked unsafe external endpoint", { mcpId, error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ error: "Unsafe external MCP endpoint" }, { status: 400 }); + return NextResponse.json( + { error: "Unsafe external MCP endpoint" }, + { status: 400 }, + ); } } else if (mcp.endpoint_type === "container" && mcp.container_id) { // Get container URL - use MCP creator's organization ID - const container = await containersService.getById(mcp.container_id, mcp.organization_id); + const container = await containersService.getById( + mcp.container_id, + mcp.organization_id, + ); if (!container || !container.load_balancer_url) { - return NextResponse.json({ error: "MCP container not available" }, { status: 503 }); + return NextResponse.json( + { error: "MCP container not available" }, + { status: 503 }, + ); } targetUrl = `${container.load_balancer_url}${mcp.endpoint_path || "/mcp"}`; } else { - return NextResponse.json({ error: "MCP endpoint not configured" }, { status: 500 }); + return NextResponse.json( + { error: "MCP endpoint not configured" }, + { status: 500 }, + ); } // Parse the request body to extract tool name @@ -186,7 +220,11 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: if (contentType?.includes("application/json")) { body = await request.json(); // MCP protocol: look for tool name in the request - if (body.method === "tools/call" && body.params && typeof body.params === "object") { + if ( + body.method === "tools/call" && + body.params && + typeof body.params === "object" + ) { const params = body.params as { name?: string }; toolName = params.name || "unknown"; } @@ -219,7 +257,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: targetUrl, error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ error: "Failed to reach MCP endpoint" }, { status: 502 }); + return NextResponse.json( + { error: "Failed to reach MCP endpoint" }, + { status: 502 }, + ); } // Record usage and distribute revenue @@ -249,7 +290,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: // Log but don't fail the request - usage tracking is secondary logger.error("[MCP Proxy] Failed to record usage", { mcpId, - error: usageError instanceof Error ? usageError.message : String(usageError), + error: + usageError instanceof Error ? usageError.message : String(usageError), }); } } else { @@ -268,7 +310,10 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: } catch (refundError) { logger.error("[MCP Proxy] Failed to refund credits", { mcpId, - error: refundError instanceof Error ? refundError.message : String(refundError), + error: + refundError instanceof Error + ? refundError.message + : String(refundError), }); } } @@ -279,7 +324,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: return new NextResponse(responseBody, { status: mcpResponse.status, headers: { - "Content-Type": mcpResponse.headers.get("content-type") || "application/json", + "Content-Type": + mcpResponse.headers.get("content-type") || "application/json", "X-MCP-Id": mcp.id, "X-MCP-Name": mcp.name, }, diff --git a/app/api/mcp/registry/route.ts b/app/api/mcp/registry/route.ts index 009b55745..2d7c132d2 100644 --- a/app/api/mcp/registry/route.ts +++ b/app/api/mcp/registry/route.ts @@ -172,7 +172,12 @@ const MCP_REGISTRY: McpRegistryEntry[] = [ icon: "cloud", color: "#3B82F6", toolCount: 4, - features: ["get_current_weather", "get_weather_forecast", "compare_weather", "search_location"], + features: [ + "get_current_weather", + "get_weather_forecast", + "compare_weather", + "search_location", + ], pricing: { type: "credits", description: "1-2 credits per request", @@ -332,7 +337,12 @@ const MCP_REGISTRY: McpRegistryEntry[] = [ icon: "git-branch", color: "#181717", toolCount: 45, - features: ["github_list_repos", "github_create_issue", "github_list_prs", "github_create_pr"], + features: [ + "github_list_repos", + "github_create_issue", + "github_list_prs", + "github_create_pr", + ], pricing: { type: "free", description: "Requires GitHub OAuth connection", @@ -407,24 +417,26 @@ export async function GET(request: NextRequest) { const { category, status, limit, search } = validationResult.data; // Process built-in registry entries - const builtInRegistry: BuiltInRegistryEntry[] = MCP_REGISTRY.map((entry) => ({ - ...entry, - source: "platform" as const, - configTemplate: { - servers: Object.fromEntries( - Object.entries(entry.configTemplate.servers).map(([key, value]) => [ - key, - { - ...value, - url: value.url.replace("${BASE_URL}", ""), - }, - ]), - ), - }, - fullEndpoint: entry.endpoint.startsWith("http") - ? entry.endpoint - : `${baseUrl}${entry.endpoint}`, - })); + const builtInRegistry: BuiltInRegistryEntry[] = MCP_REGISTRY.map( + (entry) => ({ + ...entry, + source: "platform" as const, + configTemplate: { + servers: Object.fromEntries( + Object.entries(entry.configTemplate.servers).map(([key, value]) => [ + key, + { + ...value, + url: value.url.replace("${BASE_URL}", ""), + }, + ]), + ), + }, + fullEndpoint: entry.endpoint.startsWith("http") + ? entry.endpoint + : `${baseUrl}${entry.endpoint}`, + }), + ); // Fetch user MCPs (public, live) let userMcpRegistry: UserRegistryEntry[] = []; @@ -455,7 +467,9 @@ export async function GET(request: NextRequest) { // Apply category filter with validated input if (category && category !== "all") { - filteredRegistry = filteredRegistry.filter((e) => e.category === category); + filteredRegistry = filteredRegistry.filter( + (e) => e.category === category, + ); } // Apply status filter with validated input @@ -501,7 +515,8 @@ export async function GET(request: NextRequest) { logger.error("[MCP Registry] Error:", error); return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to fetch registry", + error: + error instanceof Error ? error.message : "Failed to fetch registry", }, { status: 500 }, ); diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index ef5b06fd0..312a9732e 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -47,7 +47,9 @@ async function handleMcpRequest(req: NextRequest): Promise { try { const authResult = await requireAuthOrApiKeyWithOrg(req); - const rateLimited = await enforceMcpOrganizationRateLimit(authResult.user.organization_id!); + const rateLimited = await enforceMcpOrganizationRateLimit( + authResult.user.organization_id!, + ); if (rateLimited) return rateLimited; // Call MCP handler with auth context (lazy-loaded) @@ -73,7 +75,10 @@ async function handleMcpRequest(req: NextRequest): Promise { const bodyText = mcpResponse.text ? await mcpResponse.text() : ""; const headers: Record = {}; - if (mcpResponse.headers && typeof mcpResponse.headers.forEach === "function") { + if ( + mcpResponse.headers && + typeof mcpResponse.headers.forEach === "function" + ) { mcpResponse.headers.forEach((value: string, key: string) => { headers[key] = value; }); diff --git a/app/api/mcp/stream/route.ts b/app/api/mcp/stream/route.ts index 651619583..15650115c 100644 --- a/app/api/mcp/stream/route.ts +++ b/app/api/mcp/stream/route.ts @@ -1,13 +1,17 @@ export async function GET() { return new Response( - JSON.stringify({ error: "SSE streaming is deprecated. Use streamable-http transport." }), + JSON.stringify({ + error: "SSE streaming is deprecated. Use streamable-http transport.", + }), { status: 410, headers: { "Content-Type": "application/json" } }, ); } export async function POST() { return new Response( - JSON.stringify({ error: "SSE streaming is deprecated. Use streamable-http transport." }), + JSON.stringify({ + error: "SSE streaming is deprecated. Use streamable-http transport.", + }), { status: 410, headers: { "Content-Type": "application/json" } }, ); } diff --git a/app/api/mcp/tools/agents.ts b/app/api/mcp/tools/agents.ts index 190183e0a..06befe66c 100644 --- a/app/api/mcp/tools/agents.ts +++ b/app/api/mcp/tools/agents.ts @@ -33,12 +33,18 @@ export function registerAgentTools(server: McpServer): void { description: "Send a message to your deployed elizaOS agent and receive a response. Supports streaming via SSE. Charges $0.0001-$0.01 based on token usage.", inputSchema: { - message: z.string().min(1).max(4000).describe("Message to send to the agent"), + message: z + .string() + .min(1) + .max(4000) + .describe("Message to send to the agent"), roomId: z .string() .uuid() .optional() - .describe("Existing conversation room ID (creates new if not provided)"), + .describe( + "Existing conversation room ID (creates new if not provided)", + ), entityId: z .string() .optional() @@ -58,13 +64,18 @@ export function registerAgentTools(server: McpServer): void { return errorResponse("Account suspended due to policy violations"); } - contentModerationService.moderateInBackground(message, user.id, roomId, (result) => { - logger.warn("[MCP] chat_with_agent moderation violation", { - userId: user.id, - categories: result.flaggedCategories, - action: result.action, - }); - }); + contentModerationService.moderateInBackground( + message, + user.id, + roomId, + (result) => { + logger.warn("[MCP] chat_with_agent moderation violation", { + userId: user.id, + categories: result.flaggedCategories, + action: result.action, + }); + }, + ); const estimatedInputTokens = Math.ceil(message.length / 4); const estimatedCost = Math.max( @@ -95,7 +106,10 @@ export function registerAgentTools(server: McpServer): void { try { actualRoomId = roomId || - (await agentService.getOrCreateRoom(entityId || user.id, user.organization_id)); + (await agentService.getOrCreateRoom( + entityId || user.id, + user.organization_id, + )); response = await agentService.sendMessage({ roomId: actualRoomId, @@ -110,7 +124,8 @@ export function registerAgentTools(server: McpServer): void { } const actualCost = Math.ceil( - (response.usage?.inputTokens || estimatedInputTokens) * AGENT_CHAT_INPUT_TOKEN_COST + + (response.usage?.inputTokens || estimatedInputTokens) * + AGENT_CHAT_INPUT_TOKEN_COST + (response.usage?.outputTokens || 0) * AGENT_CHAT_OUTPUT_TOKEN_COST, ); @@ -145,7 +160,9 @@ export function registerAgentTools(server: McpServer): void { usage: response.usage, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to chat with agent"); + return errorResponse( + error instanceof Error ? error.message : "Failed to chat with agent", + ); } }, ); @@ -185,7 +202,9 @@ export function registerAgentTools(server: McpServer): void { cached: result.cached, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list agents"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list agents", + ); } }, ); @@ -201,7 +220,8 @@ export function registerAgentTools(server: McpServer): void { }, async ({ roomId }) => { try { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; const sseUrl = `${baseUrl}/api/mcp/stream?eventType=agent&resourceId=${roomId}`; return jsonResponse({ @@ -217,7 +237,9 @@ export function registerAgentTools(server: McpServer): void { ], }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to generate SSE URL"); + return errorResponse( + error instanceof Error ? error.message : "Failed to generate SSE URL", + ); } }, ); @@ -231,7 +253,11 @@ export function registerAgentTools(server: McpServer): void { name: z.string().describe("Agent name"), bio: z.union([z.string(), z.array(z.string())]).describe("Agent bio"), system: z.string().optional().describe("System prompt"), - category: z.string().optional().default("assistant").describe("Agent category"), + category: z + .string() + .optional() + .default("assistant") + .describe("Agent category"), tags: z.array(z.string()).optional().describe("Agent tags"), }, }, @@ -257,7 +283,9 @@ export function registerAgentTools(server: McpServer): void { name: character.name, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to create agent"); + return errorResponse( + error instanceof Error ? error.message : "Failed to create agent", + ); } }, ); @@ -290,12 +318,18 @@ export function registerAgentTools(server: McpServer): void { if (category) updates.category = category; if (tags) updates.tags = tags; - const updated = await charactersService.updateForUser(agentId, user.id, updates); + const updated = await charactersService.updateForUser( + agentId, + user.id, + updates, + ); if (!updated) throw new Error("Agent not found or not owned by user"); return jsonResponse({ success: true, agentId }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to update agent"); + return errorResponse( + error instanceof Error ? error.message : "Failed to update agent", + ); } }, ); @@ -318,7 +352,9 @@ export function registerAgentTools(server: McpServer): void { return jsonResponse({ success: true, agentId }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to delete agent"); + return errorResponse( + error instanceof Error ? error.message : "Failed to delete agent", + ); } }, ); diff --git a/app/api/mcp/tools/airtable.ts b/app/api/mcp/tools/airtable.ts index ddf5a62af..c432d60e3 100644 --- a/app/api/mcp/tools/airtable.ts +++ b/app/api/mcp/tools/airtable.ts @@ -25,7 +25,9 @@ async function getAirtableToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Airtable account not connected. Connect in Settings > Connections."); + throw new Error( + "Airtable account not connected. Connect in Settings > Connections.", + ); } } @@ -42,7 +44,10 @@ async function airtableFetch(endpoint: string, options: RequestInit = {}) { if (!response.ok) { const error = await response.json().catch(() => ({})); - const msg = error?.error?.message || error?.message || `Airtable API error: ${response.status}`; + const msg = + error?.error?.message || + error?.message || + `Airtable API error: ${response.status}`; throw new Error(msg); } @@ -76,7 +81,8 @@ export function registerAirtableTools(server: McpServer): void { if (!active) { return jsonResponse({ connected: false, - message: "Airtable not connected. Connect in Settings > Connections.", + message: + "Airtable not connected. Connect in Settings > Connections.", }); } return jsonResponse({ @@ -95,9 +101,13 @@ export function registerAirtableTools(server: McpServer): void { server.registerTool( "airtable_list_bases", { - description: "List all Airtable bases accessible to the connected account", + description: + "List all Airtable bases accessible to the connected account", inputSchema: { - offset: z.string().optional().describe("Pagination cursor from a previous response"), + offset: z + .string() + .optional() + .describe("Pagination cursor from a previous response"), }, }, async ({ offset }) => { @@ -117,7 +127,8 @@ export function registerAirtableTools(server: McpServer): void { server.registerTool( "airtable_get_base_schema", { - description: "Get the schema of an Airtable base including all tables and their fields", + description: + "Get the schema of an Airtable base including all tables and their fields", inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), }, @@ -140,13 +151,24 @@ export function registerAirtableTools(server: McpServer): void { "List records from an Airtable table with optional filtering, sorting, and field selection", inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), - tableIdOrName: z.string().min(1).describe("Table ID (starts with 'tbl') or table name"), - fields: z.array(z.string()).optional().describe("Only return these fields"), + tableIdOrName: z + .string() + .min(1) + .describe("Table ID (starts with 'tbl') or table name"), + fields: z + .array(z.string()) + .optional() + .describe("Only return these fields"), filterByFormula: z .string() .optional() .describe("Airtable formula to filter records, e.g. {Status}='Done'"), - maxRecords: z.number().int().min(1).optional().describe("Maximum total records to return"), + maxRecords: z + .number() + .int() + .min(1) + .optional() + .describe("Maximum total records to return"), pageSize: z .number() .int() @@ -154,12 +176,23 @@ export function registerAirtableTools(server: McpServer): void { .max(100) .optional() .describe("Records per page (max 100)"), - offset: z.string().optional().describe("Pagination cursor from a previous response"), + offset: z + .string() + .optional() + .describe("Pagination cursor from a previous response"), sort: z - .array(z.object({ field: z.string(), direction: z.enum(["asc", "desc"]).optional() })) + .array( + z.object({ + field: z.string(), + direction: z.enum(["asc", "desc"]).optional(), + }), + ) .optional() .describe("Sort configuration"), - view: z.string().optional().describe("View name or ID to use for filtering/sorting"), + view: z + .string() + .optional() + .describe("View name or ID to use for filtering/sorting"), }, }, async ({ @@ -206,7 +239,10 @@ export function registerAirtableTools(server: McpServer): void { inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), tableIdOrName: z.string().min(1).describe("Table ID or table name"), - recordId: z.string().min(1).describe("The record ID (starts with 'rec')"), + recordId: z + .string() + .min(1) + .describe("The record ID (starts with 'rec')"), }, }, async ({ baseId, tableIdOrName, recordId }) => { @@ -225,7 +261,8 @@ export function registerAirtableTools(server: McpServer): void { server.registerTool( "airtable_create_records", { - description: "Create one or more records in an Airtable table (max 10 per request)", + description: + "Create one or more records in an Airtable table (max 10 per request)", inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), tableIdOrName: z.string().min(1).describe("Table ID or table name"), @@ -244,10 +281,13 @@ export function registerAirtableTools(server: McpServer): void { try { const body: Record = { records }; if (typecast !== undefined) body.typecast = typecast; - const data = await airtableFetch(`/v0/${baseId}/${encodeURIComponent(tableIdOrName)}`, { - method: "POST", - body: JSON.stringify(body), - }); + const data = await airtableFetch( + `/v0/${baseId}/${encodeURIComponent(tableIdOrName)}`, + { + method: "POST", + body: JSON.stringify(body), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create records")); @@ -279,10 +319,13 @@ export function registerAirtableTools(server: McpServer): void { try { const body: Record = { records }; if (typecast !== undefined) body.typecast = typecast; - const data = await airtableFetch(`/v0/${baseId}/${encodeURIComponent(tableIdOrName)}`, { - method: "PATCH", - body: JSON.stringify(body), - }); + const data = await airtableFetch( + `/v0/${baseId}/${encodeURIComponent(tableIdOrName)}`, + { + method: "PATCH", + body: JSON.stringify(body), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update records")); @@ -294,7 +337,8 @@ export function registerAirtableTools(server: McpServer): void { server.registerTool( "airtable_delete_records", { - description: "Delete one or more records from an Airtable table (max 10 per request)", + description: + "Delete one or more records from an Airtable table (max 10 per request)", inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), tableIdOrName: z.string().min(1).describe("Table ID or table name"), @@ -337,10 +381,23 @@ export function registerAirtableTools(server: McpServer): void { .describe( "Airtable formula, e.g. SEARCH('term',{Name}), AND({Status}='Active',{Priority}='High')", ), - fields: z.array(z.string()).optional().describe("Only return these fields"), - maxRecords: z.number().int().min(1).optional().describe("Maximum records to return"), + fields: z + .array(z.string()) + .optional() + .describe("Only return these fields"), + maxRecords: z + .number() + .int() + .min(1) + .optional() + .describe("Maximum records to return"), sort: z - .array(z.object({ field: z.string(), direction: z.enum(["asc", "desc"]).optional() })) + .array( + z.object({ + field: z.string(), + direction: z.enum(["asc", "desc"]).optional(), + }), + ) .optional() .describe("Sort configuration"), }, @@ -375,7 +432,10 @@ export function registerAirtableTools(server: McpServer): void { inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), name: z.string().min(1).describe("Name for the new table"), - description: z.string().optional().describe("Optional table description"), + description: z + .string() + .optional() + .describe("Optional table description"), fields: z .array( z.object({ @@ -421,10 +481,13 @@ export function registerAirtableTools(server: McpServer): void { const body: Record = {}; if (name) body.name = name; if (description !== undefined) body.description = description; - const data = await airtableFetch(`/v0/meta/bases/${baseId}/tables/${tableId}`, { - method: "PATCH", - body: JSON.stringify(body), - }); + const data = await airtableFetch( + `/v0/meta/bases/${baseId}/tables/${tableId}`, + { + method: "PATCH", + body: JSON.stringify(body), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update table")); @@ -444,9 +507,14 @@ export function registerAirtableTools(server: McpServer): void { type: z .string() .min(1) - .describe("Field type, e.g. singleLineText, number, singleSelect, date, checkbox, etc."), + .describe( + "Field type, e.g. singleLineText, number, singleSelect, date, checkbox, etc.", + ), description: z.string().optional().describe("Field description"), - options: z.record(z.any()).optional().describe("Field-type-specific options"), + options: z + .record(z.any()) + .optional() + .describe("Field-type-specific options"), }, }, async ({ baseId, tableId, name, type, description, options }) => { @@ -454,10 +522,13 @@ export function registerAirtableTools(server: McpServer): void { const body: Record = { name, type }; if (description) body.description = description; if (options) body.options = options; - const data = await airtableFetch(`/v0/meta/bases/${baseId}/tables/${tableId}/fields`, { - method: "POST", - body: JSON.stringify(body), - }); + const data = await airtableFetch( + `/v0/meta/bases/${baseId}/tables/${tableId}/fields`, + { + method: "POST", + body: JSON.stringify(body), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create field")); @@ -469,7 +540,8 @@ export function registerAirtableTools(server: McpServer): void { server.registerTool( "airtable_update_field", { - description: "Update an existing field's name, description, or options in an Airtable table", + description: + "Update an existing field's name, description, or options in an Airtable table", inputSchema: { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), tableId: z.string().min(1).describe("The table ID (starts with 'tbl')"), diff --git a/app/api/mcp/tools/analytics.ts b/app/api/mcp/tools/analytics.ts index 4561bda19..2b1f9dc14 100644 --- a/app/api/mcp/tools/analytics.ts +++ b/app/api/mcp/tools/analytics.ts @@ -17,7 +17,11 @@ export function registerAnalyticsTools(server: McpServer): void { { description: "Get organization analytics. FREE tool.", inputSchema: { - period: z.enum(["day", "week", "month"]).optional().default("week").describe("Time period"), + period: z + .enum(["day", "week", "month"]) + .optional() + .default("week") + .describe("Time period"), }, }, async ({ period }) => { @@ -33,14 +37,19 @@ export function registerAnalyticsTools(server: McpServer): void { startDate.setMonth(now.getMonth() - 1); } - const analytics = await analyticsService.getUsageStats(user.organization_id, { - startDate, - endDate: now, - }); + const analytics = await analyticsService.getUsageStats( + user.organization_id, + { + startDate, + endDate: now, + }, + ); return jsonResponse({ success: true, analytics }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to get analytics"); + return errorResponse( + error instanceof Error ? error.message : "Failed to get analytics", + ); } }, ); @@ -48,13 +57,15 @@ export function registerAnalyticsTools(server: McpServer): void { server.registerTool( "stream_credit_updates", { - description: "Get SSE stream URL for real-time credit updates. FREE tool.", + description: + "Get SSE stream URL for real-time credit updates. FREE tool.", inputSchema: {}, }, async () => { try { const { user } = getAuthContext(); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; const sseUrl = `${baseUrl}/api/mcp/stream?eventType=credits&resourceId=${user.organization_id}`; return jsonResponse({ @@ -64,7 +75,9 @@ export function registerAnalyticsTools(server: McpServer): void { eventTypes: ["balance_update", "transaction", "low_balance_alert"], }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to generate SSE URL"); + return errorResponse( + error instanceof Error ? error.message : "Failed to generate SSE URL", + ); } }, ); @@ -76,20 +89,37 @@ export function registerAnalyticsTools(server: McpServer): void { "Discover services (agents, MCPs, apps) from Eliza Cloud. " + "Use this to find services to interact with. FREE tool.", inputSchema: { - query: z.string().optional().describe("Search query to filter by name or description"), + query: z + .string() + .optional() + .describe("Search query to filter by name or description"), types: z .array(z.enum(["agent", "mcp", "a2a", "app"])) .optional() .describe("Types of services to find"), - categories: z.array(z.string()).optional().describe("Filter by categories"), + categories: z + .array(z.string()) + .optional() + .describe("Filter by categories"), tags: z.array(z.string()).optional().describe("Filter by tags"), - x402Only: z.boolean().optional().describe("Only return services with x402 payment support"), - limit: z.number().int().min(1).max(50).optional().default(20).describe("Max results"), + x402Only: z + .boolean() + .optional() + .describe("Only return services with x402 payment support"), + limit: z + .number() + .int() + .min(1) + .max(50) + .optional() + .default(20) + .describe("Max results"), }, }, async ({ query, types, categories, x402Only, limit }) => { try { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const services: Array<{ id: string; name: string; @@ -112,8 +142,10 @@ export function registerAnalyticsTools(server: McpServer): void { chars = chars.filter( (c) => c.name.toLowerCase().includes(q) || - (typeof c.bio === "string" && c.bio.toLowerCase().includes(q)) || - (Array.isArray(c.bio) && c.bio.some((b) => b.toLowerCase().includes(q))), + (typeof c.bio === "string" && + c.bio.toLowerCase().includes(q)) || + (Array.isArray(c.bio) && + c.bio.some((b) => b.toLowerCase().includes(q))), ); } if (categories?.length) { @@ -124,7 +156,9 @@ export function registerAnalyticsTools(server: McpServer): void { services.push({ id: char.id, name: char.name, - description: Array.isArray(char.bio) ? char.bio.join(" ") : char.bio, + description: Array.isArray(char.bio) + ? char.bio.join(" ") + : char.bio, type: "agent", source: "local", a2aEndpoint: `${baseUrl}/api/agents/${char.id}/a2a`, @@ -163,7 +197,9 @@ export function registerAnalyticsTools(server: McpServer): void { services: services.slice(0, limit), }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Discovery failed"); + return errorResponse( + error instanceof Error ? error.message : "Discovery failed", + ); } }, ); diff --git a/app/api/mcp/tools/api-keys.ts b/app/api/mcp/tools/api-keys.ts index 47d6f4bfe..dbf9b90b3 100644 --- a/app/api/mcp/tools/api-keys.ts +++ b/app/api/mcp/tools/api-keys.ts @@ -20,7 +20,9 @@ export function registerApiKeyTools(server: McpServer): void { async () => { try { const { user } = getAuthContext(); - const keys = await apiKeysService.listByOrganization(user.organization_id); + const keys = await apiKeysService.listByOrganization( + user.organization_id, + ); return jsonResponse({ success: true, @@ -33,7 +35,9 @@ export function registerApiKeyTools(server: McpServer): void { })), }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list API keys"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list API keys", + ); } }, ); @@ -42,7 +46,8 @@ export function registerApiKeyTools(server: McpServer): void { server.registerTool( "create_api_key", { - description: "Create a new API key. FREE tool. Returns plain key only once!", + description: + "Create a new API key. FREE tool. Returns plain key only once!", inputSchema: { name: z.string().describe("API key name (required)"), description: z.string().optional().describe("Description"), @@ -79,7 +84,9 @@ export function registerApiKeyTools(server: McpServer): void { warning: "Store this key securely - it will not be shown again!", }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to create API key"); + return errorResponse( + error instanceof Error ? error.message : "Failed to create API key", + ); } }, ); @@ -110,7 +117,9 @@ export function registerApiKeyTools(server: McpServer): void { return jsonResponse({ success: true, apiKeyId }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to delete API key"); + return errorResponse( + error instanceof Error ? error.message : "Failed to delete API key", + ); } }, ); diff --git a/app/api/mcp/tools/asana.ts b/app/api/mcp/tools/asana.ts index b8c3ea3fb..e4daac23b 100644 --- a/app/api/mcp/tools/asana.ts +++ b/app/api/mcp/tools/asana.ts @@ -32,7 +32,9 @@ async function getAsanaToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Asana account not connected. Connect in Settings > Connections."); + throw new Error( + "Asana account not connected. Connect in Settings > Connections.", + ); } } @@ -58,7 +60,9 @@ async function asanaApi(method: string, path: string, body?: unknown) { try { parsed = JSON.parse(errorText); } catch {} - throw new Error(parsed?.errors?.[0]?.message || `Asana API error: ${response.status}`); + throw new Error( + parsed?.errors?.[0]?.message || `Asana API error: ${response.status}`, + ); } if (response.status === 204) return { success: true }; @@ -102,7 +106,10 @@ export function registerAsanaTools(server: McpServer): void { server.registerTool( "asana_get_myself", - { description: "Get the currently authenticated Asana user", inputSchema: {} }, + { + description: "Get the currently authenticated Asana user", + inputSchema: {}, + }, async () => { try { const data = await asanaApi( @@ -121,12 +128,20 @@ export function registerAsanaTools(server: McpServer): void { { description: "List Asana workspaces accessible to the user", inputSchema: { - limit: z.number().int().min(1).max(100).optional().describe("Max results (default 100)"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results (default 100)"), }, }, async ({ limit }) => { try { - const params = new URLSearchParams({ opt_fields: "gid,name,is_organization" }); + const params = new URLSearchParams({ + opt_fields: "gid,name,is_organization", + }); if (limit) params.set("limit", String(limit)); const data = await asanaApi("GET", `/workspaces?${params.toString()}`); return jsonResponse(data); @@ -142,14 +157,24 @@ export function registerAsanaTools(server: McpServer): void { description: "List projects in an Asana workspace", inputSchema: { workspaceGid: z.string().min(1).describe("Workspace GID"), - archived: z.boolean().optional().describe("Include archived projects (default false)"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + archived: z + .boolean() + .optional() + .describe("Include archived projects (default false)"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, }, async ({ workspaceGid, archived, limit }) => { try { const params = new URLSearchParams({ - opt_fields: "gid,name,color,created_at,modified_at,owner,team,archived", + opt_fields: + "gid,name,color,created_at,modified_at,owner,team,archived", }); if (archived !== undefined) params.set("archived", String(archived)); if (limit) params.set("limit", String(limit)); @@ -189,7 +214,13 @@ export function registerAsanaTools(server: McpServer): void { description: "List tasks in an Asana project", inputSchema: { projectGid: z.string().min(1).describe("Project GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), offset: z.string().optional().describe("Pagination offset token"), }, }, @@ -201,7 +232,10 @@ export function registerAsanaTools(server: McpServer): void { }); if (limit) params.set("limit", String(limit)); if (offset) params.set("offset", offset); - const data = await asanaApi("GET", `/projects/${projectGid}/tasks?${params.toString()}`); + const data = await asanaApi( + "GET", + `/projects/${projectGid}/tasks?${params.toString()}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list tasks")); @@ -234,16 +268,34 @@ export function registerAsanaTools(server: McpServer): void { description: "Create a new Asana task", inputSchema: { name: z.string().min(1).describe("Task name/title"), - workspaceGid: z.string().optional().describe("Workspace GID (required if no projectGid)"), - projectGid: z.string().optional().describe("Project GID to add the task to"), + workspaceGid: z + .string() + .optional() + .describe("Workspace GID (required if no projectGid)"), + projectGid: z + .string() + .optional() + .describe("Project GID to add the task to"), notes: z.string().optional().describe("Task description"), assignee: z.string().optional().describe("Assignee GID or email"), dueOn: z.string().optional().describe("Due date (YYYY-MM-DD)"), startOn: z.string().optional().describe("Start date (YYYY-MM-DD)"), - parentGid: z.string().optional().describe("Parent task GID (for subtasks)"), + parentGid: z + .string() + .optional() + .describe("Parent task GID (for subtasks)"), }, }, - async ({ name, workspaceGid, projectGid, notes, assignee, dueOn, startOn, parentGid }) => { + async ({ + name, + workspaceGid, + projectGid, + notes, + assignee, + dueOn, + startOn, + parentGid, + }) => { try { const taskData: Record = { name }; if (workspaceGid) taskData.workspace = workspaceGid; @@ -284,7 +336,9 @@ export function registerAsanaTools(server: McpServer): void { if (dueOn !== undefined) taskData.due_on = dueOn; if (startOn !== undefined) taskData.start_on = startOn; if (completed !== undefined) taskData.completed = completed; - const data = await asanaApi("PUT", `/tasks/${taskGid}`, { data: taskData }); + const data = await asanaApi("PUT", `/tasks/${taskGid}`, { + data: taskData, + }); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update task")); @@ -300,17 +354,39 @@ export function registerAsanaTools(server: McpServer): void { inputSchema: { workspaceGid: z.string().min(1).describe("Workspace GID"), text: z.string().optional().describe("Free text search"), - assignee: z.string().optional().describe("Assignee GID (use 'me' for current user)"), + assignee: z + .string() + .optional() + .describe("Assignee GID (use 'me' for current user)"), projectGid: z.string().optional().describe("Filter by project GID"), - completed: z.boolean().optional().describe("Filter by completion status"), + completed: z + .boolean() + .optional() + .describe("Filter by completion status"), sortBy: z .string() .optional() - .describe("Sort by: created_at, completed_at, modified_at, due_date, likes"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + .describe( + "Sort by: created_at, completed_at, modified_at, due_date, likes", + ), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, }, - async ({ workspaceGid, text, assignee, projectGid, completed, sortBy, limit }) => { + async ({ + workspaceGid, + text, + assignee, + projectGid, + completed, + sortBy, + limit, + }) => { try { const params = new URLSearchParams({ opt_fields: @@ -344,7 +420,9 @@ export function registerAsanaTools(server: McpServer): void { }, async ({ taskGid, text }) => { try { - const data = await asanaApi("POST", `/tasks/${taskGid}/stories`, { data: { text } }); + const data = await asanaApi("POST", `/tasks/${taskGid}/stories`, { + data: { text }, + }); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to add comment")); @@ -358,16 +436,26 @@ export function registerAsanaTools(server: McpServer): void { description: "List comments on an Asana task", inputSchema: { taskGid: z.string().min(1).describe("Task GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, }, async ({ taskGid, limit }) => { try { const params = new URLSearchParams({ - opt_fields: "gid,text,created_at,created_by,created_by.name,resource_subtype", + opt_fields: + "gid,text,created_at,created_by,created_by.name,resource_subtype", }); if (limit) params.set("limit", String(limit)); - const data = await asanaApi("GET", `/tasks/${taskGid}/stories?${params.toString()}`); + const data = await asanaApi( + "GET", + `/tasks/${taskGid}/stories?${params.toString()}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list comments")); @@ -381,14 +469,25 @@ export function registerAsanaTools(server: McpServer): void { description: "List sections in an Asana project", inputSchema: { projectGid: z.string().min(1).describe("Project GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, }, async ({ projectGid, limit }) => { try { - const params = new URLSearchParams({ opt_fields: "gid,name,created_at" }); + const params = new URLSearchParams({ + opt_fields: "gid,name,created_at", + }); if (limit) params.set("limit", String(limit)); - const data = await asanaApi("GET", `/projects/${projectGid}/sections?${params.toString()}`); + const data = await asanaApi( + "GET", + `/projects/${projectGid}/sections?${params.toString()}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list sections")); @@ -402,12 +501,20 @@ export function registerAsanaTools(server: McpServer): void { description: "List users in an Asana workspace", inputSchema: { workspaceGid: z.string().min(1).describe("Workspace GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, }, async ({ workspaceGid, limit }) => { try { - const params = new URLSearchParams({ opt_fields: "gid,name,email,photo" }); + const params = new URLSearchParams({ + opt_fields: "gid,name,email,photo", + }); if (limit) params.set("limit", String(limit)); const data = await asanaApi( "GET", diff --git a/app/api/mcp/tools/containers.ts b/app/api/mcp/tools/containers.ts index b0adc53f0..93958fbdd 100644 --- a/app/api/mcp/tools/containers.ts +++ b/app/api/mcp/tools/containers.ts @@ -6,7 +6,11 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v3"; -import { containersService, deleteContainer, getContainer } from "@/lib/services/containers"; +import { + containersService, + deleteContainer, + getContainer, +} from "@/lib/services/containers"; import { type CreditReservation, creditsService, @@ -22,28 +26,34 @@ export function registerContainerTools(server: McpServer): void { { description: "List all deployed containers with status. FREE tool.", inputSchema: { - status: z.enum(["running", "stopped", "failed", "deploying"]).optional(), + status: z + .enum(["running", "stopped", "failed", "deploying"]) + .optional(), includeMetrics: z.boolean().optional().default(false), }, }, async ({ status }) => { try { const { user } = getAuthContext(); - let containers = await containersService.listByOrganization(user.organization_id); + let containers = await containersService.listByOrganization( + user.organization_id, + ); if (status) { containers = containers.filter((c) => c.status === status); } - const formattedContainers = containers.map((container: (typeof containers)[0]) => ({ - id: container.id, - name: container.name, - status: container.status, - url: container.load_balancer_url, - createdAt: container.created_at, - errorMessage: container.error_message, - ecsServiceArn: container.ecs_service_arn, - })); + const formattedContainers = containers.map( + (container: (typeof containers)[0]) => ({ + id: container.id, + name: container.name, + status: container.status, + url: container.load_balancer_url, + createdAt: container.created_at, + errorMessage: container.error_message, + ecsServiceArn: container.ecs_service_arn, + }), + ); return jsonResponse({ success: true, @@ -51,7 +61,9 @@ export function registerContainerTools(server: McpServer): void { total: formattedContainers.length, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list containers"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list containers", + ); } }, ); @@ -81,7 +93,9 @@ export function registerContainerTools(server: McpServer): void { }, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to get container"); + return errorResponse( + error instanceof Error ? error.message : "Failed to get container", + ); } }, ); @@ -108,7 +122,9 @@ export function registerContainerTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get container health", + error instanceof Error + ? error.message + : "Failed to get container health", ); } }, @@ -121,7 +137,14 @@ export function registerContainerTools(server: McpServer): void { description: "Get container logs. FREE tool.", inputSchema: { containerId: z.string().uuid().describe("Container ID"), - limit: z.number().int().min(1).max(100).optional().default(50).describe("Max log entries"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .default(50) + .describe("Max log entries"), }, }, async ({ containerId, limit }) => { @@ -137,7 +160,9 @@ export function registerContainerTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get container logs", + error instanceof Error + ? error.message + : "Failed to get container logs", ); } }, @@ -153,13 +178,45 @@ export function registerContainerTools(server: McpServer): void { name: z.string().min(1).max(100).describe("Container name"), ecrImageUri: z.string().describe("ECR image URI"), projectName: z.string().min(1).max(50).describe("Project name"), - port: z.number().int().min(1).max(65535).optional().default(3000).describe("Port"), - cpu: z.number().int().min(256).max(2048).optional().default(1792).describe("CPU units"), - memory: z.number().int().min(256).max(2048).optional().default(1792).describe("Memory MB"), - environmentVars: z.record(z.string()).optional().describe("Environment variables"), + port: z + .number() + .int() + .min(1) + .max(65535) + .optional() + .default(3000) + .describe("Port"), + cpu: z + .number() + .int() + .min(256) + .max(2048) + .optional() + .default(1792) + .describe("CPU units"), + memory: z + .number() + .int() + .min(256) + .max(2048) + .optional() + .default(1792) + .describe("Memory MB"), + environmentVars: z + .record(z.string()) + .optional() + .describe("Environment variables"), }, }, - async ({ name, ecrImageUri, projectName, port, cpu, memory, environmentVars }) => { + async ({ + name, + ecrImageUri, + projectName, + port, + cpu, + memory, + environmentVars, + }) => { try { const { user } = getAuthContext(); const DEPLOYMENT_COST = 10; @@ -219,7 +276,9 @@ export function registerContainerTools(server: McpServer): void { cost: DEPLOYMENT_COST, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to create container"); + return errorResponse( + error instanceof Error ? error.message : "Failed to create container", + ); } }, ); @@ -242,7 +301,9 @@ export function registerContainerTools(server: McpServer): void { await deleteContainer(containerId, user.organization_id); return jsonResponse({ success: true, containerId }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to delete container"); + return errorResponse( + error instanceof Error ? error.message : "Failed to delete container", + ); } }, ); @@ -274,7 +335,9 @@ export function registerContainerTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get container metrics", + error instanceof Error + ? error.message + : "Failed to get container metrics", ); } }, @@ -290,7 +353,9 @@ export function registerContainerTools(server: McpServer): void { async () => { try { const { user } = getAuthContext(); - const containers = await containersService.listByOrganization(user.organization_id); + const containers = await containersService.listByOrganization( + user.organization_id, + ); return jsonResponse({ success: true, @@ -302,7 +367,9 @@ export function registerContainerTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get container quota", + error instanceof Error + ? error.message + : "Failed to get container quota", ); } }, diff --git a/app/api/mcp/tools/conversations.ts b/app/api/mcp/tools/conversations.ts index 46fe3ac95..5b65ba294 100644 --- a/app/api/mcp/tools/conversations.ts +++ b/app/api/mcp/tools/conversations.ts @@ -79,7 +79,11 @@ export function registerConversationTools(server: McpServer): void { let context; try { - context = await memoryService.getRoomContext(roomId, user.organization_id, depth); + context = await memoryService.getRoomContext( + roomId, + user.organization_id, + depth, + ); } catch (opError) { await reservation?.reconcile(0); throw opError; @@ -101,7 +105,9 @@ export function registerConversationTools(server: McpServer): void { is_successful: true, }); - const tokenEstimate = await memoryService.estimateTokenCount(context.messages); + const tokenEstimate = await memoryService.estimateTokenCount( + context.messages, + ); return jsonResponse({ roomId: context.roomId, @@ -119,7 +125,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get conversation context", + error instanceof Error + ? error.message + : "Failed to get conversation context", ); } }, @@ -129,11 +137,18 @@ export function registerConversationTools(server: McpServer): void { server.registerTool( "create_conversation", { - description: "Create a new conversation context with initial settings. Deducts 1 credit.", + description: + "Create a new conversation context with initial settings. Deducts 1 credit.", inputSchema: { title: z.string().min(1).describe("Conversation title"), - model: z.string().optional().describe("Default model to use (default: gpt-4o)"), - systemPrompt: z.string().optional().describe("System prompt for conversation"), + model: z + .string() + .optional() + .describe("Default model to use (default: gpt-4o)"), + systemPrompt: z + .string() + .optional() + .describe("System prompt for conversation"), settings: z .object({ temperature: z.number().optional(), @@ -210,7 +225,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to create conversation", + error instanceof Error + ? error.message + : "Failed to create conversation", ); } }, @@ -223,11 +240,21 @@ export function registerConversationTools(server: McpServer): void { description: "Search through conversation history with filters. Deducts 2 credits per search.", inputSchema: { - query: z.string().optional().describe("Search query (semantic or keyword)"), + query: z + .string() + .optional() + .describe("Search query (semantic or keyword)"), model: z.array(z.string()).optional().describe("Filter by model used"), dateFrom: z.string().optional().describe("ISO date string (from)"), dateTo: z.string().optional().describe("ISO date string (to)"), - limit: z.number().int().min(1).max(50).optional().default(10).describe("Maximum results"), + limit: z + .number() + .int() + .min(1) + .max(50) + .optional() + .default(10) + .describe("Maximum results"), }, }, async ({ query, limit = 10 }) => { @@ -293,7 +320,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to search conversations", + error instanceof Error + ? error.message + : "Failed to search conversations", ); } }, @@ -327,7 +356,12 @@ export function registerConversationTools(server: McpServer): void { .describe("Include participant and topic metadata"), }, }, - async ({ roomId, lastN = 50, style = "brief", includeMetadata = false }) => { + async ({ + roomId, + lastN = 50, + style = "brief", + includeMetadata = false, + }) => { try { const { user } = getAuthContext(); @@ -396,7 +430,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to summarize conversation", + error instanceof Error + ? error.message + : "Failed to summarize conversation", ); } }, @@ -410,8 +446,16 @@ export function registerConversationTools(server: McpServer): void { "Intelligently select the most relevant context for token-limited requests. Deducts 5 credits.", inputSchema: { roomId: z.string().describe("Room/conversation ID"), - maxTokens: z.number().int().min(100).max(100000).describe("Token budget for context"), - query: z.string().optional().describe("Current user query for relevance scoring"), + maxTokens: z + .number() + .int() + .min(100) + .max(100000) + .describe("Token budget for context"), + query: z + .string() + .optional() + .describe("Current user query for relevance scoring"), preserveRecent: z .number() .int() @@ -487,7 +531,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to optimize context window", + error instanceof Error + ? error.message + : "Failed to optimize context window", ); } }, @@ -502,7 +548,11 @@ export function registerConversationTools(server: McpServer): void { inputSchema: { conversationId: z.string().describe("Conversation ID to export"), format: z.enum(["json", "markdown", "txt"]).describe("Export format"), - includeMemories: z.boolean().optional().default(false).describe("Include related memories"), + includeMemories: z + .boolean() + .optional() + .default(false) + .describe("Include related memories"), includeMetadata: z .boolean() .optional() @@ -568,7 +618,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to export conversation", + error instanceof Error + ? error.message + : "Failed to export conversation", ); } }, @@ -578,12 +630,24 @@ export function registerConversationTools(server: McpServer): void { server.registerTool( "clone_conversation", { - description: "Duplicate a conversation with optional modifications. Deducts 2 credits.", + description: + "Duplicate a conversation with optional modifications. Deducts 2 credits.", inputSchema: { conversationId: z.string().describe("Source conversation ID"), - newTitle: z.string().optional().describe("New title (defaults to 'Original (Copy)')"), - preserveMessages: z.boolean().optional().default(true).describe("Copy all messages"), - preserveMemories: z.boolean().optional().default(false).describe("Copy related memories"), + newTitle: z + .string() + .optional() + .describe("New title (defaults to 'Original (Copy)')"), + preserveMessages: z + .boolean() + .optional() + .default(true) + .describe("Copy all messages"), + preserveMemories: z + .boolean() + .optional() + .default(false) + .describe("Copy related memories"), newModel: z.string().optional().describe("Change model (optional)"), }, }, @@ -657,7 +721,9 @@ export function registerConversationTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to clone conversation", + error instanceof Error + ? error.message + : "Failed to clone conversation", ); } }, diff --git a/app/api/mcp/tools/credits.ts b/app/api/mcp/tools/credits.ts index 96488c171..bc8a9e57e 100644 --- a/app/api/mcp/tools/credits.ts +++ b/app/api/mcp/tools/credits.ts @@ -18,7 +18,8 @@ export function registerCreditTools(server: McpServer): void { server.registerTool( "check_credits", { - description: "Check balance and recent transactions for your organization", + description: + "Check balance and recent transactions for your organization", inputSchema: { includeTransactions: z .boolean() @@ -57,10 +58,11 @@ export function registerCreditTools(server: McpServer): void { }; if (includeTransactions) { - const transactions = await creditsService.listTransactionsByOrganization( - user.organization_id, - limit, - ); + const transactions = + await creditsService.listTransactionsByOrganization( + user.organization_id, + limit, + ); response.transactions = transactions.map((t) => ({ id: t.id, amount: Number(t.amount), @@ -72,7 +74,9 @@ export function registerCreditTools(server: McpServer): void { return jsonResponse(response); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to check credits"); + return errorResponse( + error instanceof Error ? error.message : "Failed to check credits", + ); } }, ); @@ -81,7 +85,8 @@ export function registerCreditTools(server: McpServer): void { server.registerTool( "get_recent_usage", { - description: "Get recent API usage statistics including models used, costs, and tokens", + description: + "Get recent API usage statistics including models used, costs, and tokens", inputSchema: { limit: z .number() @@ -97,7 +102,10 @@ export function registerCreditTools(server: McpServer): void { try { const { user } = getAuthContext(); - const usageRecords = await usageService.listByOrganization(user.organization_id, limit); + const usageRecords = await usageService.listByOrganization( + user.organization_id, + limit, + ); const formattedUsage = usageRecords.map((record) => ({ id: record.id, @@ -108,13 +116,17 @@ export function registerCreditTools(server: McpServer): void { outputTokens: record.output_tokens, inputCost: record.input_cost || 0, outputCost: record.output_cost || 0, - totalCost: Number(record.input_cost || 0) + Number(record.output_cost || 0), + totalCost: + Number(record.input_cost || 0) + Number(record.output_cost || 0), isSuccessful: record.is_successful, errorMessage: record.error_message, createdAt: record.created_at.toISOString(), })); - const totalCost = formattedUsage.reduce((sum, record) => sum + record.totalCost, 0); + const totalCost = formattedUsage.reduce( + (sum, record) => sum + record.totalCost, + 0, + ); return jsonResponse({ usage: formattedUsage, @@ -124,7 +136,9 @@ export function registerCreditTools(server: McpServer): void { }, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to fetch usage"); + return errorResponse( + error instanceof Error ? error.message : "Failed to fetch usage", + ); } }, ); @@ -142,7 +156,9 @@ export function registerCreditTools(server: McpServer): void { const org = user.organization; const redeemable = await redeemableEarningsService.getBalance(user.id); - const agentBudgets = await agentBudgetService.getOrgBudgets(user.organization_id); + const agentBudgets = await agentBudgetService.getOrgBudgets( + user.organization_id, + ); const totalAgentBudgets = agentBudgets.reduce((sum, b) => { const allocated = Number(b.allocated_budget); const spent = Number(b.spent_budget); @@ -160,7 +176,9 @@ export function registerCreditTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get credit summary", + error instanceof Error + ? error.message + : "Failed to get credit summary", ); } }, @@ -172,8 +190,20 @@ export function registerCreditTools(server: McpServer): void { { description: "List credit transactions. FREE tool.", inputSchema: { - limit: z.number().int().min(1).max(100).optional().default(50).describe("Max results"), - hours: z.number().int().min(1).optional().describe("Filter to last N hours"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .default(50) + .describe("Max results"), + hours: z + .number() + .int() + .min(1) + .optional() + .describe("Filter to last N hours"), }, }, async ({ limit, hours }) => { @@ -186,7 +216,9 @@ export function registerCreditTools(server: McpServer): void { if (hours) { const cutoffTime = new Date(Date.now() - hours * 60 * 60 * 1000); - transactions = transactions.filter((t) => new Date(t.created_at) >= cutoffTime); + transactions = transactions.filter( + (t) => new Date(t.created_at) >= cutoffTime, + ); } return jsonResponse({ @@ -202,7 +234,9 @@ export function registerCreditTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to list transactions", + error instanceof Error + ? error.message + : "Failed to list transactions", ); } }, @@ -226,8 +260,10 @@ export function registerCreditTools(server: McpServer): void { currency?: string; popular?: boolean; }; - const currency = typeof metadata.currency === "string" ? metadata.currency : "USD"; - const popular = typeof metadata.popular === "boolean" ? metadata.popular : false; + const currency = + typeof metadata.currency === "string" ? metadata.currency : "USD"; + const popular = + typeof metadata.popular === "boolean" ? metadata.popular : false; return { id: p.id, @@ -241,7 +277,9 @@ export function registerCreditTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to list credit packs", + error instanceof Error + ? error.message + : "Failed to list credit packs", ); } }, @@ -253,19 +291,32 @@ export function registerCreditTools(server: McpServer): void { { description: "Get billing usage statistics. FREE tool.", inputSchema: { - days: z.number().int().min(1).max(90).optional().default(30).describe("Days to include"), + days: z + .number() + .int() + .min(1) + .max(90) + .optional() + .default(30) + .describe("Days to include"), }, }, async ({ days }) => { try { const { user } = getAuthContext(); - const usage = await usageService.listByOrganization(user.organization_id, 1000); + const usage = await usageService.listByOrganization( + user.organization_id, + 1000, + ); const cutoffTime = new Date(Date.now() - days * 24 * 60 * 60 * 1000); - const recentUsage = usage.filter((u) => new Date(u.created_at) >= cutoffTime); + const recentUsage = usage.filter( + (u) => new Date(u.created_at) >= cutoffTime, + ); const totalCost = recentUsage.reduce( - (sum, u) => sum + Number(u.input_cost || 0) + Number(u.output_cost || 0), + (sum, u) => + sum + Number(u.input_cost || 0) + Number(u.output_cost || 0), 0, ); const totalTokens = recentUsage.reduce( @@ -284,13 +335,16 @@ export function registerCreditTools(server: McpServer): void { chat: recentUsage.filter((u) => u.type === "chat").length, image: recentUsage.filter((u) => u.type === "image").length, video: recentUsage.filter((u) => u.type === "video").length, - embedding: recentUsage.filter((u) => u.type === "embedding").length, + embedding: recentUsage.filter((u) => u.type === "embedding") + .length, }, }, }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get billing usage", + error instanceof Error + ? error.message + : "Failed to get billing usage", ); } }, diff --git a/app/api/mcp/tools/dropbox.ts b/app/api/mcp/tools/dropbox.ts index 65e40890e..4c32b8e38 100644 --- a/app/api/mcp/tools/dropbox.ts +++ b/app/api/mcp/tools/dropbox.ts @@ -25,7 +25,9 @@ async function getDropboxToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Dropbox account not connected. Connect in Settings > Connections."); + throw new Error( + "Dropbox account not connected. Connect in Settings > Connections.", + ); } } @@ -42,11 +44,15 @@ async function dropboxRpc(endpoint: string, body?: object) { options.body = JSON.stringify(body); } - const response = await fetch(`https://api.dropboxapi.com${endpoint}`, options); + const response = await fetch( + `https://api.dropboxapi.com${endpoint}`, + options, + ); if (!response.ok) { const error = await response.json().catch(() => ({})); - const summary = error.error_summary || error.error?.[".tag"] || `HTTP ${response.status}`; + const summary = + error.error_summary || error.error?.[".tag"] || `HTTP ${response.status}`; throw new Error(`Dropbox API error: ${summary}`); } @@ -80,7 +86,8 @@ export function registerDropboxTools(server: McpServer): void { if (!active) { return jsonResponse({ connected: false, - message: "Dropbox not connected. Connect in Settings > Connections.", + message: + "Dropbox not connected. Connect in Settings > Connections.", }); } return jsonResponse({ @@ -132,11 +139,22 @@ export function registerDropboxTools(server: McpServer): void { server.registerTool( "dropbox_list_folder", { - description: "List files and folders in a Dropbox directory. Use empty string for root.", + description: + "List files and folders in a Dropbox directory. Use empty string for root.", inputSchema: { - path: z.string().describe('Folder path (empty string "" for root, or "/path/to/folder")'), + path: z + .string() + .describe( + 'Folder path (empty string "" for root, or "/path/to/folder")', + ), recursive: z.boolean().optional().describe("List recursively"), - limit: z.number().int().min(1).max(2000).optional().describe("Max entries to return"), + limit: z + .number() + .int() + .min(1) + .max(2000) + .optional() + .describe("Max entries to return"), }, }, async ({ path, recursive, limit }) => { @@ -155,14 +173,20 @@ export function registerDropboxTools(server: McpServer): void { server.registerTool( "dropbox_list_folder_continue", { - description: "Continue listing files using cursor from a previous list_folder call", + description: + "Continue listing files using cursor from a previous list_folder call", inputSchema: { - cursor: z.string().min(1).describe("Cursor from previous list_folder response"), + cursor: z + .string() + .min(1) + .describe("Cursor from previous list_folder response"), }, }, async ({ cursor }) => { try { - const data = await dropboxRpc("/2/files/list_folder/continue", { cursor }); + const data = await dropboxRpc("/2/files/list_folder/continue", { + cursor, + }); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to continue listing")); @@ -182,7 +206,8 @@ export function registerDropboxTools(server: McpServer): void { async ({ path, include_media_info }) => { try { const body: Record = { path }; - if (include_media_info !== undefined) body.include_media_info = include_media_info; + if (include_media_info !== undefined) + body.include_media_info = include_media_info; const data = await dropboxRpc("/2/files/get_metadata", body); return jsonResponse(data); } catch (error) { @@ -223,8 +248,14 @@ export function registerDropboxTools(server: McpServer): void { { description: "Create a new folder in Dropbox", inputSchema: { - path: z.string().min(1).describe("Path for the new folder (e.g. /Documents/NewFolder)"), - autorename: z.boolean().optional().describe("Auto-rename if folder exists"), + path: z + .string() + .min(1) + .describe("Path for the new folder (e.g. /Documents/NewFolder)"), + autorename: z + .boolean() + .optional() + .describe("Auto-rename if folder exists"), }, }, async ({ path, autorename }) => { @@ -245,12 +276,17 @@ export function registerDropboxTools(server: McpServer): void { description: "Upload/create a text file in Dropbox. Use for creating new files with text content.", inputSchema: { - path: z.string().min(1).describe("File path including name (e.g. /Documents/notes.txt)"), + path: z + .string() + .min(1) + .describe("File path including name (e.g. /Documents/notes.txt)"), content: z.string().describe("Text content of the file"), mode: z .enum(["add", "overwrite"]) .optional() - .describe("'add' to avoid overwriting (default), 'overwrite' to replace existing"), + .describe( + "'add' to avoid overwriting (default), 'overwrite' to replace existing", + ), autorename: z .boolean() .optional() @@ -263,19 +299,24 @@ export function registerDropboxTools(server: McpServer): void { const apiArg: Record = { path, mode: mode || "add" }; if (autorename !== undefined) apiArg.autorename = autorename; - const response = await fetch("https://content.dropboxapi.com/2/files/upload", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/octet-stream", - "Dropbox-API-Arg": JSON.stringify(apiArg), + const response = await fetch( + "https://content.dropboxapi.com/2/files/upload", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/octet-stream", + "Dropbox-API-Arg": JSON.stringify(apiArg), + }, + body: content, }, - body: content, - }); + ); if (!response.ok) { const error = await response.json().catch(() => ({})); - throw new Error(error.error_summary || `Upload failed: ${response.status}`); + throw new Error( + error.error_summary || `Upload failed: ${response.status}`, + ); } const data = await response.json(); @@ -358,7 +399,9 @@ export function registerDropboxTools(server: McpServer): void { description: "Create a shared link for a file or folder", inputSchema: { path: z.string().min(1).describe("File or folder path"), - requested_visibility: z.enum(["public", "team_only", "password"]).optional(), + requested_visibility: z + .enum(["public", "team_only", "password"]) + .optional(), }, }, async ({ path, requested_visibility }) => { @@ -367,7 +410,10 @@ export function registerDropboxTools(server: McpServer): void { if (requested_visibility) { body.settings = { requested_visibility }; } - const data = await dropboxRpc("/2/sharing/create_shared_link_with_settings", body); + const data = await dropboxRpc( + "/2/sharing/create_shared_link_with_settings", + body, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create shared link")); @@ -380,7 +426,10 @@ export function registerDropboxTools(server: McpServer): void { { description: "List shared links for a file/folder or all shared links", inputSchema: { - path: z.string().optional().describe("File/folder path (omit for all shared links)"), + path: z + .string() + .optional() + .describe("File/folder path (omit for all shared links)"), cursor: z.string().optional().describe("Cursor for pagination"), direct_only: z.boolean().optional(), }, diff --git a/app/api/mcp/tools/generation.ts b/app/api/mcp/tools/generation.ts index d8c10bc80..bf214da74 100644 --- a/app/api/mcp/tools/generation.ts +++ b/app/api/mcp/tools/generation.ts @@ -29,7 +29,9 @@ import { logger } from "@/lib/utils/logger"; import { getAuthContext } from "../lib/context"; import { errorResponse, jsonResponse } from "../lib/responses"; -async function streamToBuffer(stream: ReadableStream): Promise { +async function streamToBuffer( + stream: ReadableStream, +): Promise { const reader = stream.getReader(); const chunks: Uint8Array[] = []; @@ -64,9 +66,18 @@ export function registerGenerationTools(server: McpServer): void { description: "Generate text using AI models (GPT-4, Claude, Gemini). Deducts credits based on token usage.", inputSchema: { - prompt: z.string().min(1).max(10000).describe("The text prompt to generate from"), + prompt: z + .string() + .min(1) + .max(10000) + .describe("The text prompt to generate from"), model: z - .enum(["gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet-20241022", "gemini-2.0-flash-exp"]) + .enum([ + "gpt-4o", + "gpt-4o-mini", + "claude-3-5-sonnet-20241022", + "gemini-2.0-flash-exp", + ]) .optional() .default("gpt-4o") .describe("The AI model to use for generation"), @@ -93,13 +104,18 @@ export function registerGenerationTools(server: McpServer): void { } // Start async moderation (doesn't block) - contentModerationService.moderateInBackground(prompt, user.id, undefined, (result) => { - logger.warn("[MCP] generate_text moderation violation", { - userId: user.id, - categories: result.flaggedCategories, - action: result.action, - }); - }); + contentModerationService.moderateInBackground( + prompt, + user.id, + undefined, + (result) => { + logger.warn("[MCP] generate_text moderation violation", { + userId: user.id, + categories: result.flaggedCategories, + action: result.action, + }); + }, + ); const provider = getProviderFromModel(model); @@ -218,7 +234,8 @@ export function registerGenerationTools(server: McpServer): void { try { await generationsService.update(generationId, { status: "failed", - error: error instanceof Error ? error.message : "Generation failed", + error: + error instanceof Error ? error.message : "Generation failed", completed_at: new Date(), }); } catch (updateError) { @@ -226,7 +243,9 @@ export function registerGenerationTools(server: McpServer): void { } } - return errorResponse(error instanceof Error ? error.message : "Text generation failed"); + return errorResponse( + error instanceof Error ? error.message : "Text generation failed", + ); } }, ); @@ -235,9 +254,14 @@ export function registerGenerationTools(server: McpServer): void { server.registerTool( "generate_image", { - description: "Generate images using Google Gemini 2.5. Deducts credits per image generated.", + description: + "Generate images using Google Gemini 2.5. Deducts credits per image generated.", inputSchema: { - prompt: z.string().min(1).max(5000).describe("Description of the image to generate"), + prompt: z + .string() + .min(1) + .max(5000) + .describe("Description of the image to generate"), aspectRatio: z .enum(["1:1", "16:9", "9:16", "4:3", "3:4"]) .optional() @@ -263,13 +287,18 @@ export function registerGenerationTools(server: McpServer): void { return errorResponse("Account suspended due to policy violations"); } - contentModerationService.moderateInBackground(prompt, user.id, undefined, (result) => { - logger.warn("[MCP] generate_image moderation violation", { - userId: user.id, - categories: result.flaggedCategories, - action: result.action, - }); - }); + contentModerationService.moderateInBackground( + prompt, + user.id, + undefined, + (result) => { + logger.warn("[MCP] generate_image moderation violation", { + userId: user.id, + categories: result.flaggedCategories, + action: result.action, + }); + }, + ); try { reservation = await creditsService.reserve({ @@ -447,7 +476,8 @@ export function registerGenerationTools(server: McpServer): void { try { await generationsService.update(generationId, { status: "failed", - error: error instanceof Error ? error.message : "Generation failed", + error: + error instanceof Error ? error.message : "Generation failed", completed_at: new Date(), }); } catch (updateError) { @@ -455,7 +485,9 @@ export function registerGenerationTools(server: McpServer): void { } } - return errorResponse(error instanceof Error ? error.message : "Image generation failed"); + return errorResponse( + error instanceof Error ? error.message : "Image generation failed", + ); } }, ); @@ -467,7 +499,11 @@ export function registerGenerationTools(server: McpServer): void { description: "Generate a video using AI models. Cost: $5 per video", inputSchema: { prompt: z.string().describe("Video generation prompt"), - model: z.string().optional().default("fal-ai/veo3").describe("Model to use for generation"), + model: z + .string() + .optional() + .default("fal-ai/veo3") + .describe("Model to use for generation"), }, }, async ({ prompt, model }) => { @@ -485,7 +521,9 @@ export function registerGenerationTools(server: McpServer): void { }); } catch (error) { if (error instanceof InsufficientCreditsError) { - throw new Error(`Insufficient credits: need $${VIDEO_COST.toFixed(2)}`); + throw new Error( + `Insufficient credits: need $${VIDEO_COST.toFixed(2)}`, + ); } throw error; } @@ -516,10 +554,13 @@ export function registerGenerationTools(server: McpServer): void { jobId: generation.id, status: "pending", cost: VIDEO_COST, - message: "Video generation started. Poll /api/v1/gallery to check status.", + message: + "Video generation started. Poll /api/v1/gallery to check status.", }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to generate video"); + return errorResponse( + error instanceof Error ? error.message : "Failed to generate video", + ); } }, ); @@ -563,7 +604,11 @@ export function registerGenerationTools(server: McpServer): void { }); const audioBuffer = await streamToBuffer(audioStream); const { uploadFromBuffer } = await import("@/lib/blob"); - audioUrl = await uploadFromBuffer(audioBuffer, `tts-${Date.now()}.mp3`, "audio/mpeg"); + audioUrl = await uploadFromBuffer( + audioBuffer, + `tts-${Date.now()}.mp3`, + "audio/mpeg", + ); } catch (opError) { await reservation?.reconcile(0); throw opError; @@ -578,7 +623,9 @@ export function registerGenerationTools(server: McpServer): void { cost: TTS_COST, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to generate speech"); + return errorResponse( + error instanceof Error ? error.message : "Failed to generate speech", + ); } }, ); @@ -604,7 +651,9 @@ export function registerGenerationTools(server: McpServer): void { })), }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list voices"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list voices", + ); } }, ); @@ -656,7 +705,9 @@ export function registerGenerationTools(server: McpServer): void { return jsonResponse({ success: true, prompts, cost: COST }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to generate prompts"); + return errorResponse( + error instanceof Error ? error.message : "Failed to generate prompts", + ); } }, ); diff --git a/app/api/mcp/tools/github.ts b/app/api/mcp/tools/github.ts index 48887ffb8..c72abe4d1 100644 --- a/app/api/mcp/tools/github.ts +++ b/app/api/mcp/tools/github.ts @@ -25,7 +25,9 @@ async function getGitHubToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("GitHub account not connected. Connect in Settings > Connections."); + throw new Error( + "GitHub account not connected. Connect in Settings > Connections.", + ); } } @@ -57,7 +59,9 @@ function errMsg(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; } -function buildQuery(params: Record) { +function buildQuery( + params: Record, +) { const sp = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined) sp.set(key, String(value)); @@ -218,7 +222,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}`, { method: "DELETE" }); + const data = await githubFetch(`/repos/${owner}/${repo}`, { + method: "DELETE", + }); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to delete repo")); @@ -245,7 +251,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, ...params }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues${buildQuery(params)}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues${buildQuery(params)}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list issues")); @@ -265,7 +273,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, issue_number }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/${issue_number}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to get issue")); @@ -318,10 +328,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, issue_number, ...rest }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}`, { - method: "PATCH", - body: JSON.stringify(rest), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/${issue_number}`, + { + method: "PATCH", + body: JSON.stringify(rest), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update issue")); @@ -341,10 +354,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, issue_number }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}`, { - method: "PATCH", - body: JSON.stringify({ state: "closed" }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/${issue_number}`, + { + method: "PATCH", + body: JSON.stringify({ state: "closed" }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to close issue")); @@ -365,10 +381,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, issue_number, lock_reason }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}/lock`, { - method: "PUT", - body: lock_reason ? JSON.stringify({ lock_reason }) : undefined, - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/${issue_number}/lock`, + { + method: "PUT", + body: lock_reason ? JSON.stringify({ lock_reason }) : undefined, + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to lock issue")); @@ -413,10 +432,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, issue_number, body }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}/comments`, { - method: "POST", - body: JSON.stringify({ body }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/${issue_number}/comments`, + { + method: "POST", + body: JSON.stringify({ body }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create issue comment")); @@ -437,10 +459,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, comment_id, body }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/comments/${comment_id}`, { - method: "PATCH", - body: JSON.stringify({ body }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/comments/${comment_id}`, + { + method: "PATCH", + body: JSON.stringify({ body }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update issue comment")); @@ -460,9 +485,12 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, comment_id }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/issues/comments/${comment_id}`, { - method: "DELETE", - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/issues/comments/${comment_id}`, + { + method: "DELETE", + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to delete issue comment")); @@ -488,7 +516,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, ...params }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/pulls${buildQuery(params)}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/pulls${buildQuery(params)}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list PRs")); @@ -508,7 +538,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, pull_number }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/pulls/${pull_number}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to get PR")); @@ -560,10 +592,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, pull_number, ...rest }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}`, { - method: "PATCH", - body: JSON.stringify(rest), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/pulls/${pull_number}`, + { + method: "PATCH", + body: JSON.stringify(rest), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update PR")); @@ -584,12 +619,26 @@ export function registerGitHubTools(server: McpServer): void { merge_method: z.string().optional(), }, }, - async ({ owner, repo, pull_number, commit_title, commit_message, merge_method }) => { + async ({ + owner, + repo, + pull_number, + commit_title, + commit_message, + merge_method, + }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}/merge`, { - method: "PUT", - body: JSON.stringify({ commit_title, commit_message, merge_method }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/pulls/${pull_number}/merge`, + { + method: "PUT", + body: JSON.stringify({ + commit_title, + commit_message, + merge_method, + }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to merge PR")); @@ -636,10 +685,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, pull_number, body, event, comments }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}/reviews`, { - method: "POST", - body: JSON.stringify({ body, event, comments }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/pulls/${pull_number}/reviews`, + { + method: "POST", + body: JSON.stringify({ body, event, comments }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create PR review")); @@ -763,7 +815,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, ...params }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/milestones${buildQuery(params)}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/milestones${buildQuery(params)}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list milestones")); @@ -813,10 +867,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, milestone_number, ...rest }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/milestones/${milestone_number}`, { - method: "PATCH", - body: JSON.stringify(rest), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/milestones/${milestone_number}`, + { + method: "PATCH", + body: JSON.stringify(rest), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update milestone")); @@ -836,9 +893,12 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, milestone_number }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/milestones/${milestone_number}`, { - method: "DELETE", - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/milestones/${milestone_number}`, + { + method: "DELETE", + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to delete milestone")); @@ -857,7 +917,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ per_page, page }) => { try { - const data = await githubFetch(`/user/orgs${buildQuery({ per_page, page })}`); + const data = await githubFetch( + `/user/orgs${buildQuery({ per_page, page })}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list orgs")); @@ -895,7 +957,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ org, per_page, page }) => { try { - const data = await githubFetch(`/orgs/${org}/members${buildQuery({ per_page, page })}`); + const data = await githubFetch( + `/orgs/${org}/members${buildQuery({ per_page, page })}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list org members")); @@ -915,7 +979,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ org, per_page, page }) => { try { - const data = await githubFetch(`/orgs/${org}/teams${buildQuery({ per_page, page })}`); + const data = await githubFetch( + `/orgs/${org}/teams${buildQuery({ per_page, page })}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list teams")); @@ -1022,9 +1088,12 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, branch }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, { - method: "DELETE", - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/git/refs/heads/${branch}`, + { + method: "DELETE", + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to delete branch")); @@ -1050,7 +1119,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, ...params }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/commits${buildQuery(params)}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/commits${buildQuery(params)}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to list commits")); @@ -1070,7 +1141,9 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, ref }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/commits/${ref}`); + const data = await githubFetch( + `/repos/${owner}/${repo}/commits/${ref}`, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to get commit")); @@ -1116,13 +1189,31 @@ export function registerGitHubTools(server: McpServer): void { author: z.record(z.string(), z.any()).optional(), }, }, - async ({ owner, repo, path, message, content, branch, committer, author }) => { + async ({ + owner, + repo, + path, + message, + content, + branch, + committer, + author, + }) => { try { const encodedContent = Buffer.from(content).toString("base64"); - const data = await githubFetch(`/repos/${owner}/${repo}/contents/${path}`, { - method: "PUT", - body: JSON.stringify({ message, content: encodedContent, branch, committer, author }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/contents/${path}`, + { + method: "PUT", + body: JSON.stringify({ + message, + content: encodedContent, + branch, + committer, + author, + }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create file")); @@ -1146,20 +1237,33 @@ export function registerGitHubTools(server: McpServer): void { author: z.record(z.string(), z.any()).optional(), }, }, - async ({ owner, repo, path, message, content, sha, branch, committer, author }) => { + async ({ + owner, + repo, + path, + message, + content, + sha, + branch, + committer, + author, + }) => { try { const encodedContent = Buffer.from(content).toString("base64"); - const data = await githubFetch(`/repos/${owner}/${repo}/contents/${path}`, { - method: "PUT", - body: JSON.stringify({ - message, - content: encodedContent, - sha, - branch, - committer, - author, - }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/contents/${path}`, + { + method: "PUT", + body: JSON.stringify({ + message, + content: encodedContent, + sha, + branch, + committer, + author, + }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to update file")); @@ -1184,10 +1288,13 @@ export function registerGitHubTools(server: McpServer): void { }, async ({ owner, repo, path, message, sha, branch, committer, author }) => { try { - const data = await githubFetch(`/repos/${owner}/${repo}/contents/${path}`, { - method: "DELETE", - body: JSON.stringify({ message, sha, branch, committer, author }), - }); + const data = await githubFetch( + `/repos/${owner}/${repo}/contents/${path}`, + { + method: "DELETE", + body: JSON.stringify({ message, sha, branch, committer, author }), + }, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to delete file")); diff --git a/app/api/mcp/tools/google.ts b/app/api/mcp/tools/google.ts index 9f8cdc8cd..da72025e9 100644 --- a/app/api/mcp/tools/google.ts +++ b/app/api/mcp/tools/google.ts @@ -30,11 +30,16 @@ async function getGoogleToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Google account not connected. Connect in Settings > Connections."); + throw new Error( + "Google account not connected. Connect in Settings > Connections.", + ); } } -async function googleFetch(url: string, options: RequestInit = {}): Promise { +async function googleFetch( + url: string, + options: RequestInit = {}, +): Promise { const token = await getGoogleToken(); return googleFetchWithToken(token, url, options); } @@ -80,12 +85,20 @@ export function registerGoogleTools(server: McpServer): void { server.registerTool( "gmail_send", { - description: "Send email via Gmail. Supports plain text and HTML, with CC/BCC.", + description: + "Send email via Gmail. Supports plain text and HTML, with CC/BCC.", inputSchema: { - to: z.string().min(1).describe("Recipient(s), comma-separated email addresses"), + to: z + .string() + .min(1) + .describe("Recipient(s), comma-separated email addresses"), subject: z.string().min(1).describe("Subject line"), body: z.string().min(1).describe("Email body content"), - isHtml: z.boolean().optional().default(false).describe("Send as HTML format"), + isHtml: z + .boolean() + .optional() + .default(false) + .describe("Send as HTML format"), cc: z.string().optional().describe("CC recipients, comma-separated"), bcc: z.string().optional().describe("BCC recipients, comma-separated"), }, @@ -153,7 +166,9 @@ export function registerGoogleTools(server: McpServer): void { labelIds: z .string() .optional() - .describe("Label IDs, comma-separated (e.g., 'INBOX', 'UNREAD', 'STARRED')"), + .describe( + "Label IDs, comma-separated (e.g., 'INBOX', 'UNREAD', 'STARRED')", + ), after: z .string() .optional() @@ -226,7 +241,9 @@ export function registerGoogleTools(server: McpServer): void { }); } - const messageIds = messages.slice(0, maxResults).map((m: { id: string }) => m.id); + const messageIds = messages + .slice(0, maxResults) + .map((m: { id: string }) => m.id); const results = await Promise.all( messageIds.map(async (id: string) => { try { @@ -276,7 +293,9 @@ export function registerGoogleTools(server: McpServer): void { .enum(["full", "metadata", "minimal"]) .optional() .default("full") - .describe("Response format: full (with body), metadata (headers only), or minimal"), + .describe( + "Response format: full (with body), metadata (headers only), or minimal", + ), }, }, async ({ messageId, format = "full" }) => { @@ -301,7 +320,10 @@ export function registerGoogleTools(server: McpServer): void { } const headers = Object.fromEntries( - msg.payload.headers?.map((h: { name: string; value: string }) => [h.name, h.value]) || [], + msg.payload.headers?.map((h: { name: string; value: string }) => [ + h.name, + h.value, + ]) || [], ); return jsonResponse({ @@ -344,20 +366,34 @@ export function registerGoogleTools(server: McpServer): void { timeMax: z .string() .optional() - .describe("Only events starting before this time (ISO 8601, e.g. 2026-02-20T23:59:59Z)"), + .describe( + "Only events starting before this time (ISO 8601, e.g. 2026-02-20T23:59:59Z)", + ), calendarId: z .string() .optional() .default("primary") .describe("Calendar ID (default: 'primary')"), - query: z.string().optional().describe("Free-text search across event fields"), + query: z + .string() + .optional() + .describe("Free-text search across event fields"), pageToken: z .string() .optional() - .describe("Token from a previous response's nextPageToken to fetch the next page"), + .describe( + "Token from a previous response's nextPageToken to fetch the next page", + ), }, }, - async ({ maxResults = 10, timeMin, timeMax, calendarId = "primary", query, pageToken }) => { + async ({ + maxResults = 10, + timeMin, + timeMax, + calendarId = "primary", + query, + pageToken, + }) => { try { if (timeMin && Number.isNaN(new Date(timeMin).getTime())) { return errorResponse( @@ -410,8 +446,14 @@ export function registerGoogleTools(server: McpServer): void { "Create a new calendar event. Supports timed events, attendees, location, and notification preferences.", inputSchema: { summary: z.string().min(1).describe("Event title"), - start: z.string().min(1).describe("Start time (ISO 8601, e.g. 2026-02-20T14:00:00Z)"), - end: z.string().min(1).describe("End time (ISO 8601, e.g. 2026-02-20T15:00:00Z)"), + start: z + .string() + .min(1) + .describe("Start time (ISO 8601, e.g. 2026-02-20T14:00:00Z)"), + end: z + .string() + .min(1) + .describe("End time (ISO 8601, e.g. 2026-02-20T15:00:00Z)"), timeZone: z .string() .optional() @@ -420,7 +462,10 @@ export function registerGoogleTools(server: McpServer): void { ), description: z.string().optional().describe("Event description/notes"), location: z.string().optional().describe("Event location"), - attendees: z.array(z.string().email()).optional().describe("Attendee email addresses"), + attendees: z + .array(z.string().email()) + .optional() + .describe("Attendee email addresses"), calendarId: z .string() .optional() @@ -451,7 +496,9 @@ export function registerGoogleTools(server: McpServer): void { end: applyTimeZone(end, timeZone), ...(description && { description }), ...(location && { location }), - ...(attendees?.length && { attendees: attendees.map((email) => ({ email })) }), + ...(attendees?.length && { + attendees: attendees.map((email) => ({ email })), + }), }; const response = await googleFetch( @@ -464,7 +511,10 @@ export function registerGoogleTools(server: McpServer): void { ); const result = await response.json(); - logger.info("[GoogleMCP] Event created", { eventId: result.id, summary }); + logger.info("[GoogleMCP] Event created", { + eventId: result.id, + summary, + }); return jsonResponse({ success: true, @@ -524,7 +574,8 @@ export function registerGoogleTools(server: McpServer): void { const existingResponse = await googleFetch(baseUrl); const existing = await existingResponse.json(); - const existingTimeZone = existing?.start?.timeZone || existing?.end?.timeZone || undefined; + const existingTimeZone = + existing?.start?.timeZone || existing?.end?.timeZone || undefined; const effectiveTimeZone = timeZone || existingTimeZone; const updated = { @@ -548,11 +599,14 @@ export function registerGoogleTools(server: McpServer): void { }), }; - const response = await googleFetch(`${baseUrl}?sendUpdates=${sendUpdates}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(updated), - }); + const response = await googleFetch( + `${baseUrl}?sendUpdates=${sendUpdates}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updated), + }, + ); const result = await response.json(); logger.info("[GoogleMCP] Event updated", { eventId: result.id }); @@ -619,11 +673,15 @@ export function registerGoogleTools(server: McpServer): void { query: z .string() .optional() - .describe("Search query to find contacts by name, email, phone, or organization"), + .describe( + "Search query to find contacts by name, email, phone, or organization", + ), pageToken: z .string() .optional() - .describe("Token from a previous response's nextPageToken to fetch the next page"), + .describe( + "Token from a previous response's nextPageToken to fetch the next page", + ), }, }, async ({ pageSize = 20, query, pageToken }) => { @@ -637,7 +695,10 @@ export function registerGoogleTools(server: McpServer): void { if (query) { url = "https://people.googleapis.com/v1/people:searchContacts"; params.set("query", query); - params.set("readMask", "names,emailAddresses,phoneNumbers,organizations"); + params.set( + "readMask", + "names,emailAddresses,phoneNumbers,organizations", + ); } else if (pageToken) { params.set("pageToken", pageToken); } diff --git a/app/api/mcp/tools/hubspot.ts b/app/api/mcp/tools/hubspot.ts index 40e39fb3d..1cb1a2c45 100644 --- a/app/api/mcp/tools/hubspot.ts +++ b/app/api/mcp/tools/hubspot.ts @@ -67,18 +67,24 @@ export function registerHubSpotTools(server: McpServer): void { properties: z .array(z.string()) .optional() - .describe("Properties to include (default: firstname, lastname, email)"), + .describe( + "Properties to include (default: firstname, lastname, email)", + ), }, }, async ({ limit = 20, after, properties }) => { try { const { user } = getAuthContext(); const props = properties || DEFAULT_CONTACT_PROPERTIES; - const data = await listHubSpotObjects(user.organization_id, "contacts", { - limit, - after, - properties: props, - }); + const data = await listHubSpotObjects( + user.organization_id, + "contacts", + { + limit, + after, + properties: props, + }, + ); return jsonResponse({ success: true, @@ -98,7 +104,10 @@ export function registerHubSpotTools(server: McpServer): void { description: "Get a specific contact by ID", inputSchema: { contactId: z.string().describe("Contact ID"), - properties: z.array(z.string()).optional().describe("Properties to include"), + properties: z + .array(z.string()) + .optional() + .describe("Properties to include"), }, }, async ({ contactId, properties }) => { @@ -145,7 +154,15 @@ export function registerHubSpotTools(server: McpServer): void { .optional(), }, }, - async ({ email, firstname, lastname, phone, company, jobtitle, lifecyclestage }) => { + async ({ + email, + firstname, + lastname, + phone, + company, + jobtitle, + lifecyclestage, + }) => { try { const properties: Record = { email }; if (firstname) properties.firstname = firstname; @@ -156,7 +173,11 @@ export function registerHubSpotTools(server: McpServer): void { if (lifecyclestage) properties.lifecyclestage = lifecyclestage; const { user } = getAuthContext(); - const contact = await createHubSpotObject(user.organization_id, "contacts", properties); + const contact = await createHubSpotObject( + user.organization_id, + "contacts", + properties, + ); logger.info("[HubSpotMCP] Contact created", { contactId: contact.id, email, @@ -179,7 +200,9 @@ export function registerHubSpotTools(server: McpServer): void { description: "Update an existing contact", inputSchema: { contactId: z.string().describe("Contact ID"), - properties: z.record(z.string()).describe("Properties to update (key-value pairs)"), + properties: z + .record(z.string()) + .describe("Properties to update (key-value pairs)"), }, }, async ({ contactId, properties }) => { @@ -208,22 +231,30 @@ export function registerHubSpotTools(server: McpServer): void { { description: "Search for contacts", inputSchema: { - query: z.string().describe("Search query (searches email, firstname, lastname)"), + query: z + .string() + .describe("Search query (searches email, firstname, lastname)"), limit: z.number().int().min(1).max(100).optional().default(20), }, }, async ({ query, limit = 20 }) => { try { const { user } = getAuthContext(); - const data = await searchHubSpotObjects(user.organization_id, "contacts", { - query, - limit, - properties: DEFAULT_CONTACT_PROPERTIES, - }); + const data = await searchHubSpotObjects( + user.organization_id, + "contacts", + { + query, + limit, + properties: DEFAULT_CONTACT_PROPERTIES, + }, + ); return jsonResponse({ success: true, - contacts: data.results.map((contact) => mapHubSpotRecord(contact, false)), + contacts: data.results.map((contact) => + mapHubSpotRecord(contact, false), + ), paging: data.paging, count: data.total ?? data.count, }); @@ -248,11 +279,15 @@ export function registerHubSpotTools(server: McpServer): void { async ({ limit = 20, after, properties }) => { try { const { user } = getAuthContext(); - const data = await listHubSpotObjects(user.organization_id, "companies", { - limit, - after, - properties: properties || DEFAULT_COMPANY_PROPERTIES, - }); + const data = await listHubSpotObjects( + user.organization_id, + "companies", + { + limit, + after, + properties: properties || DEFAULT_COMPANY_PROPERTIES, + }, + ); return jsonResponse({ success: true, @@ -274,12 +309,23 @@ export function registerHubSpotTools(server: McpServer): void { name: z.string().describe("Company name (required)"), domain: z.string().optional().describe("Company website domain"), industry: z.string().optional().describe("Industry"), - numberofemployees: z.number().int().optional().describe("Number of employees"), + numberofemployees: z + .number() + .int() + .optional() + .describe("Number of employees"), annualrevenue: z.number().optional().describe("Annual revenue"), description: z.string().optional().describe("Company description"), }, }, - async ({ name, domain, industry, numberofemployees, annualrevenue, description }) => { + async ({ + name, + domain, + industry, + numberofemployees, + annualrevenue, + description, + }) => { try { const properties: Record = { name }; if (domain) properties.domain = domain; @@ -289,7 +335,11 @@ export function registerHubSpotTools(server: McpServer): void { if (description) properties.description = description; const { user } = getAuthContext(); - const company = await createHubSpotObject(user.organization_id, "companies", properties); + const company = await createHubSpotObject( + user.organization_id, + "companies", + properties, + ); logger.info("[HubSpotMCP] Company created", { companyId: company.id, name, @@ -318,15 +368,21 @@ export function registerHubSpotTools(server: McpServer): void { async ({ query, limit = 20 }) => { try { const { user } = getAuthContext(); - const data = await searchHubSpotObjects(user.organization_id, "companies", { - query, - limit, - properties: DEFAULT_COMPANY_PROPERTIES, - }); + const data = await searchHubSpotObjects( + user.organization_id, + "companies", + { + query, + limit, + properties: DEFAULT_COMPANY_PROPERTIES, + }, + ); return jsonResponse({ success: true, - companies: data.results.map((company) => mapHubSpotRecord(company, false)), + companies: data.results.map((company) => + mapHubSpotRecord(company, false), + ), paging: data.paging, count: data.total ?? data.count, }); @@ -377,12 +433,25 @@ export function registerHubSpotTools(server: McpServer): void { dealname: z.string().describe("Deal name (required)"), amount: z.number().optional().describe("Deal amount"), dealstage: z.string().optional().describe("Deal stage ID"), - pipeline: z.string().optional().describe("Pipeline ID (default: default)"), - closedate: z.string().optional().describe("Expected close date (ISO 8601)"), + pipeline: z + .string() + .optional() + .describe("Pipeline ID (default: default)"), + closedate: z + .string() + .optional() + .describe("Expected close date (ISO 8601)"), hubspot_owner_id: z.string().optional().describe("Owner ID"), }, }, - async ({ dealname, amount, dealstage, pipeline, closedate, hubspot_owner_id }) => { + async ({ + dealname, + amount, + dealstage, + pipeline, + closedate, + hubspot_owner_id, + }) => { try { const properties: Record = { dealname }; if (amount !== undefined) properties.amount = amount; @@ -392,7 +461,11 @@ export function registerHubSpotTools(server: McpServer): void { if (hubspot_owner_id) properties.hubspot_owner_id = hubspot_owner_id; const { user } = getAuthContext(); - const deal = await createHubSpotObject(user.organization_id, "deals", properties); + const deal = await createHubSpotObject( + user.organization_id, + "deals", + properties, + ); logger.info("[HubSpotMCP] Deal created", { dealId: deal.id, dealname }); return jsonResponse({ @@ -412,13 +485,20 @@ export function registerHubSpotTools(server: McpServer): void { description: "Update an existing deal", inputSchema: { dealId: z.string().describe("Deal ID"), - properties: z.record(z.union([z.string(), z.number()])).describe("Properties to update"), + properties: z + .record(z.union([z.string(), z.number()])) + .describe("Properties to update"), }, }, async ({ dealId, properties }) => { try { const { user } = getAuthContext(); - const deal = await updateHubSpotObject(user.organization_id, "deals", dealId, properties); + const deal = await updateHubSpotObject( + user.organization_id, + "deals", + dealId, + properties, + ); logger.info("[HubSpotMCP] Deal updated", { dealId }); return jsonResponse({ @@ -496,11 +576,16 @@ export function registerHubSpotTools(server: McpServer): void { server.registerTool( "hubspot_associate", { - description: "Associate two HubSpot objects (e.g., link contact to company)", + description: + "Associate two HubSpot objects (e.g., link contact to company)", inputSchema: { - fromObjectType: z.enum(["contacts", "companies", "deals"]).describe("Source object type"), + fromObjectType: z + .enum(["contacts", "companies", "deals"]) + .describe("Source object type"), fromObjectId: z.string().describe("Source object ID"), - toObjectType: z.enum(["contacts", "companies", "deals"]).describe("Target object type"), + toObjectType: z + .enum(["contacts", "companies", "deals"]) + .describe("Target object type"), toObjectId: z.string().describe("Target object ID"), }, }, diff --git a/app/api/mcp/tools/jira.ts b/app/api/mcp/tools/jira.ts index aa8081932..ad70c525b 100644 --- a/app/api/mcp/tools/jira.ts +++ b/app/api/mcp/tools/jira.ts @@ -29,7 +29,9 @@ async function getJiraToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Jira account not connected. Connect in Settings > Connections."); + throw new Error( + "Jira account not connected. Connect in Settings > Connections.", + ); } } @@ -41,11 +43,16 @@ async function getCloudId(token: string, orgId: string): Promise { const cached = cloudIdCache.get(orgId); if (cached && cached.expiresAt > Date.now()) return cached.id; - const response = await fetch("https://api.atlassian.com/oauth/token/accessible-resources", { - headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }, - }); + const response = await fetch( + "https://api.atlassian.com/oauth/token/accessible-resources", + { + headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }, + }, + ); if (!response.ok) { - throw new Error(`Failed to get Jira accessible resources: ${response.status}`); + throw new Error( + `Failed to get Jira accessible resources: ${response.status}`, + ); } const resources = await response.json(); if (!Array.isArray(resources) || resources.length === 0) { @@ -55,7 +62,10 @@ async function getCloudId(token: string, orgId: string): Promise { } const cloudId = resources[0].id; - cloudIdCache.set(orgId, { id: cloudId, expiresAt: Date.now() + CLOUD_ID_TTL_MS }); + cloudIdCache.set(orgId, { + id: cloudId, + expiresAt: Date.now() + CLOUD_ID_TTL_MS, + }); return cloudId; } @@ -80,7 +90,9 @@ async function jiraApi(method: string, path: string, body?: unknown) { if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error( - error?.errorMessages?.join("; ") || error?.message || `Jira API error: ${response.status}`, + error?.errorMessages?.join("; ") || + error?.message || + `Jira API error: ${response.status}`, ); } @@ -159,7 +171,12 @@ export function registerJiraTools(server: McpServer): void { .max(100) .optional() .describe("Max results (default 50)"), - startAt: z.number().int().min(0).optional().describe("Pagination offset"), + startAt: z + .number() + .int() + .min(0) + .optional() + .describe("Pagination offset"), fields: z.array(z.string()).optional().describe("Fields to return"), }, }, @@ -212,15 +229,26 @@ export function registerJiraTools(server: McpServer): void { issueType: z .string() .optional() - .describe("Issue type (default: Task). Common: Bug, Story, Task, Epic"), + .describe( + "Issue type (default: Task). Common: Bug, Story, Task, Epic", + ), description: z .string() .optional() .describe("Issue description (plain text, converted to ADF)"), - assigneeAccountId: z.string().optional().describe("Assignee account ID"), - priority: z.string().optional().describe("Priority name (e.g., High, Medium, Low)"), + assigneeAccountId: z + .string() + .optional() + .describe("Assignee account ID"), + priority: z + .string() + .optional() + .describe("Priority name (e.g., High, Medium, Low)"), labels: z.array(z.string()).optional().describe("Labels to add"), - parentKey: z.string().optional().describe("Parent issue key for subtasks"), + parentKey: z + .string() + .optional() + .describe("Parent issue key for subtasks"), }, }, async ({ @@ -240,7 +268,8 @@ export function registerJiraTools(server: McpServer): void { issuetype: { name: issueType || "Task" }, }; if (description) fields.description = textToAdf(description); - if (assigneeAccountId) fields.assignee = { accountId: assigneeAccountId }; + if (assigneeAccountId) + fields.assignee = { accountId: assigneeAccountId }; if (priority) fields.priority = { name: priority }; if (labels) fields.labels = labels; if (parentKey) fields.parent = { key: parentKey }; @@ -263,17 +292,31 @@ export function registerJiraTools(server: McpServer): void { .string() .optional() .describe("New description (plain text, converted to ADF)"), - assigneeAccountId: z.string().optional().describe("New assignee account ID"), + assigneeAccountId: z + .string() + .optional() + .describe("New assignee account ID"), priority: z.string().optional().describe("New priority name"), - labels: z.array(z.string()).optional().describe("New labels (replaces existing)"), + labels: z + .array(z.string()) + .optional() + .describe("New labels (replaces existing)"), }, }, - async ({ issueKey, summary, description, assigneeAccountId, priority, labels }) => { + async ({ + issueKey, + summary, + description, + assigneeAccountId, + priority, + labels, + }) => { try { const fields: Record = {}; if (summary) fields.summary = summary; if (description) fields.description = textToAdf(description); - if (assigneeAccountId) fields.assignee = { accountId: assigneeAccountId }; + if (assigneeAccountId) + fields.assignee = { accountId: assigneeAccountId }; if (priority) fields.priority = { name: priority }; if (labels) fields.labels = labels; const data = await jiraApi("PUT", `/issue/${issueKey}`, { fields }); @@ -290,7 +333,10 @@ export function registerJiraTools(server: McpServer): void { description: "Add a comment to a Jira issue", inputSchema: { issueKey: z.string().min(1).describe("Issue key (e.g., PROJ-123)"), - body: z.string().min(1).describe("Comment text (plain text, converted to ADF)"), + body: z + .string() + .min(1) + .describe("Comment text (plain text, converted to ADF)"), }, }, async ({ issueKey, body }) => { @@ -395,8 +441,14 @@ export function registerJiraTools(server: McpServer): void { "Transition a Jira issue to a new status (use jira_get_transitions to find valid transition IDs)", inputSchema: { issueKey: z.string().min(1).describe("Issue key (e.g., PROJ-123)"), - transitionId: z.string().min(1).describe("Transition ID (from jira_get_transitions)"), - comment: z.string().optional().describe("Optional comment to add with the transition"), + transitionId: z + .string() + .min(1) + .describe("Transition ID (from jira_get_transitions)"), + comment: z + .string() + .optional() + .describe("Optional comment to add with the transition"), }, }, async ({ issueKey, transitionId, comment }) => { @@ -409,7 +461,11 @@ export function registerJiraTools(server: McpServer): void { comment: [{ add: { body: textToAdf(comment) } }], }; } - const data = await jiraApi("POST", `/issue/${issueKey}/transitions`, body); + const data = await jiraApi( + "POST", + `/issue/${issueKey}/transitions`, + body, + ); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to transition issue")); @@ -423,7 +479,9 @@ export function registerJiraTools(server: McpServer): void { description: "Assign a Jira issue to a user", inputSchema: { issueKey: z.string().min(1).describe("Issue key (e.g., PROJ-123)"), - accountId: z.string().describe("Assignee account ID (empty string to unassign)"), + accountId: z + .string() + .describe("Assignee account ID (empty string to unassign)"), }, }, async ({ issueKey, accountId }) => { diff --git a/app/api/mcp/tools/knowledge.ts b/app/api/mcp/tools/knowledge.ts index c07a46288..d0b883178 100644 --- a/app/api/mcp/tools/knowledge.ts +++ b/app/api/mcp/tools/knowledge.ts @@ -14,11 +14,22 @@ export function registerKnowledgeTools(server: McpServer): void { server.registerTool( "query_knowledge", { - description: "Query the knowledge base using semantic search. Cost: varies by result count", + description: + "Query the knowledge base using semantic search. Cost: varies by result count", inputSchema: { query: z.string().describe("Search query"), - characterId: z.string().optional().describe("Filter by character/agent ID"), - limit: z.number().int().min(1).max(20).optional().default(5).describe("Max results"), + characterId: z + .string() + .optional() + .describe("Filter by character/agent ID"), + limit: z + .number() + .int() + .min(1) + .max(20) + .optional() + .default(5) + .describe("Max results"), }, }, async ({ query, characterId, limit }) => { @@ -43,7 +54,9 @@ export function registerKnowledgeTools(server: McpServer): void { count: results.length, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to query knowledge"); + return errorResponse( + error instanceof Error ? error.message : "Failed to query knowledge", + ); } }, ); @@ -53,15 +66,28 @@ export function registerKnowledgeTools(server: McpServer): void { { description: "List all generated media (images and videos). FREE tool.", inputSchema: { - type: z.enum(["image", "video"]).optional().describe("Filter by media type"), - limit: z.number().int().min(1).max(50).optional().default(20).describe("Max results"), + type: z + .enum(["image", "video"]) + .optional() + .describe("Filter by media type"), + limit: z + .number() + .int() + .min(1) + .max(50) + .optional() + .default(20) + .describe("Max results"), }, }, async ({ type, limit }) => { try { const { user } = getAuthContext(); - let generations = await generationsService.listByOrganization(user.organization_id, limit); + let generations = await generationsService.listByOrganization( + user.organization_id, + limit, + ); if (type) { generations = generations.filter((g) => g.type === type); } @@ -79,7 +105,9 @@ export function registerKnowledgeTools(server: McpServer): void { total: generations.length, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list gallery"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list gallery", + ); } }, ); diff --git a/app/api/mcp/tools/linear.ts b/app/api/mcp/tools/linear.ts index 9ea9f4342..f3558cc37 100644 --- a/app/api/mcp/tools/linear.ts +++ b/app/api/mcp/tools/linear.ts @@ -25,11 +25,16 @@ async function getLinearToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Linear account not connected. Connect in Settings > Connections."); + throw new Error( + "Linear account not connected. Connect in Settings > Connections.", + ); } } -async function linearGraphQL(query: string, variables?: Record) { +async function linearGraphQL( + query: string, + variables?: Record, +) { const token = await getLinearToken(); const response = await fetch("https://api.linear.app/graphql", { method: "POST", @@ -55,7 +60,8 @@ async function linearGraphQL(query: string, variables?: Record) } if (data?.errors?.length) { throw new Error( - data.errors.map((e: { message: string }) => e.message).join("; ") || "Linear API error", + data.errors.map((e: { message: string }) => e.message).join("; ") || + "Linear API error", ); } return data?.data; diff --git a/app/api/mcp/tools/linkedin.ts b/app/api/mcp/tools/linkedin.ts index 0b5cc5568..74a26e9ac 100644 --- a/app/api/mcp/tools/linkedin.ts +++ b/app/api/mcp/tools/linkedin.ts @@ -31,7 +31,9 @@ async function getLinkedInToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("LinkedIn account not connected. Connect in Settings > Connections."); + throw new Error( + "LinkedIn account not connected. Connect in Settings > Connections.", + ); } } @@ -53,7 +55,9 @@ async function linkedinFetch(path: string, options: RequestInit = {}) { if (!response.ok) { const error = await response.json().catch(() => ({})); const msg = - error?.message || error?.serviceErrorCode || `LinkedIn API error: ${response.status}`; + error?.message || + error?.serviceErrorCode || + `LinkedIn API error: ${response.status}`; throw new Error(msg); } @@ -70,7 +74,8 @@ async function getUserInfo() { const response = await fetch(LINKEDIN_USERINFO_URL, { headers: { Authorization: `Bearer ${token}` }, }); - if (!response.ok) throw new Error(`LinkedIn userinfo error: ${response.status}`); + if (!response.ok) + throw new Error(`LinkedIn userinfo error: ${response.status}`); return response.json(); } @@ -98,7 +103,8 @@ export function registerLinkedInTools(server: McpServer): void { if (!active) { return jsonResponse({ connected: false, - message: "LinkedIn not connected. Connect in Settings > Connections.", + message: + "LinkedIn not connected. Connect in Settings > Connections.", }); } return jsonResponse({ @@ -181,7 +187,10 @@ export function registerLinkedInTools(server: McpServer): void { const postId = data._headers?.get("x-restli-id") || "unknown"; - logger.info("[LinkedInMCP] Post created", { postId, author: authorUrn }); + logger.info("[LinkedInMCP] Post created", { + postId, + author: authorUrn, + }); return jsonResponse({ success: true, @@ -206,7 +215,9 @@ export function registerLinkedInTools(server: McpServer): void { inputSchema: { postUrn: z .string() - .describe("The post URN to delete (e.g., urn:li:share:12345 or urn:li:ugcPost:12345)"), + .describe( + "The post URN to delete (e.g., urn:li:share:12345 or urn:li:ugcPost:12345)", + ), }, }, async ({ postUrn }) => { diff --git a/app/api/mcp/tools/mcps.ts b/app/api/mcp/tools/mcps.ts index 102e0fbf3..de4d26c67 100644 --- a/app/api/mcp/tools/mcps.ts +++ b/app/api/mcp/tools/mcps.ts @@ -15,8 +15,19 @@ export function registerMcpTools(server: McpServer): void { { description: "List MCP servers. FREE tool.", inputSchema: { - scope: z.enum(["own", "public"]).optional().default("own").describe("Scope"), - limit: z.number().int().min(1).max(50).optional().default(20).describe("Max results"), + scope: z + .enum(["own", "public"]) + .optional() + .default("own") + .describe("Scope"), + limit: z + .number() + .int() + .min(1) + .max(50) + .optional() + .default(20) + .describe("Max results"), }, }, async ({ scope, limit }) => { @@ -41,7 +52,9 @@ export function registerMcpTools(server: McpServer): void { total: mcps.length, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list MCPs"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list MCPs", + ); } }, ); @@ -74,7 +87,9 @@ export function registerMcpTools(server: McpServer): void { return jsonResponse({ success: true, mcpId: mcp.id, slug: mcp.slug }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to create MCP"); + return errorResponse( + error instanceof Error ? error.message : "Failed to create MCP", + ); } }, ); @@ -93,7 +108,9 @@ export function registerMcpTools(server: McpServer): void { await userMcpsService.delete(mcpId, user.organization_id); return jsonResponse({ success: true, mcpId }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to delete MCP"); + return errorResponse( + error instanceof Error ? error.message : "Failed to delete MCP", + ); } }, ); diff --git a/app/api/mcp/tools/memory.ts b/app/api/mcp/tools/memory.ts index a5b3defd5..c008f2bf9 100644 --- a/app/api/mcp/tools/memory.ts +++ b/app/api/mcp/tools/memory.ts @@ -40,12 +40,22 @@ export function registerMemoryTools(server: McpServer): void { description: "Save important information to long-term memory with semantic tagging. Deducts 1 credit per save.", inputSchema: { - content: z.string().min(1).max(10000).describe("The memory content to save"), + content: z + .string() + .min(1) + .max(10000) + .describe("The memory content to save"), type: z .enum(["fact", "preference", "context", "document"]) .describe("Type of memory being saved"), - tags: z.array(z.string()).optional().describe("Optional tags for categorization"), - metadata: z.record(z.unknown()).optional().describe("Additional metadata"), + tags: z + .array(z.string()) + .optional() + .describe("Optional tags for categorization"), + metadata: z + .record(z.unknown()) + .optional() + .describe("Additional metadata"), ttl: z .number() .int() @@ -57,10 +67,20 @@ export function registerMemoryTools(server: McpServer): void { .optional() .default(true) .describe("Store in PostgreSQL (default: true)"), - roomId: z.string().describe("Room ID to associate memory with (required)"), + roomId: z + .string() + .describe("Room ID to associate memory with (required)"), }, }, - async ({ content, type, tags, metadata, ttl, persistent = true, roomId }: SaveMemoryArgs) => { + async ({ + content, + type, + tags, + metadata, + ttl, + persistent = true, + roomId, + }: SaveMemoryArgs) => { try { const { user } = getAuthContext(); @@ -173,7 +193,9 @@ export function registerMemoryTools(server: McpServer): void { cost: String(MEMORY_SAVE_COST), }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to save memory"); + return errorResponse( + error instanceof Error ? error.message : "Failed to save memory", + ); } }, ); @@ -186,7 +208,10 @@ export function registerMemoryTools(server: McpServer): void { "Search and retrieve memories using semantic search or filters. Deducts 0.1 credit per memory retrieved (max 5 credits).", inputSchema: { query: z.string().optional().describe("Semantic search query"), - roomId: z.string().optional().describe("Filter to specific room/conversation"), + roomId: z + .string() + .optional() + .describe("Filter to specific room/conversation"), type: z.array(z.string()).optional().describe("Filter by memory type"), tags: z.array(z.string()).optional().describe("Filter by tags"), limit: z @@ -227,11 +252,15 @@ export function registerMemoryTools(server: McpServer): void { } const sortOrder = - sortBy === "relevance" || sortBy === "recent" || sortBy === "importance" + sortBy === "relevance" || + sortBy === "recent" || + sortBy === "importance" ? sortBy : "relevance"; - let memories: Awaited>; + let memories: Awaited< + ReturnType + >; try { memories = await memoryService.retrieveMemories({ organizationId: user.organization_id, @@ -282,7 +311,9 @@ export function registerMemoryTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to retrieve memories", + error instanceof Error + ? error.message + : "Failed to retrieve memories", ); } }, @@ -292,9 +323,13 @@ export function registerMemoryTools(server: McpServer): void { server.registerTool( "delete_memory", { - description: "Remove a specific memory or bulk delete by filters. No credit cost.", + description: + "Remove a specific memory or bulk delete by filters. No credit cost.", inputSchema: { - memoryId: z.string().optional().describe("Specific memory ID to delete"), + memoryId: z + .string() + .optional() + .describe("Specific memory ID to delete"), olderThan: z .number() .int() @@ -337,7 +372,9 @@ export function registerMemoryTools(server: McpServer): void { storageFreed: result.storageFreed, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to delete memory"); + return errorResponse( + error instanceof Error ? error.message : "Failed to delete memory", + ); } }, ); @@ -390,7 +427,10 @@ export function registerMemoryTools(server: McpServer): void { let analysis; try { - analysis = await memoryService.analyzeMemoryPatterns(user.organization_id, analysisType); + analysis = await memoryService.analyzeMemoryPatterns( + user.organization_id, + analysisType, + ); } catch (opError) { await reservation?.reconcile(0); throw opError; @@ -421,7 +461,9 @@ export function registerMemoryTools(server: McpServer): void { }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to analyze memory patterns", + error instanceof Error + ? error.message + : "Failed to analyze memory patterns", ); } }, diff --git a/app/api/mcp/tools/notion.ts b/app/api/mcp/tools/notion.ts index c3d3dd9f0..fcdc2e0f2 100644 --- a/app/api/mcp/tools/notion.ts +++ b/app/api/mcp/tools/notion.ts @@ -25,7 +25,9 @@ async function getNotionToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Notion account not connected. Connect in Settings > Connections."); + throw new Error( + "Notion account not connected. Connect in Settings > Connections.", + ); } } @@ -106,7 +108,13 @@ export function registerNotionTools(server: McpServer): void { try { const data = await notionFetch("/v1/search", { method: "POST", - body: JSON.stringify({ query, filter, sort, start_cursor, page_size }), + body: JSON.stringify({ + query, + filter, + sort, + start_cursor, + page_size, + }), }); return jsonResponse(data); } catch (error) { @@ -143,7 +151,10 @@ export function registerNotionTools(server: McpServer): void { }, async ({ body }) => { try { - const data = await notionFetch("/v1/pages", { method: "POST", body: JSON.stringify(body) }); + const data = await notionFetch("/v1/pages", { + method: "POST", + body: JSON.stringify(body), + }); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to create page")); @@ -291,7 +302,9 @@ export function registerNotionTools(server: McpServer): void { }, async ({ id }) => { try { - const data = await notionFetch(`/v1/blocks/${id}`, { method: "DELETE" }); + const data = await notionFetch(`/v1/blocks/${id}`, { + method: "DELETE", + }); return jsonResponse(data); } catch (error) { return errorResponse(errMsg(error, "Failed to delete block")); diff --git a/app/api/mcp/tools/redemption.ts b/app/api/mcp/tools/redemption.ts index ea48bfbf7..43349139f 100644 --- a/app/api/mcp/tools/redemption.ts +++ b/app/api/mcp/tools/redemption.ts @@ -28,7 +28,9 @@ export function registerRedemptionTools(server: McpServer): void { userId: user.id, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to get balance"); + return errorResponse( + error instanceof Error ? error.message : "Failed to get balance", + ); } }, ); @@ -38,8 +40,15 @@ export function registerRedemptionTools(server: McpServer): void { { description: "Get token redemption quote. FREE tool.", inputSchema: { - pointsAmount: z.number().int().min(100).max(100000).describe("Points to redeem"), - network: z.enum(["ethereum", "base", "bnb", "solana"]).describe("Payout network"), + pointsAmount: z + .number() + .int() + .min(100) + .max(100000) + .describe("Points to redeem"), + network: z + .enum(["ethereum", "base", "bnb", "solana"]) + .describe("Payout network"), }, }, async ({ pointsAmount, network }) => { @@ -51,12 +60,16 @@ export function registerRedemptionTools(server: McpServer): void { user.id, ); if (!quoteResult.success) { - return errorResponse(quoteResult.error || "Failed to get redemption quote"); + return errorResponse( + quoteResult.error || "Failed to get redemption quote", + ); } return jsonResponse({ success: true, quote: quoteResult.quote }); } catch (error) { return errorResponse( - error instanceof Error ? error.message : "Failed to get redemption quote", + error instanceof Error + ? error.message + : "Failed to get redemption quote", ); } }, diff --git a/app/api/mcp/tools/rooms.ts b/app/api/mcp/tools/rooms.ts index 70d3628ee..c4104b478 100644 --- a/app/api/mcp/tools/rooms.ts +++ b/app/api/mcp/tools/rooms.ts @@ -32,7 +32,9 @@ export function registerRoomTools(server: McpServer): void { total: rooms.length, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to list rooms"); + return errorResponse( + error instanceof Error ? error.message : "Failed to list rooms", + ); } }, ); @@ -58,7 +60,8 @@ export function registerRoomTools(server: McpServer): void { const isOwner = character.user_id === user.id; const isPublic = character.is_public === true; - const claimCheck = await charactersService.isClaimableAffiliateCharacter(characterId); + const claimCheck = + await charactersService.isClaimableAffiliateCharacter(characterId); if (!isPublic && !isOwner && !claimCheck.claimable) { return errorResponse("Access denied - this character is private"); @@ -77,7 +80,9 @@ export function registerRoomTools(server: McpServer): void { characterId: room.agentId, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to create room"); + return errorResponse( + error instanceof Error ? error.message : "Failed to create room", + ); } }, ); diff --git a/app/api/mcp/tools/salesforce.ts b/app/api/mcp/tools/salesforce.ts index 998acab46..1bb416884 100644 --- a/app/api/mcp/tools/salesforce.ts +++ b/app/api/mcp/tools/salesforce.ts @@ -34,7 +34,9 @@ async function getSalesforceToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Salesforce account not connected. Connect in Settings > Connections."); + throw new Error( + "Salesforce account not connected. Connect in Settings > Connections.", + ); } } @@ -42,13 +44,19 @@ async function getSalesforceToken(): Promise { * Resolve the Salesforce instance URL for an org. * Calls the userinfo endpoint and extracts the custom_domain or profile base URL. */ -async function resolveInstanceUrl(token: string, orgId: string): Promise { +async function resolveInstanceUrl( + token: string, + orgId: string, +): Promise { const cached = instanceUrlCache.get(orgId); if (cached && cached.expiresAt > Date.now()) return cached.url; - const res = await fetch("https://login.salesforce.com/services/oauth2/userinfo", { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await fetch( + "https://login.salesforce.com/services/oauth2/userinfo", + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); if (!res.ok) { throw new Error(`Failed to resolve Salesforce instance URL: ${res.status}`); @@ -65,11 +73,16 @@ async function resolveInstanceUrl(token: string, orgId: string): Promise } if (!instanceUrl) { - throw new Error("Could not determine Salesforce instance URL from userinfo response"); + throw new Error( + "Could not determine Salesforce instance URL from userinfo response", + ); } instanceUrl = instanceUrl.replace(/\/$/, ""); - instanceUrlCache.set(orgId, { url: instanceUrl, expiresAt: Date.now() + INSTANCE_URL_TTL_MS }); + instanceUrlCache.set(orgId, { + url: instanceUrl, + expiresAt: Date.now() + INSTANCE_URL_TTL_MS, + }); return instanceUrl; } @@ -135,7 +148,8 @@ export function registerSalesforceTools(server: McpServer): void { if (!active) { return jsonResponse({ connected: false, - message: "Salesforce not connected. Connect in Settings > Connections.", + message: + "Salesforce not connected. Connect in Settings > Connections.", }); } return jsonResponse({ @@ -247,7 +261,9 @@ export function registerSalesforceTools(server: McpServer): void { }, async () => { try { - const data = await salesforceFetch(`/services/data/${SALESFORCE_API_VERSION}/sobjects`); + const data = await salesforceFetch( + `/services/data/${SALESFORCE_API_VERSION}/sobjects`, + ); const objects = data.sobjects?.map((obj: Record) => ({ name: obj.name, label: obj.label, @@ -297,7 +313,8 @@ export function registerSalesforceTools(server: McpServer): void { .filter((v: Record) => v.active) .map((v: Record) => v.value) : undefined, - referenceTo: (f.referenceTo as string[])?.length > 0 ? f.referenceTo : undefined, + referenceTo: + (f.referenceTo as string[])?.length > 0 ? f.referenceTo : undefined, })); return jsonResponse({ name: data.name, @@ -320,12 +337,20 @@ export function registerSalesforceTools(server: McpServer): void { description: "Get a single Salesforce record by its ID. Optionally specify which fields to return.", inputSchema: { - objectName: z.string().min(1).describe("API name of the SObject, e.g. Account, Contact"), - recordId: z.string().min(1).describe("The 15 or 18-character Salesforce record ID"), + objectName: z + .string() + .min(1) + .describe("API name of the SObject, e.g. Account, Contact"), + recordId: z + .string() + .min(1) + .describe("The 15 or 18-character Salesforce record ID"), fields: z .array(z.string()) .optional() - .describe("Specific fields to return. If omitted, returns all accessible fields."), + .describe( + "Specific fields to return. If omitted, returns all accessible fields.", + ), }, }, async ({ objectName, recordId, fields }) => { @@ -369,7 +394,10 @@ export function registerSalesforceTools(server: McpServer): void { body: JSON.stringify(fields), }, ); - logger.info("[SalesforceMCP] Record created", { objectName, id: data.id }); + logger.info("[SalesforceMCP] Record created", { + objectName, + id: data.id, + }); return jsonResponse({ success: data.success, id: data.id }); } catch (error) { return errorResponse(errMsg(error, "Failed to create record")); @@ -384,11 +412,16 @@ export function registerSalesforceTools(server: McpServer): void { description: "Update fields on an existing Salesforce record. Only include the fields you want to change.", inputSchema: { - objectName: z.string().min(1).describe("API name of the SObject, e.g. Account, Contact"), + objectName: z + .string() + .min(1) + .describe("API name of the SObject, e.g. Account, Contact"), recordId: z.string().min(1).describe("The record ID to update"), fields: z .record(z.any()) - .describe("Fields to update, e.g. { Industry: 'Finance', Phone: '555-1234' }"), + .describe( + "Fields to update, e.g. { Industry: 'Finance', Phone: '555-1234' }", + ), }, }, async ({ objectName, recordId, fields }) => { @@ -414,7 +447,10 @@ export function registerSalesforceTools(server: McpServer): void { { description: "Delete a Salesforce record by its ID.", inputSchema: { - objectName: z.string().min(1).describe("API name of the SObject, e.g. Account, Contact"), + objectName: z + .string() + .min(1) + .describe("API name of the SObject, e.g. Account, Contact"), recordId: z.string().min(1).describe("The record ID to delete"), }, }, @@ -436,12 +472,15 @@ export function registerSalesforceTools(server: McpServer): void { server.registerTool( "salesforce_recent_records", { - description: "Get recently viewed records for a specific Salesforce object type.", + description: + "Get recently viewed records for a specific Salesforce object type.", inputSchema: { objectName: z .string() .min(1) - .describe("API name of the SObject, e.g. Account, Contact, Opportunity"), + .describe( + "API name of the SObject, e.g. Account, Contact, Opportunity", + ), limit: z .number() .int() diff --git a/app/api/mcp/tools/twitter.ts b/app/api/mcp/tools/twitter.ts index 1c50455fe..36d20b719 100644 --- a/app/api/mcp/tools/twitter.ts +++ b/app/api/mcp/tools/twitter.ts @@ -42,7 +42,9 @@ async function getTwitterClient(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Twitter account not connected. Connect in Settings > Connections."); + throw new Error( + "Twitter account not connected. Connect in Settings > Connections.", + ); } if (!result.accessTokenSecret) { @@ -82,7 +84,10 @@ async function getAuthenticatedUserId(client: TwitterApi): Promise { const oldestKey = userIdCache.keys().next().value; if (oldestKey) userIdCache.delete(oldestKey); } - userIdCache.set(orgId, { id: me.data.id, expiry: Date.now() + USER_ID_CACHE_TTL_MS }); + userIdCache.set(orgId, { + id: me.data.id, + expiry: Date.now() + USER_ID_CACHE_TTL_MS, + }); return me.data.id; } @@ -104,7 +109,10 @@ function errMsg(error: unknown, fallback: string): string { return parts.join(" — "); } -async function resolveUserIdFromUsername(client: TwitterApi, username: string): Promise { +async function resolveUserIdFromUsername( + client: TwitterApi, + username: string, +): Promise { const cleaned = username.replace(/^@/, ""); const user = await client.v2.userByUsername(cleaned); if (!user.data) throw new Error(`User @${cleaned} not found`); @@ -128,13 +136,25 @@ async function resolveTargetUserId( } function extractTweetIdFromUrl(url: string): string | null { - const match = url.match(/(?:(?:mobile\.)?twitter\.com|x\.com)\/\w+\/status\/(\d+)/); + const match = url.match( + /(?:(?:mobile\.)?twitter\.com|x\.com)\/\w+\/status\/(\d+)/, + ); return match ? match[1] : null; } // ── Shared field selections ────────────────────────────────────────────────── -const TIMELINE_TWEET_FIELDS = ["created_at", "public_metrics", "entities", "referenced_tweets"]; -const SEARCH_TWEET_FIELDS = ["created_at", "public_metrics", "author_id", "entities"]; +const TIMELINE_TWEET_FIELDS = [ + "created_at", + "public_metrics", + "entities", + "referenced_tweets", +]; +const SEARCH_TWEET_FIELDS = [ + "created_at", + "public_metrics", + "author_id", + "entities", +]; const MENTION_TWEET_FIELDS = [...SEARCH_TWEET_FIELDS, "referenced_tweets"]; const DETAIL_TWEET_FIELDS = [ "created_at", @@ -154,7 +174,12 @@ const USER_PROFILE_FIELDS = [ "url", "verified", ]; -const USER_SUMMARY_FIELDS = ["description", "public_metrics", "profile_image_url", "verified"]; +const USER_SUMMARY_FIELDS = [ + "description", + "public_metrics", + "profile_image_url", + "verified", +]; // ── Shared mappers ─────────────────────────────────────────────────────────── type TweetSummaryRecord = { @@ -269,7 +294,11 @@ async function fetchUserTimeline( if (paginationToken) opts.pagination_token = paginationToken; const timeline = await client.v2.userTimeline(userId, opts); - return paginatedTweetResponse(timeline.data?.data || [], timeline.data?.meta, { userId }); + return paginatedTweetResponse( + timeline.data?.data || [], + timeline.data?.meta, + { userId }, + ); } async function fetchTweetDetails(client: TwitterApi, tweetId: string) { @@ -311,7 +340,8 @@ export function registerTwitterTools(server: McpServer): void { if (!active) { return jsonResponse({ connected: false, - message: "Twitter not connected. Connect in Settings > Connections.", + message: + "Twitter not connected. Connect in Settings > Connections.", }); } return jsonResponse({ @@ -350,7 +380,9 @@ export function registerTwitterTools(server: McpServer): void { { description: "Get a Twitter/X user's profile by their username (handle)", inputSchema: { - username: z.string().describe("The Twitter username/handle (without @)"), + username: z + .string() + .describe("The Twitter username/handle (without @)"), }, }, async ({ username }) => { @@ -371,9 +403,14 @@ export function registerTwitterTools(server: McpServer): void { server.registerTool( "twitter_create_tweet", { - description: "Post a new tweet on Twitter/X. Supports text tweets and replies.", + description: + "Post a new tweet on Twitter/X. Supports text tweets and replies.", inputSchema: { - text: z.string().min(1).max(280).describe("The tweet text content (max 280 characters)"), + text: z + .string() + .min(1) + .max(280) + .describe("The tweet text content (max 280 characters)"), replyToTweetId: z .string() .regex(/^\d+$/) @@ -417,9 +454,13 @@ export function registerTwitterTools(server: McpServer): void { server.registerTool( "twitter_delete_tweet", { - description: "Delete a tweet by its ID. Only works for tweets by the authenticated user.", + description: + "Delete a tweet by its ID. Only works for tweets by the authenticated user.", inputSchema: { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to delete"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to delete"), }, }, async ({ tweetId }) => { @@ -427,7 +468,11 @@ export function registerTwitterTools(server: McpServer): void { const client = await getTwitterClient(); const result = await client.v2.deleteTweet(tweetId); logger.warn("[TwitterMCP] Tweet deleted", { tweetId }); - return jsonResponse({ success: true, deleted: result.data.deleted, tweetId }); + return jsonResponse({ + success: true, + deleted: result.data.deleted, + tweetId, + }); } catch (error) { return errorResponse(errMsg(error, "Failed to delete tweet")); } @@ -440,7 +485,10 @@ export function registerTwitterTools(server: McpServer): void { { description: "Get a specific tweet by its ID with full details", inputSchema: { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to retrieve"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to retrieve"), }, }, async ({ tweetId }) => { @@ -474,8 +522,13 @@ export function registerTwitterTools(server: McpServer): void { startTime: z .string() .optional() - .describe("Only tweets after this date (ISO 8601, must be within last 7 days)"), - endTime: z.string().optional().describe("Only tweets before this date (ISO 8601)"), + .describe( + "Only tweets after this date (ISO 8601, must be within last 7 days)", + ), + endTime: z + .string() + .optional() + .describe("Only tweets before this date (ISO 8601)"), sortOrder: z .enum(["recency", "relevancy"]) .optional() @@ -483,10 +536,19 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, - async ({ query, maxResults = 10, startTime, endTime, sortOrder, paginationToken }) => { + async ({ + query, + maxResults = 10, + startTime, + endTime, + sortOrder, + paginationToken, + }) => { try { const client = await getTwitterClient(); const opts: Record = { @@ -520,7 +582,10 @@ export function registerTwitterTools(server: McpServer): void { description: "Get tweets posted by a specific user. Supports date filtering, pagination, and excluding retweets/replies. For the authenticated user's own tweets, prefer twitter_get_my_tweets.", inputSchema: { - userId: z.string().regex(/^\d+$/).describe("The Twitter user ID (numeric)"), + userId: z + .string() + .regex(/^\d+$/) + .describe("The Twitter user ID (numeric)"), maxResults: z .number() .min(5) @@ -530,11 +595,15 @@ export function registerTwitterTools(server: McpServer): void { startTime: z .string() .optional() - .describe("Only tweets after this date (ISO 8601, e.g. 2026-02-01T00:00:00Z)"), + .describe( + "Only tweets after this date (ISO 8601, e.g. 2026-02-01T00:00:00Z)", + ), endTime: z .string() .optional() - .describe("Only tweets before this date (ISO 8601, e.g. 2026-02-28T23:59:59Z)"), + .describe( + "Only tweets before this date (ISO 8601, e.g. 2026-02-28T23:59:59Z)", + ), exclude: z .array(z.enum(["retweets", "replies"])) .optional() @@ -542,10 +611,19 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, - async ({ userId, maxResults, startTime, endTime, exclude, paginationToken }) => { + async ({ + userId, + maxResults, + startTime, + endTime, + exclude, + paginationToken, + }) => { try { const client = await getTwitterClient(); return jsonResponse( @@ -579,11 +657,15 @@ export function registerTwitterTools(server: McpServer): void { startTime: z .string() .optional() - .describe("Only tweets after this date (ISO 8601, e.g. 2026-02-01T00:00:00Z)"), + .describe( + "Only tweets after this date (ISO 8601, e.g. 2026-02-01T00:00:00Z)", + ), endTime: z .string() .optional() - .describe("Only tweets before this date (ISO 8601, e.g. 2026-02-28T23:59:59Z)"), + .describe( + "Only tweets before this date (ISO 8601, e.g. 2026-02-28T23:59:59Z)", + ), exclude: z .array(z.enum(["retweets", "replies"])) .optional() @@ -591,7 +673,9 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, async ({ maxResults, startTime, endTime, exclude, paginationToken }) => { @@ -619,7 +703,10 @@ export function registerTwitterTools(server: McpServer): void { { description: "Like a tweet on behalf of the authenticated user", inputSchema: { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to like"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to like"), }, }, async ({ tweetId }) => { @@ -628,7 +715,11 @@ export function registerTwitterTools(server: McpServer): void { const userId = await getAuthenticatedUserId(client); const result = await client.v2.like(userId, tweetId); logger.info("[TwitterMCP] Tweet liked", { tweetId }); - return jsonResponse({ success: true, liked: result.data.liked, tweetId }); + return jsonResponse({ + success: true, + liked: result.data.liked, + tweetId, + }); } catch (error) { return errorResponse(errMsg(error, "Failed to like tweet")); } @@ -641,7 +732,10 @@ export function registerTwitterTools(server: McpServer): void { { description: "Remove a like from a tweet", inputSchema: { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to unlike"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to unlike"), }, }, async ({ tweetId }) => { @@ -650,7 +744,11 @@ export function registerTwitterTools(server: McpServer): void { const userId = await getAuthenticatedUserId(client); const result = await client.v2.unlike(userId, tweetId); logger.info("[TwitterMCP] Tweet unliked", { tweetId }); - return jsonResponse({ success: true, liked: result.data.liked, tweetId }); + return jsonResponse({ + success: true, + liked: result.data.liked, + tweetId, + }); } catch (error) { return errorResponse(errMsg(error, "Failed to unlike tweet")); } @@ -663,7 +761,10 @@ export function registerTwitterTools(server: McpServer): void { { description: "Retweet a tweet on behalf of the authenticated user", inputSchema: { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to retweet"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to retweet"), }, }, async ({ tweetId }) => { @@ -672,7 +773,11 @@ export function registerTwitterTools(server: McpServer): void { const userId = await getAuthenticatedUserId(client); const result = await client.v2.retweet(userId, tweetId); logger.info("[TwitterMCP] Retweeted", { tweetId }); - return jsonResponse({ success: true, retweeted: result.data.retweeted, tweetId }); + return jsonResponse({ + success: true, + retweeted: result.data.retweeted, + tweetId, + }); } catch (error) { return errorResponse(errMsg(error, "Failed to retweet")); } @@ -685,7 +790,10 @@ export function registerTwitterTools(server: McpServer): void { { description: "Remove a retweet from a tweet", inputSchema: { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to unretweet"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to unretweet"), }, }, async ({ tweetId }) => { @@ -694,7 +802,11 @@ export function registerTwitterTools(server: McpServer): void { const userId = await getAuthenticatedUserId(client); const result = await client.v2.unretweet(userId, tweetId); logger.info("[TwitterMCP] Unretweeted", { tweetId }); - return jsonResponse({ success: true, retweeted: result.data.retweeted, tweetId }); + return jsonResponse({ + success: true, + retweeted: result.data.retweeted, + tweetId, + }); } catch (error) { return errorResponse(errMsg(error, "Failed to unretweet")); } @@ -708,7 +820,10 @@ export function registerTwitterTools(server: McpServer): void { description: "Get a list of users who follow the specified user. Supports pagination to fetch all followers.", inputSchema: { - userId: z.string().regex(/^\d+$/).describe("The Twitter user ID to get followers for"), + userId: z + .string() + .regex(/^\d+$/) + .describe("The Twitter user ID to get followers for"), maxResults: z .number() .min(1) @@ -718,7 +833,9 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, async ({ userId, maxResults = 20, paginationToken }) => { @@ -751,7 +868,10 @@ export function registerTwitterTools(server: McpServer): void { description: "Get a list of users that the specified user is following. Supports pagination to fetch all.", inputSchema: { - userId: z.string().regex(/^\d+$/).describe("The Twitter user ID to get following list for"), + userId: z + .string() + .regex(/^\d+$/) + .describe("The Twitter user ID to get following list for"), maxResults: z .number() .min(1) @@ -761,7 +881,9 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, async ({ userId, maxResults = 20, paginationToken }) => { @@ -862,7 +984,9 @@ export function registerTwitterTools(server: McpServer): void { getAuthenticatedUserId(client), ]); const result = await client.v2.unfollow(userId, resolvedId); - logger.warn("[TwitterMCP] Unfollowed user", { targetUserId: resolvedId }); + logger.warn("[TwitterMCP] Unfollowed user", { + targetUserId: resolvedId, + }); return jsonResponse({ success: true, following: result.data.following, @@ -887,12 +1011,20 @@ export function registerTwitterTools(server: McpServer): void { .max(100) .optional() .describe("Number of mentions per page (5-100, default 10)"), - startTime: z.string().optional().describe("Only mentions after this date (ISO 8601)"), - endTime: z.string().optional().describe("Only mentions before this date (ISO 8601)"), + startTime: z + .string() + .optional() + .describe("Only mentions after this date (ISO 8601)"), + endTime: z + .string() + .optional() + .describe("Only mentions before this date (ISO 8601)"), paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, async ({ maxResults = 10, startTime, endTime, paginationToken }) => { @@ -912,10 +1044,14 @@ export function registerTwitterTools(server: McpServer): void { const mentions = await client.v2.userMentionTimeline(userId, opts); return jsonResponse( - paginatedTweetResponse(mentions.data?.data || [], mentions.data?.meta, { - userId, - includes: mentions.data?.includes, - }), + paginatedTweetResponse( + mentions.data?.data || [], + mentions.data?.meta, + { + userId, + includes: mentions.data?.includes, + }, + ), ); } catch (error) { return errorResponse(errMsg(error, "Failed to get mentions")); @@ -939,7 +1075,9 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, async ({ maxResults = 10, paginationToken }) => { @@ -983,7 +1121,9 @@ export function registerTwitterTools(server: McpServer): void { paginationToken: z .string() .optional() - .describe("Token from a previous response's nextToken to fetch the next page"), + .describe( + "Token from a previous response's nextToken to fetch the next page", + ), }, }, async ({ maxResults = 10, paginationToken }) => { @@ -1000,13 +1140,20 @@ export function registerTwitterTools(server: McpServer): void { const bookmarks = await client.v2.bookmarks(opts); return jsonResponse( - paginatedTweetResponse(bookmarks.data?.data || [], bookmarks.data?.meta, { - includes: bookmarks.data?.includes, - }), + paginatedTweetResponse( + bookmarks.data?.data || [], + bookmarks.data?.meta, + { + includes: bookmarks.data?.includes, + }, + ), ); } catch (error) { return errorResponse( - errMsg(error, "Failed to get bookmarks — requires OAuth 2.0 user context"), + errMsg( + error, + "Failed to get bookmarks — requires OAuth 2.0 user context", + ), ); } }, @@ -1023,7 +1170,9 @@ export function registerTwitterTools(server: McpServer): void { .array(z.string().min(1).max(280)) .min(2) .max(25) - .describe("Array of tweet texts in thread order (2-25 tweets, each max 280 chars)"), + .describe( + "Array of tweet texts in thread order (2-25 tweets, each max 280 chars)", + ), }, }, async ({ tweets }) => { @@ -1043,13 +1192,18 @@ export function registerTwitterTools(server: McpServer): void { posted.push({ id: tweet.data.id, text: tweet.data.text }); lastTweetId = tweet.data.id; } catch (error) { - const threadUrl = posted[0] ? `https://x.com/i/status/${posted[0].id}` : null; + const threadUrl = posted[0] + ? `https://x.com/i/status/${posted[0].id}` + : null; logger.error("[TwitterMCP] Thread partially failed", { posted: posted.length, total: tweets.length, }); return errorResponse( - errMsg(error, `Thread failed at tweet ${posted.length + 1} of ${tweets.length}`), + errMsg( + error, + `Thread failed at tweet ${posted.length + 1} of ${tweets.length}`, + ), { partialThread: posted, threadUrl, @@ -1084,7 +1238,10 @@ export function registerTwitterTools(server: McpServer): void { description: "Get full tweet details from a Twitter/X URL (e.g. https://x.com/user/status/123). Use when user pastes a tweet link and wants to interact with it.", inputSchema: { - url: z.string().min(1).describe("The tweet URL from twitter.com or x.com"), + url: z + .string() + .min(1) + .describe("The tweet URL from twitter.com or x.com"), }, }, async ({ url }) => { @@ -1113,8 +1270,12 @@ export function registerTwitterTools(server: McpServer): void { description: "Check the follow relationship between two Twitter users. Use when user asks 'does X follow me', 'do I follow X', or 'are we mutuals'. Provide usernames (handles).", inputSchema: { - sourceUsername: z.string().describe("First username/handle (without @)"), - targetUsername: z.string().describe("Second username/handle (without @)"), + sourceUsername: z + .string() + .describe("First username/handle (without @)"), + targetUsername: z + .string() + .describe("Second username/handle (without @)"), }, }, async ({ sourceUsername, targetUsername }) => { @@ -1135,7 +1296,8 @@ export function registerTwitterTools(server: McpServer): void { targetUsername: cleanTarget, sourceFollowsTarget: relationship.source.following, targetFollowsSource: relationship.source.followed_by, - mutualFollow: relationship.source.following && relationship.source.followed_by, + mutualFollow: + relationship.source.following && relationship.source.followed_by, }); } catch (error) { return errorResponse(errMsg(error, "Failed to check relationship")); diff --git a/app/api/mcp/tools/user.ts b/app/api/mcp/tools/user.ts index 53c81e63c..af91878cc 100644 --- a/app/api/mcp/tools/user.ts +++ b/app/api/mcp/tools/user.ts @@ -31,7 +31,9 @@ export function registerUserTools(server: McpServer): void { }, }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to get user profile"); + return errorResponse( + error instanceof Error ? error.message : "Failed to get user profile", + ); } }, ); @@ -41,7 +43,12 @@ export function registerUserTools(server: McpServer): void { { description: "Update user profile. FREE tool.", inputSchema: { - name: z.string().min(1).max(100).optional().describe("New display name"), + name: z + .string() + .min(1) + .max(100) + .optional() + .describe("New display name"), }, }, async ({ name }) => { @@ -52,7 +59,9 @@ export function registerUserTools(server: McpServer): void { } return jsonResponse({ success: true }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : "Failed to update profile"); + return errorResponse( + error instanceof Error ? error.message : "Failed to update profile", + ); } }, ); diff --git a/app/api/mcp/tools/zoom.ts b/app/api/mcp/tools/zoom.ts index 75854e0c5..03b0836bb 100644 --- a/app/api/mcp/tools/zoom.ts +++ b/app/api/mcp/tools/zoom.ts @@ -27,7 +27,9 @@ async function getZoomToken(): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Zoom account not connected. Connect in Settings > Connections."); + throw new Error( + "Zoom account not connected. Connect in Settings > Connections.", + ); } } @@ -132,7 +134,13 @@ export function registerZoomTools(server: McpServer): void { "List meetings for the current Zoom user. Returns upcoming, past, or all meetings depending on type parameter.", inputSchema: { type: z - .enum(["scheduled", "live", "upcoming", "upcoming_meetings", "previous_meetings"]) + .enum([ + "scheduled", + "live", + "upcoming", + "upcoming_meetings", + "previous_meetings", + ]) .optional() .describe( "Meeting type filter. Default: 'scheduled'. Use 'upcoming' for future meetings, 'previous_meetings' for past ones.", @@ -152,7 +160,10 @@ export function registerZoomTools(server: McpServer): void { }, async ({ type = "scheduled", page_size = 30, next_page_token }) => { try { - const params = new URLSearchParams({ type, page_size: String(page_size) }); + const params = new URLSearchParams({ + type, + page_size: String(page_size), + }); if (next_page_token) params.set("next_page_token", next_page_token); const data = await zoomFetch(`/users/me/meetings?${params}`); @@ -184,7 +195,9 @@ export function registerZoomTools(server: McpServer): void { description: "Get detailed information about a specific Zoom meeting including settings, recurrence, and join URL", inputSchema: { - meetingId: z.union([z.string(), z.number()]).describe("The meeting ID to retrieve"), + meetingId: z + .union([z.string(), z.number()]) + .describe("The meeting ID to retrieve"), }, }, async ({ meetingId }) => { @@ -243,17 +256,42 @@ export function registerZoomTools(server: McpServer): void { .describe( "Meeting start time in ISO 8601 format (e.g. 2024-01-15T10:00:00Z). Required for scheduled meetings.", ), - duration: z.number().int().optional().describe("Meeting duration in minutes"), - timezone: z.string().optional().describe("Timezone (e.g. America/New_York, UTC)"), + duration: z + .number() + .int() + .optional() + .describe("Meeting duration in minutes"), + timezone: z + .string() + .optional() + .describe("Timezone (e.g. America/New_York, UTC)"), agenda: z.string().optional().describe("Meeting description/agenda"), - password: z.string().optional().describe("Meeting password (max 10 chars)"), + password: z + .string() + .optional() + .describe("Meeting password (max 10 chars)"), settings: z .object({ - host_video: z.boolean().optional().describe("Start with host video on"), - participant_video: z.boolean().optional().describe("Start with participant video on"), - join_before_host: z.boolean().optional().describe("Allow joining before host"), - mute_upon_entry: z.boolean().optional().describe("Mute participants on entry"), - waiting_room: z.boolean().optional().describe("Enable waiting room"), + host_video: z + .boolean() + .optional() + .describe("Start with host video on"), + participant_video: z + .boolean() + .optional() + .describe("Start with participant video on"), + join_before_host: z + .boolean() + .optional() + .describe("Allow joining before host"), + mute_upon_entry: z + .boolean() + .optional() + .describe("Mute participants on entry"), + waiting_room: z + .boolean() + .optional() + .describe("Enable waiting room"), auto_recording: z .enum(["local", "cloud", "none"]) .optional() @@ -263,7 +301,16 @@ export function registerZoomTools(server: McpServer): void { .describe("Meeting settings"), }, }, - async ({ topic, type = 2, start_time, duration, timezone, agenda, password, settings }) => { + async ({ + topic, + type = 2, + start_time, + duration, + timezone, + agenda, + password, + settings, + }) => { try { const body: Record = { topic, type }; if (start_time) body.start_time = start_time; @@ -301,12 +348,22 @@ export function registerZoomTools(server: McpServer): void { server.registerTool( "zoom_update_meeting", { - description: "Update an existing Zoom meeting's details, time, or settings", + description: + "Update an existing Zoom meeting's details, time, or settings", inputSchema: { - meetingId: z.union([z.string(), z.number()]).describe("The meeting ID to update"), + meetingId: z + .union([z.string(), z.number()]) + .describe("The meeting ID to update"), topic: z.string().optional().describe("New meeting topic"), - start_time: z.string().optional().describe("New start time in ISO 8601 format"), - duration: z.number().int().optional().describe("New duration in minutes"), + start_time: z + .string() + .optional() + .describe("New start time in ISO 8601 format"), + duration: z + .number() + .int() + .optional() + .describe("New duration in minutes"), timezone: z.string().optional().describe("New timezone"), agenda: z.string().optional().describe("New agenda/description"), password: z.string().optional().describe("New password"), @@ -323,7 +380,16 @@ export function registerZoomTools(server: McpServer): void { .describe("Updated meeting settings"), }, }, - async ({ meetingId, topic, start_time, duration, timezone, agenda, password, settings }) => { + async ({ + meetingId, + topic, + start_time, + duration, + timezone, + agenda, + password, + settings, + }) => { try { const body: Record = {}; if (topic !== undefined) body.topic = topic; @@ -352,7 +418,9 @@ export function registerZoomTools(server: McpServer): void { { description: "Delete a Zoom meeting permanently", inputSchema: { - meetingId: z.union([z.string(), z.number()]).describe("The meeting ID to delete"), + meetingId: z + .union([z.string(), z.number()]) + .describe("The meeting ID to delete"), }, }, async ({ meetingId }) => { diff --git a/app/api/mcps/airtable/[transport]/route.ts b/app/api/mcps/airtable/[transport]/route.ts index c28bef2dc..f2f4f61aa 100644 --- a/app/api/mcps/airtable/[transport]/route.ts +++ b/app/api/mcps/airtable/[transport]/route.ts @@ -48,7 +48,11 @@ async function getAirtableMcpHandler() { return result.accessToken; } - async function airtableFetch(orgId: string, endpoint: string, options: RequestInit = {}) { + async function airtableFetch( + orgId: string, + endpoint: string, + options: RequestInit = {}, + ) { const token = await getAirtableToken(orgId); const response = await fetch(`https://api.airtable.com${endpoint}`, { ...options, @@ -62,7 +66,9 @@ async function getAirtableMcpHandler() { if (!response.ok) { const error = await response.json().catch(() => ({})); const msg = - error?.error?.message || error?.message || `Airtable API error: ${response.status}`; + error?.error?.message || + error?.message || + `Airtable API error: ${response.status}`; throw new Error(msg); } @@ -90,7 +96,9 @@ async function getAirtableMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -98,27 +106,41 @@ async function getAirtableMcpHandler() { mcpHandler = createMcpHandler( (server) => { // --- Connection status --- - server.tool("airtable_status", "Check Airtable OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "airtable", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) return jsonResult({ connected: false }); - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "airtable_status", + "Check Airtable OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "airtable", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); // --- List bases --- server.tool( "airtable_list_bases", "List all Airtable bases accessible to the connected account", - { offset: z.string().optional().describe("Pagination cursor from a previous response") }, + { + offset: z + .string() + .optional() + .describe("Pagination cursor from a previous response"), + }, async ({ offset }) => { try { const orgId = getOrgId(); @@ -128,7 +150,9 @@ async function getAirtableMcpHandler() { const data = await airtableFetch(orgId, `/v0/meta/bases${suffix}`); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to list bases"); + return errorResult( + e instanceof Error ? e.message : "Failed to list bases", + ); } }, ); @@ -137,14 +161,21 @@ async function getAirtableMcpHandler() { server.tool( "airtable_get_base_schema", "Get the schema of an Airtable base including all tables and their fields", - { baseId: z.string().min(1).describe("The base ID (starts with 'app')") }, + { + baseId: z.string().min(1).describe("The base ID (starts with 'app')"), + }, async ({ baseId }) => { try { const orgId = getOrgId(); - const data = await airtableFetch(orgId, `/v0/meta/bases/${baseId}/tables`); + const data = await airtableFetch( + orgId, + `/v0/meta/bases/${baseId}/tables`, + ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get base schema"); + return errorResult( + e instanceof Error ? e.message : "Failed to get base schema", + ); } }, ); @@ -155,12 +186,20 @@ async function getAirtableMcpHandler() { "List records from an Airtable table with optional filtering, sorting, and field selection", { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), - tableIdOrName: z.string().min(1).describe("Table ID (starts with 'tbl') or table name"), - fields: z.array(z.string()).optional().describe("Only return these fields"), + tableIdOrName: z + .string() + .min(1) + .describe("Table ID (starts with 'tbl') or table name"), + fields: z + .array(z.string()) + .optional() + .describe("Only return these fields"), filterByFormula: z .string() .optional() - .describe("Airtable formula to filter records, e.g. {Status}='Done'"), + .describe( + "Airtable formula to filter records, e.g. {Status}='Done'", + ), maxRecords: z .number() .int() @@ -174,12 +213,23 @@ async function getAirtableMcpHandler() { .max(100) .optional() .describe("Records per page (max 100)"), - offset: z.string().optional().describe("Pagination cursor from a previous response"), + offset: z + .string() + .optional() + .describe("Pagination cursor from a previous response"), sort: z - .array(z.object({ field: z.string(), direction: z.enum(["asc", "desc"]).optional() })) + .array( + z.object({ + field: z.string(), + direction: z.enum(["asc", "desc"]).optional(), + }), + ) .optional() .describe("Sort configuration"), - view: z.string().optional().describe("View name or ID to use for filtering/sorting"), + view: z + .string() + .optional() + .describe("View name or ID to use for filtering/sorting"), }, async ({ baseId, @@ -204,7 +254,8 @@ async function getAirtableMcpHandler() { if (sort) { sort.forEach((s, i) => { params.set(`sort[${i}][field]`, s.field); - if (s.direction) params.set(`sort[${i}][direction]`, s.direction); + if (s.direction) + params.set(`sort[${i}][direction]`, s.direction); }); } const suffix = params.toString() ? `?${params.toString()}` : ""; @@ -214,7 +265,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to list records"); + return errorResult( + e instanceof Error ? e.message : "Failed to list records", + ); } }, ); @@ -226,7 +279,10 @@ async function getAirtableMcpHandler() { { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), tableIdOrName: z.string().min(1).describe("Table ID or table name"), - recordId: z.string().min(1).describe("The record ID (starts with 'rec')"), + recordId: z + .string() + .min(1) + .describe("The record ID (starts with 'rec')"), }, async ({ baseId, tableIdOrName, recordId }) => { try { @@ -237,7 +293,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get record"); + return errorResult( + e instanceof Error ? e.message : "Failed to get record", + ); } }, ); @@ -257,7 +315,9 @@ async function getAirtableMcpHandler() { typecast: z .boolean() .optional() - .describe("If true, auto-convert string values to appropriate types"), + .describe( + "If true, auto-convert string values to appropriate types", + ), }, async ({ baseId, tableIdOrName, records, typecast }) => { try { @@ -274,7 +334,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to create records"); + return errorResult( + e instanceof Error ? e.message : "Failed to create records", + ); } }, ); @@ -287,14 +349,18 @@ async function getAirtableMcpHandler() { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), tableIdOrName: z.string().min(1).describe("Table ID or table name"), records: z - .array(z.object({ id: z.string().min(1), fields: z.record(z.any()) })) + .array( + z.object({ id: z.string().min(1), fields: z.record(z.any()) }), + ) .min(1) .max(10) .describe("Array of records to update, each with id and fields"), typecast: z .boolean() .optional() - .describe("If true, auto-convert string values to appropriate types"), + .describe( + "If true, auto-convert string values to appropriate types", + ), }, async ({ baseId, tableIdOrName, records, typecast }) => { try { @@ -311,7 +377,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to update records"); + return errorResult( + e instanceof Error ? e.message : "Failed to update records", + ); } }, ); @@ -343,7 +411,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to delete records"); + return errorResult( + e instanceof Error ? e.message : "Failed to delete records", + ); } }, ); @@ -361,14 +431,34 @@ async function getAirtableMcpHandler() { .describe( "Airtable formula, e.g. SEARCH('term',{Name}), AND({Status}='Active',{Priority}='High')", ), - fields: z.array(z.string()).optional().describe("Only return these fields"), - maxRecords: z.number().int().min(1).optional().describe("Maximum records to return"), + fields: z + .array(z.string()) + .optional() + .describe("Only return these fields"), + maxRecords: z + .number() + .int() + .min(1) + .optional() + .describe("Maximum records to return"), sort: z - .array(z.object({ field: z.string(), direction: z.enum(["asc", "desc"]).optional() })) + .array( + z.object({ + field: z.string(), + direction: z.enum(["asc", "desc"]).optional(), + }), + ) .optional() .describe("Sort configuration"), }, - async ({ baseId, tableIdOrName, formula, fields, maxRecords, sort }) => { + async ({ + baseId, + tableIdOrName, + formula, + fields, + maxRecords, + sort, + }) => { try { const orgId = getOrgId(); const params = new URLSearchParams(); @@ -378,7 +468,8 @@ async function getAirtableMcpHandler() { if (sort) { sort.forEach((s, i) => { params.set(`sort[${i}][field]`, s.field); - if (s.direction) params.set(`sort[${i}][direction]`, s.direction); + if (s.direction) + params.set(`sort[${i}][direction]`, s.direction); }); } const data = await airtableFetch( @@ -387,7 +478,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to search records"); + return errorResult( + e instanceof Error ? e.message : "Failed to search records", + ); } }, ); @@ -399,7 +492,10 @@ async function getAirtableMcpHandler() { { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), name: z.string().min(1).describe("Name for the new table"), - description: z.string().optional().describe("Optional table description"), + description: z + .string() + .optional() + .describe("Optional table description"), fields: z .array( z.object({ @@ -417,13 +513,19 @@ async function getAirtableMcpHandler() { const orgId = getOrgId(); const body: Record = { name, fields: fieldDefs }; if (description) body.description = description; - const data = await airtableFetch(orgId, `/v0/meta/bases/${baseId}/tables`, { - method: "POST", - body: JSON.stringify(body), - }); + const data = await airtableFetch( + orgId, + `/v0/meta/bases/${baseId}/tables`, + { + method: "POST", + body: JSON.stringify(body), + }, + ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to create table"); + return errorResult( + e instanceof Error ? e.message : "Failed to create table", + ); } }, ); @@ -434,7 +536,10 @@ async function getAirtableMcpHandler() { "Update a table's name or description in an Airtable base", { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), - tableId: z.string().min(1).describe("The table ID (starts with 'tbl')"), + tableId: z + .string() + .min(1) + .describe("The table ID (starts with 'tbl')"), name: z.string().optional().describe("New table name"), description: z.string().optional().describe("New table description"), }, @@ -444,13 +549,19 @@ async function getAirtableMcpHandler() { const body: Record = {}; if (name) body.name = name; if (description !== undefined) body.description = description; - const data = await airtableFetch(orgId, `/v0/meta/bases/${baseId}/tables/${tableId}`, { - method: "PATCH", - body: JSON.stringify(body), - }); + const data = await airtableFetch( + orgId, + `/v0/meta/bases/${baseId}/tables/${tableId}`, + { + method: "PATCH", + body: JSON.stringify(body), + }, + ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to update table"); + return errorResult( + e instanceof Error ? e.message : "Failed to update table", + ); } }, ); @@ -461,7 +572,10 @@ async function getAirtableMcpHandler() { "Create a new field (column) in an Airtable table", { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), - tableId: z.string().min(1).describe("The table ID (starts with 'tbl')"), + tableId: z + .string() + .min(1) + .describe("The table ID (starts with 'tbl')"), name: z.string().min(1).describe("Field name"), type: z .string() @@ -470,7 +584,10 @@ async function getAirtableMcpHandler() { "Field type, e.g. singleLineText, number, singleSelect, date, checkbox, etc.", ), description: z.string().optional().describe("Field description"), - options: z.record(z.any()).optional().describe("Field-type-specific options"), + options: z + .record(z.any()) + .optional() + .describe("Field-type-specific options"), }, async ({ baseId, tableId, name, type, description, options }) => { try { @@ -488,7 +605,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to create field"); + return errorResult( + e instanceof Error ? e.message : "Failed to create field", + ); } }, ); @@ -499,11 +618,20 @@ async function getAirtableMcpHandler() { "Update an existing field's name, description, or options in an Airtable table", { baseId: z.string().min(1).describe("The base ID (starts with 'app')"), - tableId: z.string().min(1).describe("The table ID (starts with 'tbl')"), - fieldId: z.string().min(1).describe("The field ID (starts with 'fld')"), + tableId: z + .string() + .min(1) + .describe("The table ID (starts with 'tbl')"), + fieldId: z + .string() + .min(1) + .describe("The field ID (starts with 'fld')"), name: z.string().optional().describe("New field name"), description: z.string().optional().describe("New field description"), - options: z.record(z.any()).optional().describe("Updated field options"), + options: z + .record(z.any()) + .optional() + .describe("Updated field options"), }, async ({ baseId, tableId, fieldId, name, description, options }) => { try { @@ -522,7 +650,9 @@ async function getAirtableMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to update field"); + return errorResult( + e instanceof Error ? e.message : "Failed to update field", + ); } }, ); @@ -549,7 +679,9 @@ async function handleRequest(req: NextRequest): Promise { if (rateLimited) return rateLimited; const handler = await getAirtableMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { @@ -578,7 +710,9 @@ async function withTransportValidation( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } diff --git a/app/api/mcps/asana/[transport]/route.ts b/app/api/mcps/asana/[transport]/route.ts index 261bb1ecc..d23418a01 100644 --- a/app/api/mcps/asana/[transport]/route.ts +++ b/app/api/mcps/asana/[transport]/route.ts @@ -56,7 +56,12 @@ async function getAsanaMcpHandler() { return result.accessToken; } - async function asanaApi(orgId: string, method: string, path: string, body?: unknown) { + async function asanaApi( + orgId: string, + method: string, + path: string, + body?: unknown, + ) { const token = await getAsanaToken(orgId); const url = `${API_BASE}${path}`; @@ -84,7 +89,9 @@ async function getAsanaMcpHandler() { try { parsed = JSON.parse(errorText); } catch {} - throw new Error(parsed?.errors?.[0]?.message || `Asana API error: ${response.status}`); + throw new Error( + parsed?.errors?.[0]?.message || `Asana API error: ${response.status}`, + ); } if (response.status === 204) return { success: true }; @@ -109,7 +116,9 @@ async function getAsanaMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -117,24 +126,33 @@ async function getAsanaMcpHandler() { mcpHandler = createMcpHandler( (server) => { // --- Connection Status --- - server.tool("asana_status", "Check Asana OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "asana", - }); - const active = connections.find((c) => c.status === "active"); - return jsonResult( - active - ? { connected: true, email: active.email, scopes: active.scopes } - : { connected: false }, - ); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "asana_status", + "Check Asana OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "asana", + }); + const active = connections.find((c) => c.status === "active"); + return jsonResult( + active + ? { + connected: true, + email: active.email, + scopes: active.scopes, + } + : { connected: false }, + ); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); // --- Current User --- server.tool( @@ -161,14 +179,26 @@ async function getAsanaMcpHandler() { "asana_list_workspaces", "List Asana workspaces accessible to the user", { - limit: z.number().int().min(1).max(100).optional().describe("Max results (default 100)"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results (default 100)"), }, async ({ limit }) => { try { const orgId = getOrgId(); - const params = new URLSearchParams({ opt_fields: "gid,name,is_organization" }); + const params = new URLSearchParams({ + opt_fields: "gid,name,is_organization", + }); if (limit) params.set("limit", String(limit)); - const data = await asanaApi(orgId, "GET", `/workspaces?${params.toString()}`); + const data = await asanaApi( + orgId, + "GET", + `/workspaces?${params.toString()}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -182,16 +212,27 @@ async function getAsanaMcpHandler() { "List projects in an Asana workspace", { workspaceGid: z.string().min(1).describe("Workspace GID"), - archived: z.boolean().optional().describe("Include archived projects (default false)"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + archived: z + .boolean() + .optional() + .describe("Include archived projects (default false)"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, async ({ workspaceGid, archived, limit }) => { try { const orgId = getOrgId(); const params = new URLSearchParams({ - opt_fields: "gid,name,color,created_at,modified_at,owner,team,archived", + opt_fields: + "gid,name,color,created_at,modified_at,owner,team,archived", }); - if (archived !== undefined) params.set("archived", String(archived)); + if (archived !== undefined) + params.set("archived", String(archived)); if (limit) params.set("limit", String(limit)); const data = await asanaApi( orgId, @@ -230,7 +271,13 @@ async function getAsanaMcpHandler() { "List tasks in an Asana project", { projectGid: z.string().min(1).describe("Project GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), offset: z.string().optional().describe("Pagination offset token"), }, async ({ projectGid, limit, offset }) => { @@ -278,15 +325,33 @@ async function getAsanaMcpHandler() { "Create a new Asana task", { name: z.string().min(1).describe("Task name/title"), - workspaceGid: z.string().optional().describe("Workspace GID (required if no projectGid)"), - projectGid: z.string().optional().describe("Project GID to add the task to"), + workspaceGid: z + .string() + .optional() + .describe("Workspace GID (required if no projectGid)"), + projectGid: z + .string() + .optional() + .describe("Project GID to add the task to"), notes: z.string().optional().describe("Task description"), assignee: z.string().optional().describe("Assignee GID or email"), dueOn: z.string().optional().describe("Due date (YYYY-MM-DD)"), startOn: z.string().optional().describe("Start date (YYYY-MM-DD)"), - parentGid: z.string().optional().describe("Parent task GID (for subtasks)"), + parentGid: z + .string() + .optional() + .describe("Parent task GID (for subtasks)"), }, - async ({ name, workspaceGid, projectGid, notes, assignee, dueOn, startOn, parentGid }) => { + async ({ + name, + workspaceGid, + projectGid, + notes, + assignee, + dueOn, + startOn, + parentGid, + }) => { try { const orgId = getOrgId(); const taskData: Record = { name }; @@ -297,7 +362,9 @@ async function getAsanaMcpHandler() { if (dueOn) taskData.due_on = dueOn; if (startOn) taskData.start_on = startOn; if (parentGid) taskData.parent = parentGid; - const data = await asanaApi(orgId, "POST", "/tasks", { data: taskData }); + const data = await asanaApi(orgId, "POST", "/tasks", { + data: taskData, + }); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -314,10 +381,21 @@ async function getAsanaMcpHandler() { notes: z.string().optional().describe("New description"), assignee: z.string().optional().describe("New assignee GID or email"), dueOn: z.string().optional().describe("New due date (YYYY-MM-DD)"), - startOn: z.string().optional().describe("New start date (YYYY-MM-DD)"), + startOn: z + .string() + .optional() + .describe("New start date (YYYY-MM-DD)"), completed: z.boolean().optional().describe("Mark task as completed"), }, - async ({ taskGid, name, notes, assignee, dueOn, startOn, completed }) => { + async ({ + taskGid, + name, + notes, + assignee, + dueOn, + startOn, + completed, + }) => { try { const orgId = getOrgId(); const taskData: Record = {}; @@ -327,7 +405,9 @@ async function getAsanaMcpHandler() { if (dueOn !== undefined) taskData.due_on = dueOn; if (startOn !== undefined) taskData.start_on = startOn; if (completed !== undefined) taskData.completed = completed; - const data = await asanaApi(orgId, "PUT", `/tasks/${taskGid}`, { data: taskData }); + const data = await asanaApi(orgId, "PUT", `/tasks/${taskGid}`, { + data: taskData, + }); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -341,15 +421,32 @@ async function getAsanaMcpHandler() { { workspaceGid: z.string().min(1).describe("Workspace GID"), text: z.string().optional().describe("Free text search"), - assignee: z.string().optional().describe("Assignee GID (use 'me' for current user)"), + assignee: z + .string() + .optional() + .describe("Assignee GID (use 'me' for current user)"), projectGid: z.string().optional().describe("Filter by project GID"), - completed: z.boolean().optional().describe("Filter by completion status"), + completed: z + .boolean() + .optional() + .describe("Filter by completion status"), sortBy: z .string() .optional() - .describe("Sort by: created_at, completed_at, modified_at, due_date, likes"), - sortAscending: z.boolean().optional().describe("Sort ascending (default false)"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + .describe( + "Sort by: created_at, completed_at, modified_at, due_date, likes", + ), + sortAscending: z + .boolean() + .optional() + .describe("Sort ascending (default false)"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, async ({ workspaceGid, @@ -370,9 +467,11 @@ async function getAsanaMcpHandler() { if (text) params.set("text", text); if (assignee) params.set("assignee.any", assignee); if (projectGid) params.set("projects.any", projectGid); - if (completed !== undefined) params.set("completed", String(completed)); + if (completed !== undefined) + params.set("completed", String(completed)); if (sortBy) params.set("sort_by", sortBy); - if (sortAscending !== undefined) params.set("sort_ascending", String(sortAscending)); + if (sortAscending !== undefined) + params.set("sort_ascending", String(sortAscending)); if (limit) params.set("limit", String(limit)); const data = await asanaApi( orgId, @@ -397,9 +496,14 @@ async function getAsanaMcpHandler() { async ({ taskGid, text }) => { try { const orgId = getOrgId(); - const data = await asanaApi(orgId, "POST", `/tasks/${taskGid}/stories`, { - data: { text }, - }); + const data = await asanaApi( + orgId, + "POST", + `/tasks/${taskGid}/stories`, + { + data: { text }, + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -412,13 +516,20 @@ async function getAsanaMcpHandler() { "List comments on an Asana task", { taskGid: z.string().min(1).describe("Task GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, async ({ taskGid, limit }) => { try { const orgId = getOrgId(); const params = new URLSearchParams({ - opt_fields: "gid,text,created_at,created_by,created_by.name,resource_subtype", + opt_fields: + "gid,text,created_at,created_by,created_by.name,resource_subtype", }); if (limit) params.set("limit", String(limit)); const data = await asanaApi( @@ -439,12 +550,20 @@ async function getAsanaMcpHandler() { "List sections in an Asana project", { projectGid: z.string().min(1).describe("Project GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, async ({ projectGid, limit }) => { try { const orgId = getOrgId(); - const params = new URLSearchParams({ opt_fields: "gid,name,created_at" }); + const params = new URLSearchParams({ + opt_fields: "gid,name,created_at", + }); if (limit) params.set("limit", String(limit)); const data = await asanaApi( orgId, @@ -464,12 +583,20 @@ async function getAsanaMcpHandler() { "List users in an Asana workspace", { workspaceGid: z.string().min(1).describe("Workspace GID"), - limit: z.number().int().min(1).max(100).optional().describe("Max results"), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Max results"), }, async ({ workspaceGid, limit }) => { try { const orgId = getOrgId(); - const params = new URLSearchParams({ opt_fields: "gid,name,email,photo" }); + const params = new URLSearchParams({ + opt_fields: "gid,name,email,photo", + }); if (limit) params.set("limit", String(limit)); const data = await asanaApi( orgId, @@ -505,7 +632,9 @@ async function handleRequest(req: NextRequest): Promise { if (rateLimited) return rateLimited; const handler = await getAsanaMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { @@ -534,7 +663,9 @@ async function withTransportValidation( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } diff --git a/app/api/mcps/crypto/[transport]/route.ts b/app/api/mcps/crypto/[transport]/route.ts index 4f8b22734..6cd96a36a 100644 --- a/app/api/mcps/crypto/[transport]/route.ts +++ b/app/api/mcps/crypto/[transport]/route.ts @@ -87,7 +87,9 @@ async function fetchWithCache(url: string, cacheKey: string) { }); if (!response.ok) { - throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`); + throw new Error( + `CoinGecko API error: ${response.status} ${response.statusText}`, + ); } const data = await response.json(); @@ -158,7 +160,9 @@ function createHandler() { .string() .optional() .default("usd") - .describe("The fiat currency for price (e.g., 'usd', 'eur', 'gbp'). Defaults to USD."), + .describe( + "The fiat currency for price (e.g., 'usd', 'eur', 'gbp'). Defaults to USD.", + ), }, async ({ coin, currency = "usd" }) => { try { @@ -178,7 +182,8 @@ function createHandler() { text: JSON.stringify( { error: `Cryptocurrency "${coin}" not found. Try using the full name (e.g., "bitcoin") or check the spelling.`, - suggestion: "Use list_trending to see popular cryptocurrencies.", + suggestion: + "Use list_trending to see popular cryptocurrencies.", }, null, 2, @@ -191,9 +196,12 @@ function createHandler() { const coinData = data[coinId]; const price = coinData[currency]; - const change24h = coinData[`${currency}_24h_change`] || coinData.usd_24h_change; - const marketCap = coinData[`${currency}_market_cap`] || coinData.usd_market_cap; - const volume24h = coinData[`${currency}_24h_vol`] || coinData.usd_24h_vol; + const change24h = + coinData[`${currency}_24h_change`] || coinData.usd_24h_change; + const marketCap = + coinData[`${currency}_market_cap`] || coinData.usd_market_cap; + const volume24h = + coinData[`${currency}_24h_vol`] || coinData.usd_24h_vol; const response = { coin: coinId, @@ -201,10 +209,13 @@ function createHandler() { price: { value: price, currency: currency.toUpperCase(), - formatted: `${currency.toUpperCase()} ${price?.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: price < 1 ? 8 : 2, - })}`, + formatted: `${currency.toUpperCase()} ${price?.toLocaleString( + undefined, + { + minimumFractionDigits: 2, + maximumFractionDigits: price < 1 ? 8 : 2, + }, + )}`, }, change24h: change24h ? { @@ -244,7 +255,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to fetch price", + error: + error instanceof Error + ? error.message + : "Failed to fetch price", }, null, 2, @@ -265,7 +279,9 @@ function createHandler() { { coin: z .string() - .describe("The cryptocurrency name or symbol (e.g., 'bitcoin', 'eth', 'solana')"), + .describe( + "The cryptocurrency name or symbol (e.g., 'bitcoin', 'eth', 'solana')", + ), }, async ({ coin }) => { try { @@ -340,7 +356,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to fetch market data", + error: + error instanceof Error + ? error.message + : "Failed to fetch market data", }, null, 2, @@ -377,7 +396,8 @@ function createHandler() { marketCapRank: coin.item.market_cap_rank, image: coin.item.small || coin.item.thumb, priceUsd: coin.item.data?.price, - priceChange24h: coin.item.data?.price_change_percentage_24h?.usd, + priceChange24h: + coin.item.data?.price_change_percentage_24h?.usd, marketCap: coin.item.data?.market_cap, volume24h: coin.item.data?.total_volume, })) || []; @@ -408,7 +428,9 @@ function createHandler() { text: JSON.stringify( { error: - error instanceof Error ? error.message : "Failed to fetch trending coins", + error instanceof Error + ? error.message + : "Failed to fetch trending coins", }, null, 2, diff --git a/app/api/mcps/crypto/route.ts b/app/api/mcps/crypto/route.ts index 881bda135..d35a755e7 100644 --- a/app/api/mcps/crypto/route.ts +++ b/app/api/mcps/crypto/route.ts @@ -24,13 +24,15 @@ export async function GET() { }, { name: "get_market_data", - description: "Get comprehensive market data including price, volume, supply, ATH/ATL", + description: + "Get comprehensive market data including price, volume, supply, ATH/ATL", price: "Free", example: { coin: "ethereum" }, }, { name: "list_trending", - description: "Get list of trending cryptocurrencies by search popularity", + description: + "Get list of trending cryptocurrencies by search popularity", price: "Free", example: {}, }, diff --git a/app/api/mcps/dropbox/[transport]/route.ts b/app/api/mcps/dropbox/[transport]/route.ts index 92cc677a4..a69859c2c 100644 --- a/app/api/mcps/dropbox/[transport]/route.ts +++ b/app/api/mcps/dropbox/[transport]/route.ts @@ -61,11 +61,17 @@ async function getDropboxMcpHandler() { options.body = JSON.stringify(body); } - const response = await fetch(`https://api.dropboxapi.com${endpoint}`, options); + const response = await fetch( + `https://api.dropboxapi.com${endpoint}`, + options, + ); if (!response.ok) { const error = await response.json().catch(() => ({})); - const summary = error.error_summary || error.error?.[".tag"] || `HTTP ${response.status}`; + const summary = + error.error_summary || + error.error?.[".tag"] || + `HTTP ${response.status}`; throw new Error(`Dropbox API error: ${summary}`); } @@ -93,7 +99,9 @@ async function getDropboxMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -101,66 +109,106 @@ async function getDropboxMcpHandler() { mcpHandler = createMcpHandler( (server) => { // --- Connection Status --- - server.tool("dropbox_status", "Check Dropbox OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "dropbox", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) { - const expired = connections.find((c) => c.status === "expired"); - if (expired) { - return jsonResult({ - connected: false, - status: "expired", - message: "Dropbox connection expired. Please reconnect in Settings > Connections.", - }); + server.tool( + "dropbox_status", + "Check Dropbox OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "dropbox", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) { + const expired = connections.find((c) => c.status === "expired"); + if (expired) { + return jsonResult({ + connected: false, + status: "expired", + message: + "Dropbox connection expired. Please reconnect in Settings > Connections.", + }); + } + return jsonResult({ connected: false }); } - return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); } - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + }, + ); // --- Account --- - server.tool("dropbox_get_account", "Get current Dropbox account info", {}, async () => { - try { - const data = await dropboxRpc(getOrgId(), "/2/users/get_current_account"); - return jsonResult(data); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "dropbox_get_account", + "Get current Dropbox account info", + {}, + async () => { + try { + const data = await dropboxRpc( + getOrgId(), + "/2/users/get_current_account", + ); + return jsonResult(data); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); - server.tool("dropbox_get_space_usage", "Get Dropbox storage space usage", {}, async () => { - try { - const data = await dropboxRpc(getOrgId(), "/2/users/get_space_usage"); - return jsonResult(data); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "dropbox_get_space_usage", + "Get Dropbox storage space usage", + {}, + async () => { + try { + const data = await dropboxRpc( + getOrgId(), + "/2/users/get_space_usage", + ); + return jsonResult(data); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); // --- File Operations --- server.tool( "dropbox_list_folder", "List files and folders in a Dropbox directory. Use empty string for root.", { - path: z.string().describe('Folder path (empty string "" for root, or "/path/to/folder")'), + path: z + .string() + .describe( + 'Folder path (empty string "" for root, or "/path/to/folder")', + ), recursive: z.boolean().optional().describe("List recursively"), - limit: z.number().int().min(1).max(2000).optional().describe("Max entries to return"), + limit: z + .number() + .int() + .min(1) + .max(2000) + .optional() + .describe("Max entries to return"), }, async ({ path, recursive, limit }) => { try { const body: Record = { path }; if (recursive !== undefined) body.recursive = recursive; if (limit !== undefined) body.limit = limit; - const data = await dropboxRpc(getOrgId(), "/2/files/list_folder", body); + const data = await dropboxRpc( + getOrgId(), + "/2/files/list_folder", + body, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -171,10 +219,19 @@ async function getDropboxMcpHandler() { server.tool( "dropbox_list_folder_continue", "Continue listing files using cursor from a previous list_folder call", - { cursor: z.string().min(1).describe("Cursor from previous list_folder response") }, + { + cursor: z + .string() + .min(1) + .describe("Cursor from previous list_folder response"), + }, async ({ cursor }) => { try { - const data = await dropboxRpc(getOrgId(), "/2/files/list_folder/continue", { cursor }); + const data = await dropboxRpc( + getOrgId(), + "/2/files/list_folder/continue", + { cursor }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -192,8 +249,13 @@ async function getDropboxMcpHandler() { async ({ path, include_media_info }) => { try { const body: Record = { path }; - if (include_media_info !== undefined) body.include_media_info = include_media_info; - const data = await dropboxRpc(getOrgId(), "/2/files/get_metadata", body); + if (include_media_info !== undefined) + body.include_media_info = include_media_info; + const data = await dropboxRpc( + getOrgId(), + "/2/files/get_metadata", + body, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -218,7 +280,11 @@ async function getDropboxMcpHandler() { if (max_results) options.max_results = max_results; if (file_status) options.file_status = file_status; if (Object.keys(options).length > 0) body.options = options; - const data = await dropboxRpc(getOrgId(), "/2/files/search_v2", body); + const data = await dropboxRpc( + getOrgId(), + "/2/files/search_v2", + body, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -230,14 +296,24 @@ async function getDropboxMcpHandler() { "dropbox_create_folder", "Create a new folder in Dropbox", { - path: z.string().min(1).describe("Path for the new folder (e.g. /Documents/NewFolder)"), - autorename: z.boolean().optional().describe("Auto-rename if folder exists"), + path: z + .string() + .min(1) + .describe("Path for the new folder (e.g. /Documents/NewFolder)"), + autorename: z + .boolean() + .optional() + .describe("Auto-rename if folder exists"), }, async ({ path, autorename }) => { try { const body: Record = { path }; if (autorename !== undefined) body.autorename = autorename; - const data = await dropboxRpc(getOrgId(), "/2/files/create_folder_v2", body); + const data = await dropboxRpc( + getOrgId(), + "/2/files/create_folder_v2", + body, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -249,12 +325,17 @@ async function getDropboxMcpHandler() { "dropbox_upload_text", "Upload/create a text file in Dropbox. Use for creating new files with text content.", { - path: z.string().min(1).describe("File path including name (e.g. /Documents/notes.txt)"), + path: z + .string() + .min(1) + .describe("File path including name (e.g. /Documents/notes.txt)"), content: z.string().describe("Text content of the file"), mode: z .enum(["add", "overwrite"]) .optional() - .describe("'add' to avoid overwriting (default), 'overwrite' to replace existing"), + .describe( + "'add' to avoid overwriting (default), 'overwrite' to replace existing", + ), autorename: z .boolean() .optional() @@ -264,22 +345,30 @@ async function getDropboxMcpHandler() { try { const orgId = getOrgId(); const token = await getDropboxToken(orgId); - const apiArg: Record = { path, mode: mode || "add" }; + const apiArg: Record = { + path, + mode: mode || "add", + }; if (autorename !== undefined) apiArg.autorename = autorename; - const response = await fetch("https://content.dropboxapi.com/2/files/upload", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/octet-stream", - "Dropbox-API-Arg": JSON.stringify(apiArg), + const response = await fetch( + "https://content.dropboxapi.com/2/files/upload", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/octet-stream", + "Dropbox-API-Arg": JSON.stringify(apiArg), + }, + body: content, }, - body: content, - }); + ); if (!response.ok) { const error = await response.json().catch(() => ({})); - throw new Error(error.error_summary || `Upload failed: ${response.status}`); + throw new Error( + error.error_summary || `Upload failed: ${response.status}`, + ); } const data = await response.json(); @@ -296,7 +385,9 @@ async function getDropboxMcpHandler() { { path: z.string().min(1).describe("Path to delete") }, async ({ path }) => { try { - const data = await dropboxRpc(getOrgId(), "/2/files/delete_v2", { path }); + const data = await dropboxRpc(getOrgId(), "/2/files/delete_v2", { + path, + }); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -313,7 +404,12 @@ async function getDropboxMcpHandler() { autorename: z.boolean().optional(), allow_ownership_transfer: z.boolean().optional(), }, - async ({ from_path, to_path, autorename, allow_ownership_transfer }) => { + async ({ + from_path, + to_path, + autorename, + allow_ownership_transfer, + }) => { try { const body: Record = { from_path, to_path }; if (autorename !== undefined) body.autorename = autorename; @@ -353,7 +449,9 @@ async function getDropboxMcpHandler() { "Create a shared link for a file or folder", { path: z.string().min(1).describe("File or folder path"), - requested_visibility: z.enum(["public", "team_only", "password"]).optional(), + requested_visibility: z + .enum(["public", "team_only", "password"]) + .optional(), }, async ({ path, requested_visibility }) => { try { @@ -377,7 +475,10 @@ async function getDropboxMcpHandler() { "dropbox_list_shared_links", "List shared links for a file/folder or all shared links", { - path: z.string().optional().describe("File/folder path (omit for all shared links)"), + path: z + .string() + .optional() + .describe("File/folder path (omit for all shared links)"), cursor: z.string().optional().describe("Cursor for pagination"), direct_only: z.boolean().optional(), }, @@ -387,7 +488,11 @@ async function getDropboxMcpHandler() { if (path) body.path = path; if (cursor) body.cursor = cursor; if (direct_only !== undefined) body.direct_only = direct_only; - const data = await dropboxRpc(getOrgId(), "/2/sharing/list_shared_links", body); + const data = await dropboxRpc( + getOrgId(), + "/2/sharing/list_shared_links", + body, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -401,7 +506,9 @@ async function getDropboxMcpHandler() { { url: z.string().min(1).describe("The shared link URL to revoke") }, async ({ url }) => { try { - await dropboxRpc(getOrgId(), "/2/sharing/revoke_shared_link", { url }); + await dropboxRpc(getOrgId(), "/2/sharing/revoke_shared_link", { + url, + }); return jsonResult({ success: true }); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -431,7 +538,9 @@ async function handleRequest(req: NextRequest): Promise { if (rateLimited) return rateLimited; const handler = await getDropboxMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { @@ -460,7 +569,9 @@ async function withTransportValidation( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } diff --git a/app/api/mcps/github/[transport]/route.ts b/app/api/mcps/github/[transport]/route.ts index 2bb281cb3..2ea4dcec7 100644 --- a/app/api/mcps/github/[transport]/route.ts +++ b/app/api/mcps/github/[transport]/route.ts @@ -44,7 +44,11 @@ async function getGitHubMcpHandler() { return result.accessToken; } - async function githubFetch(orgId: string, endpoint: string, options: RequestInit = {}) { + async function githubFetch( + orgId: string, + endpoint: string, + options: RequestInit = {}, + ) { const token = await getGitHubToken(orgId); const response = await fetch(`https://api.github.com${endpoint}`, { ...options, @@ -86,12 +90,16 @@ async function getGitHubMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } - function buildQuery(params: Record) { + function buildQuery( + params: Record, + ) { const sp = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined) sp.set(key, String(value)); @@ -102,39 +110,46 @@ async function getGitHubMcpHandler() { return createMcpHandler( (server) => { - server.tool("github_status", "Check GitHub OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "github", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) { - const expired = connections.find((c) => c.status === "expired"); - if (expired) { + server.tool( + "github_status", + "Check GitHub OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "github", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) { + const expired = connections.find((c) => c.status === "expired"); + if (expired) { + return jsonResult({ + connected: false, + status: "expired", + message: + "GitHub connection expired. Please reconnect in Settings > Connections.", + }); + } return jsonResult({ connected: false, - status: "expired", - message: "GitHub connection expired. Please reconnect in Settings > Connections.", + message: + "GitHub not connected. Connect in Settings > Connections.", }); } return jsonResult({ - connected: false, - message: "GitHub not connected. Connect in Settings > Connections.", + connected: true, + email: active.email, + scopes: active.scopes, + linkedAt: active.linkedAt, }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); } - return jsonResult({ - connected: true, - email: active.email, - scopes: active.scopes, - linkedAt: active.linkedAt, - }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + }, + ); server.tool( "github_list_repos", @@ -244,7 +259,9 @@ async function getGitHubMcpHandler() { async ({ owner, repo }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}`, { method: "DELETE" }); + const data = await githubFetch(orgId, `/repos/${owner}/${repo}`, { + method: "DELETE", + }); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -292,7 +309,10 @@ async function getGitHubMcpHandler() { async ({ owner, repo, issue_number }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/issues/${issue_number}`); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/issues/${issue_number}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -315,10 +335,20 @@ async function getGitHubMcpHandler() { async ({ owner, repo, title, body, assignees, labels, milestone }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/issues`, { - method: "POST", - body: JSON.stringify({ title, body, assignees, labels, milestone }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/issues`, + { + method: "POST", + body: JSON.stringify({ + title, + body, + assignees, + labels, + milestone, + }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -492,7 +522,11 @@ async function getGitHubMcpHandler() { server.tool( "github_delete_issue_comment", "Delete issue comment", - { owner: z.string().min(1), repo: z.string().min(1), comment_id: z.number().int().min(1) }, + { + owner: z.string().min(1), + repo: z.string().min(1), + comment_id: z.number().int().min(1), + }, async ({ owner, repo, comment_id }) => { try { const orgId = getOrgId(); @@ -539,11 +573,18 @@ async function getGitHubMcpHandler() { server.tool( "github_get_pr", "Get pull request", - { owner: z.string().min(1), repo: z.string().min(1), pull_number: z.number().int().min(1) }, + { + owner: z.string().min(1), + repo: z.string().min(1), + pull_number: z.number().int().min(1), + }, async ({ owner, repo, pull_number }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/pulls/${pull_number}`); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/pulls/${pull_number}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -566,10 +607,14 @@ async function getGitHubMcpHandler() { async ({ owner, repo, title, head, base, body, draft }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/pulls`, { - method: "POST", - body: JSON.stringify({ title, head, base, body, draft }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/pulls`, + { + method: "POST", + body: JSON.stringify({ title, head, base, body, draft }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -593,10 +638,14 @@ async function getGitHubMcpHandler() { async ({ owner, repo, pull_number, ...rest }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/pulls/${pull_number}`, { - method: "PATCH", - body: JSON.stringify(rest), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/pulls/${pull_number}`, + { + method: "PATCH", + body: JSON.stringify(rest), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -615,7 +664,14 @@ async function getGitHubMcpHandler() { commit_message: z.string().optional(), merge_method: z.string().optional(), }, - async ({ owner, repo, pull_number, commit_title, commit_message, merge_method }) => { + async ({ + owner, + repo, + pull_number, + commit_title, + commit_message, + merge_method, + }) => { try { const orgId = getOrgId(); const data = await githubFetch( @@ -623,7 +679,11 @@ async function getGitHubMcpHandler() { `/repos/${owner}/${repo}/pulls/${pull_number}/merge`, { method: "PUT", - body: JSON.stringify({ commit_title, commit_message, merge_method }), + body: JSON.stringify({ + commit_title, + commit_message, + merge_method, + }), }, ); return jsonResult(data); @@ -722,10 +782,14 @@ async function getGitHubMcpHandler() { async ({ owner, repo, name, color, description }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/labels`, { - method: "POST", - body: JSON.stringify({ name, color, description }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/labels`, + { + method: "POST", + body: JSON.stringify({ name, color, description }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -765,7 +829,11 @@ async function getGitHubMcpHandler() { server.tool( "github_delete_label", "Delete label", - { owner: z.string().min(1), repo: z.string().min(1), name: z.string().min(1) }, + { + owner: z.string().min(1), + repo: z.string().min(1), + name: z.string().min(1), + }, async ({ owner, repo, name }) => { try { const orgId = getOrgId(); @@ -821,10 +889,14 @@ async function getGitHubMcpHandler() { async ({ owner, repo, title, state, description, due_on }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/milestones`, { - method: "POST", - body: JSON.stringify({ title, state, description, due_on }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/milestones`, + { + method: "POST", + body: JSON.stringify({ title, state, description, due_on }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -895,7 +967,10 @@ async function getGitHubMcpHandler() { async ({ per_page, page }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/user/orgs${buildQuery({ per_page, page })}`); + const data = await githubFetch( + orgId, + `/user/orgs${buildQuery({ per_page, page })}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -969,7 +1044,10 @@ async function getGitHubMcpHandler() { async ({ org, team_slug }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/orgs/${org}/teams/${team_slug}`); + const data = await githubFetch( + orgId, + `/orgs/${org}/teams/${team_slug}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -1026,7 +1104,11 @@ async function getGitHubMcpHandler() { server.tool( "github_get_branch", "Get branch", - { owner: z.string().min(1), repo: z.string().min(1), branch: z.string().min(1) }, + { + owner: z.string().min(1), + repo: z.string().min(1), + branch: z.string().min(1), + }, async ({ owner, repo, branch }) => { try { const orgId = getOrgId(); @@ -1044,7 +1126,11 @@ async function getGitHubMcpHandler() { server.tool( "github_delete_branch", "Delete branch", - { owner: z.string().min(1), repo: z.string().min(1), branch: z.string().min(1) }, + { + owner: z.string().min(1), + repo: z.string().min(1), + branch: z.string().min(1), + }, async ({ owner, repo, branch }) => { try { const orgId = getOrgId(); @@ -1093,11 +1179,18 @@ async function getGitHubMcpHandler() { server.tool( "github_get_commit", "Get commit", - { owner: z.string().min(1), repo: z.string().min(1), ref: z.string().min(1) }, + { + owner: z.string().min(1), + repo: z.string().min(1), + ref: z.string().min(1), + }, async ({ owner, repo, ref }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/commits/${ref}`); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/commits/${ref}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -1141,14 +1234,33 @@ async function getGitHubMcpHandler() { committer: z.record(z.string(), z.any()).optional(), author: z.record(z.string(), z.any()).optional(), }, - async ({ owner, repo, path, message, content, branch, committer, author }) => { + async ({ + owner, + repo, + path, + message, + content, + branch, + committer, + author, + }) => { try { const orgId = getOrgId(); const encodedContent = Buffer.from(content).toString("base64"); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/contents/${path}`, { - method: "PUT", - body: JSON.stringify({ message, content: encodedContent, branch, committer, author }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/contents/${path}`, + { + method: "PUT", + body: JSON.stringify({ + message, + content: encodedContent, + branch, + committer, + author, + }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -1170,21 +1282,35 @@ async function getGitHubMcpHandler() { committer: z.record(z.string(), z.any()).optional(), author: z.record(z.string(), z.any()).optional(), }, - async ({ owner, repo, path, message, content, sha, branch, committer, author }) => { + async ({ + owner, + repo, + path, + message, + content, + sha, + branch, + committer, + author, + }) => { try { const orgId = getOrgId(); const encodedContent = Buffer.from(content).toString("base64"); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/contents/${path}`, { - method: "PUT", - body: JSON.stringify({ - message, - content: encodedContent, - sha, - branch, - committer, - author, - }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/contents/${path}`, + { + method: "PUT", + body: JSON.stringify({ + message, + content: encodedContent, + sha, + branch, + committer, + author, + }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -1205,13 +1331,32 @@ async function getGitHubMcpHandler() { committer: z.record(z.string(), z.any()).optional(), author: z.record(z.string(), z.any()).optional(), }, - async ({ owner, repo, path, message, sha, branch, committer, author }) => { + async ({ + owner, + repo, + path, + message, + sha, + branch, + committer, + author, + }) => { try { const orgId = getOrgId(); - const data = await githubFetch(orgId, `/repos/${owner}/${repo}/contents/${path}`, { - method: "DELETE", - body: JSON.stringify({ message, sha, branch, committer, author }), - }); + const data = await githubFetch( + orgId, + `/repos/${owner}/${repo}/contents/${path}`, + { + method: "DELETE", + body: JSON.stringify({ + message, + sha, + branch, + committer, + author, + }), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -1235,7 +1380,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -1250,7 +1397,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getGitHubMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/google/[transport]/route.ts b/app/api/mcps/google/[transport]/route.ts index cc84c1041..21a87cc1e 100644 --- a/app/api/mcps/google/[transport]/route.ts +++ b/app/api/mcps/google/[transport]/route.ts @@ -75,7 +75,9 @@ async function getGoogleMcpHandler() { organizationId, error: error instanceof Error ? error.message : String(error), }); - throw new Error("Google account not connected. Connect in Settings > Connections."); + throw new Error( + "Google account not connected. Connect in Settings > Connections.", + ); } } @@ -89,12 +91,16 @@ async function getGoogleMcpHandler() { } function jsonResult(data: object) { - return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] }; + return { + content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }], + }; } function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -127,13 +133,15 @@ async function getGoogleMcpHandler() { success: true, connected: false, status: "expired", - message: "Google connection expired. Please reconnect in Settings > Connections.", + message: + "Google connection expired. Please reconnect in Settings > Connections.", }); } return jsonResult({ success: true, connected: false, - message: "Google not connected. Connect in Settings > Connections.", + message: + "Google not connected. Connect in Settings > Connections.", }); } @@ -159,12 +167,22 @@ async function getGoogleMcpHandler() { "gmail_send", "Send email via Gmail. Supports plain text and HTML, with CC/BCC.", { - to: z.string().min(1).describe("Recipient(s), comma-separated email addresses"), + to: z + .string() + .min(1) + .describe("Recipient(s), comma-separated email addresses"), subject: z.string().min(1).describe("Subject line"), body: z.string().min(1).describe("Email body content"), - isHtml: z.boolean().optional().default(false).describe("Send as HTML format"), + isHtml: z + .boolean() + .optional() + .default(false) + .describe("Send as HTML format"), cc: z.string().optional().describe("CC recipients, comma-separated"), - bcc: z.string().optional().describe("BCC recipients, comma-separated"), + bcc: z + .string() + .optional() + .describe("BCC recipients, comma-separated"), }, async ({ to, subject, body, isHtml = false, cc, bcc }) => { try { @@ -193,7 +211,11 @@ async function getGoogleMcpHandler() { ); const result = await res.json(); logger.info("[GoogleMCP] Email sent", { messageId: result.id, to }); - return jsonResult({ success: true, messageId: result.id, threadId: result.threadId }); + return jsonResult({ + success: true, + messageId: result.id, + threadId: result.threadId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to send email")); } @@ -223,7 +245,9 @@ async function getGoogleMcpHandler() { labelIds: z .string() .optional() - .describe("Label IDs, comma-separated (e.g. 'INBOX', 'UNREAD', 'STARRED')"), + .describe( + "Label IDs, comma-separated (e.g. 'INBOX', 'UNREAD', 'STARRED')", + ), after: z .string() .optional() @@ -239,9 +263,18 @@ async function getGoogleMcpHandler() { pageToken: z .string() .optional() - .describe("Token from a previous response's nextPageToken to fetch the next page"), + .describe( + "Token from a previous response's nextPageToken to fetch the next page", + ), }, - async ({ query, maxResults = 10, labelIds, after, before, pageToken }) => { + async ({ + query, + maxResults = 10, + labelIds, + after, + before, + pageToken, + }) => { try { const orgId = getOrgId(); let effectiveQuery = query || ""; @@ -266,7 +299,9 @@ async function getGoogleMcpHandler() { } effectiveQuery = effectiveQuery.trim(); - const params = new URLSearchParams({ maxResults: String(maxResults) }); + const params = new URLSearchParams({ + maxResults: String(maxResults), + }); if (effectiveQuery) params.set("q", effectiveQuery); if (pageToken) params.set("pageToken", pageToken); if (labelIds) { @@ -296,7 +331,9 @@ async function getGoogleMcpHandler() { }); } - const messageIds = messages.slice(0, maxResults).map((m: { id: string }) => m.id); + const messageIds = messages + .slice(0, maxResults) + .map((m: { id: string }) => m.id); const results = await Promise.all( messageIds.map(async (id: string) => { try { @@ -347,7 +384,9 @@ async function getGoogleMcpHandler() { .enum(["full", "metadata", "minimal"]) .optional() .default("full") - .describe("Response format: full (with body), metadata (headers only), or minimal"), + .describe( + "Response format: full (with body), metadata (headers only), or minimal", + ), }, async ({ messageId, format = "full" }) => { try { @@ -367,7 +406,9 @@ async function getGoogleMcpHandler() { headers: {}, body: "", internalDate: msg.internalDate - ? new Date(Number.parseInt(msg.internalDate, 10)).toISOString() + ? new Date( + Number.parseInt(msg.internalDate, 10), + ).toISOString() : undefined, }); } @@ -378,10 +419,9 @@ async function getGoogleMcpHandler() { labelIds: msg.labelIds, snippet: msg.snippet, headers: Object.fromEntries( - msg.payload.headers?.map((h: { name: string; value: string }) => [ - h.name, - h.value, - ]) || [], + msg.payload.headers?.map( + (h: { name: string; value: string }) => [h.name, h.value], + ) || [], ), body: extractBody(msg.payload), internalDate: msg.internalDate @@ -417,13 +457,18 @@ async function getGoogleMcpHandler() { timeMax: z .string() .optional() - .describe("Only events before this time (ISO 8601, e.g. 2026-02-20T23:59:59Z)"), + .describe( + "Only events before this time (ISO 8601, e.g. 2026-02-20T23:59:59Z)", + ), calendarId: z .string() .optional() .default("primary") .describe("Calendar ID (default: 'primary')"), - query: z.string().optional().describe("Free-text search across event fields"), + query: z + .string() + .optional() + .describe("Free-text search across event fields"), timeZone: z .string() .optional() @@ -433,7 +478,9 @@ async function getGoogleMcpHandler() { pageToken: z .string() .optional() - .describe("Token from a previous response's nextPageToken to fetch the next page"), + .describe( + "Token from a previous response's nextPageToken to fetch the next page", + ), }, async ({ maxResults = 10, @@ -520,9 +567,15 @@ async function getGoogleMcpHandler() { .describe( "IANA timezone for start/end times (e.g. 'Asia/Kolkata', 'America/New_York'). Get from google_status → calendarTimeZone. If omitted, fetched automatically from user's calendar.", ), - description: z.string().optional().describe("Event description/notes"), + description: z + .string() + .optional() + .describe("Event description/notes"), location: z.string().optional().describe("Event location"), - attendees: z.array(z.string().email()).optional().describe("Attendee email addresses"), + attendees: z + .array(z.string().email()) + .optional() + .describe("Attendee email addresses"), calendarId: z .string() .optional() @@ -550,7 +603,9 @@ async function getGoogleMcpHandler() { const hasUtcSuffix = start.endsWith("Z") || end.endsWith("Z"); const tz = timeZone || - (!hasUtcSuffix ? ((await fetchCalendarTimeZone(orgId)) ?? undefined) : undefined); + (!hasUtcSuffix + ? ((await fetchCalendarTimeZone(orgId)) ?? undefined) + : undefined); const event: Record = { summary, @@ -573,7 +628,11 @@ async function getGoogleMcpHandler() { }, ); const result = await res.json(); - logger.info("[GoogleMCP] Event created", { eventId: result.id, summary, timeZone: tz }); + logger.info("[GoogleMCP] Event created", { + eventId: result.id, + summary, + timeZone: tz, + }); return jsonResult({ success: true, eventId: result.id, @@ -646,13 +705,17 @@ async function getGoogleMcpHandler() { const hasUtcSuffix = start?.endsWith("Z") || end?.endsWith("Z"); const existingEventTimeZone = (!hasUtcSuffix && - ((typeof existing?.start?.timeZone === "string" && existing.start.timeZone) || - (typeof existing?.end?.timeZone === "string" && existing.end.timeZone))) || + ((typeof existing?.start?.timeZone === "string" && + existing.start.timeZone) || + (typeof existing?.end?.timeZone === "string" && + existing.end.timeZone))) || undefined; const tz = timeZone || existingEventTimeZone || - (!hasUtcSuffix ? ((await fetchCalendarTimeZone(orgId)) ?? undefined) : undefined); + (!hasUtcSuffix + ? ((await fetchCalendarTimeZone(orgId)) ?? undefined) + : undefined); const updated = { ...existing, @@ -675,13 +738,20 @@ async function getGoogleMcpHandler() { }), }; - const res = await googleFetch(orgId, `${baseUrl}?sendUpdates=${sendUpdates}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(updated), - }); + const res = await googleFetch( + orgId, + `${baseUrl}?sendUpdates=${sendUpdates}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updated), + }, + ); const result = await res.json(); - logger.info("[GoogleMCP] Event updated", { eventId: result.id, timeZone: tz }); + logger.info("[GoogleMCP] Event updated", { + eventId: result.id, + timeZone: tz, + }); return jsonResult({ success: true, eventId: result.id, @@ -745,11 +815,15 @@ async function getGoogleMcpHandler() { query: z .string() .optional() - .describe("Search query to find contacts by name, email, phone, or organization"), + .describe( + "Search query to find contacts by name, email, phone, or organization", + ), pageToken: z .string() .optional() - .describe("Token from a previous response's nextPageToken to fetch the next page"), + .describe( + "Token from a previous response's nextPageToken to fetch the next page", + ), }, async ({ pageSize = 20, query, pageToken }) => { try { @@ -763,7 +837,10 @@ async function getGoogleMcpHandler() { if (query) { url = "https://people.googleapis.com/v1/people:searchContacts"; params.set("query", query); - params.set("readMask", "names,emailAddresses,phoneNumbers,organizations"); + params.set( + "readMask", + "names,emailAddresses,phoneNumbers,organizations", + ); } else if (pageToken) { params.set("pageToken", pageToken); } @@ -809,7 +886,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -824,7 +903,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getGoogleMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/hubspot/[transport]/route.ts b/app/api/mcps/hubspot/[transport]/route.ts index dcbb259a6..5cc8699c8 100644 --- a/app/api/mcps/hubspot/[transport]/route.ts +++ b/app/api/mcps/hubspot/[transport]/route.ts @@ -63,7 +63,9 @@ async function getHubSpotMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -71,18 +73,23 @@ async function getHubSpotMcpHandler() { mcpHandler = createMcpHandler( (server) => { // ==================== CONNECTION STATUS ==================== - server.tool("hubspot_status", "Check HubSpot OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const status = await getHubSpotStatus(orgId); - if (status.connected === false && !status.message) { - return jsonResult({ connected: false }); + server.tool( + "hubspot_status", + "Check HubSpot OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const status = await getHubSpotStatus(orgId); + if (status.connected === false && !status.message) { + return jsonResult({ connected: false }); + } + return jsonResult(status); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); } - return jsonResult(status); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + }, + ); // ==================== CONTACTS ==================== server.tool( @@ -152,7 +159,11 @@ async function getHubSpotMcpHandler() { if (lastname) properties.lastname = lastname; if (phone) properties.phone = phone; if (company) properties.company = company; - const contact = await createHubSpotObject(orgId, "contacts", properties); + const contact = await createHubSpotObject( + orgId, + "contacts", + properties, + ); logger.info("[HubSpotMCP] Contact created", { contactId: contact.id, }); @@ -173,7 +184,12 @@ async function getHubSpotMcpHandler() { async ({ contactId, properties }) => { try { const orgId = getOrgId(); - const contact = await updateHubSpotObject(orgId, "contacts", contactId, properties); + const contact = await updateHubSpotObject( + orgId, + "contacts", + contactId, + properties, + ); logger.info("[HubSpotMCP] Contact updated", { contactId }); return jsonResult({ success: true, contact }); } catch (e) { @@ -253,7 +269,11 @@ async function getHubSpotMcpHandler() { if (domain) properties.domain = domain; if (industry) properties.industry = industry; if (phone) properties.phone = phone; - const company = await createHubSpotObject(orgId, "companies", properties); + const company = await createHubSpotObject( + orgId, + "companies", + properties, + ); logger.info("[HubSpotMCP] Company created", { companyId: company.id, }); @@ -326,7 +346,10 @@ async function getHubSpotMcpHandler() { dealname: z.string().describe("Deal name"), amount: z.number().optional().describe("Deal amount"), dealstage: z.string().optional().describe("Deal stage"), - closedate: z.string().optional().describe("Expected close date (ISO 8601)"), + closedate: z + .string() + .optional() + .describe("Expected close date (ISO 8601)"), }, async ({ dealname, amount, dealstage, closedate }) => { try { @@ -397,9 +420,13 @@ async function getHubSpotMcpHandler() { "hubspot_associate", "Associate two HubSpot objects (e.g., link contact to company)", { - fromObjectType: z.enum(["contacts", "companies", "deals"]).describe("Source object type"), + fromObjectType: z + .enum(["contacts", "companies", "deals"]) + .describe("Source object type"), fromObjectId: z.string().describe("Source object ID"), - toObjectType: z.enum(["contacts", "companies", "deals"]).describe("Target object type"), + toObjectType: z + .enum(["contacts", "companies", "deals"]) + .describe("Target object type"), toObjectId: z.string().describe("Target object ID"), }, async ({ fromObjectType, fromObjectId, toObjectType, toObjectId }) => { @@ -440,7 +467,9 @@ async function handleRequest(req: NextRequest): Promise { if (rateLimited) return rateLimited; const handler = await getHubSpotMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/jira/[transport]/route.ts b/app/api/mcps/jira/[transport]/route.ts index 467311b9e..6347dfb1e 100644 --- a/app/api/mcps/jira/[transport]/route.ts +++ b/app/api/mcps/jira/[transport]/route.ts @@ -57,15 +57,26 @@ async function getJiraMcpHandler() { const cloudIdCache = new Map(); const CLOUD_ID_TTL_MS = 30 * 60 * 1000; - async function getCloudId(token: string, organizationId: string): Promise { + async function getCloudId( + token: string, + organizationId: string, + ): Promise { const cached = cloudIdCache.get(organizationId); if (cached && cached.expiresAt > Date.now()) return cached.id; - const response = await fetch("https://api.atlassian.com/oauth/token/accessible-resources", { - headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }, - }); + const response = await fetch( + "https://api.atlassian.com/oauth/token/accessible-resources", + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + }, + ); if (!response.ok) { - throw new Error(`Failed to get Jira accessible resources: ${response.status}`); + throw new Error( + `Failed to get Jira accessible resources: ${response.status}`, + ); } const resources = await response.json(); if (!Array.isArray(resources) || resources.length === 0) { @@ -75,11 +86,19 @@ async function getJiraMcpHandler() { } const cloudId = resources[0].id; - cloudIdCache.set(organizationId, { id: cloudId, expiresAt: Date.now() + CLOUD_ID_TTL_MS }); + cloudIdCache.set(organizationId, { + id: cloudId, + expiresAt: Date.now() + CLOUD_ID_TTL_MS, + }); return cloudId; } - async function jiraApi(orgId: string, method: string, path: string, body?: unknown) { + async function jiraApi( + orgId: string, + method: string, + path: string, + body?: unknown, + ) { const token = await getJiraToken(orgId); const cloudId = await getCloudId(token, orgId); const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3${path}`; @@ -165,31 +184,42 @@ async function getJiraMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } mcpHandler = createMcpHandler( (server) => { - server.tool("jira_status", "Check Jira OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "jira", - }); - const active = connections.find((c) => c.status === "active"); - return jsonResult( - active - ? { connected: true, email: active.email, scopes: active.scopes } - : { connected: false }, - ); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "jira_status", + "Check Jira OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "jira", + }); + const active = connections.find((c) => c.status === "active"); + return jsonResult( + active + ? { + connected: true, + email: active.email, + scopes: active.scopes, + } + : { connected: false }, + ); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); server.tool( "jira_search_issues", @@ -203,7 +233,12 @@ async function getJiraMcpHandler() { .max(100) .optional() .describe("Max results (default 50)"), - startAt: z.number().int().min(0).optional().describe("Pagination offset"), + startAt: z + .number() + .int() + .min(0) + .optional() + .describe("Pagination offset"), fields: z .array(z.string()) .optional() @@ -223,7 +258,11 @@ async function getJiraMcpHandler() { "fields", "summary,status,assignee,priority,issuetype,created,updated,project", ); - const data = await jiraApi(orgId, "GET", `/search?${params.toString()}`); + const data = await jiraApi( + orgId, + "GET", + `/search?${params.toString()}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -242,7 +281,11 @@ async function getJiraMcpHandler() { try { const orgId = getOrgId(); const params = fields ? `?fields=${fields.join(",")}` : ""; - const data = await jiraApi(orgId, "GET", `/issue/${issueKey}${params}`); + const data = await jiraApi( + orgId, + "GET", + `/issue/${issueKey}${params}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -259,15 +302,26 @@ async function getJiraMcpHandler() { issueType: z .string() .optional() - .describe("Issue type (default: Task). Common: Bug, Story, Task, Epic"), + .describe( + "Issue type (default: Task). Common: Bug, Story, Task, Epic", + ), description: z .string() .optional() .describe("Issue description (plain text, converted to ADF)"), - assigneeAccountId: z.string().optional().describe("Assignee account ID"), - priority: z.string().optional().describe("Priority name (e.g., High, Medium, Low)"), + assigneeAccountId: z + .string() + .optional() + .describe("Assignee account ID"), + priority: z + .string() + .optional() + .describe("Priority name (e.g., High, Medium, Low)"), labels: z.array(z.string()).optional().describe("Labels to add"), - parentKey: z.string().optional().describe("Parent issue key for subtasks"), + parentKey: z + .string() + .optional() + .describe("Parent issue key for subtasks"), }, async ({ projectKey, @@ -287,7 +341,8 @@ async function getJiraMcpHandler() { issuetype: { name: issueType || "Task" }, }; if (description) fields.description = textToAdf(description); - if (assigneeAccountId) fields.assignee = { accountId: assigneeAccountId }; + if (assigneeAccountId) + fields.assignee = { accountId: assigneeAccountId }; if (priority) fields.priority = { name: priority }; if (labels) fields.labels = labels; if (parentKey) fields.parent = { key: parentKey }; @@ -309,20 +364,36 @@ async function getJiraMcpHandler() { .string() .optional() .describe("New description (plain text, converted to ADF)"), - assigneeAccountId: z.string().optional().describe("New assignee account ID"), + assigneeAccountId: z + .string() + .optional() + .describe("New assignee account ID"), priority: z.string().optional().describe("New priority name"), - labels: z.array(z.string()).optional().describe("New labels (replaces existing)"), + labels: z + .array(z.string()) + .optional() + .describe("New labels (replaces existing)"), }, - async ({ issueKey, summary, description, assigneeAccountId, priority, labels }) => { + async ({ + issueKey, + summary, + description, + assigneeAccountId, + priority, + labels, + }) => { try { const orgId = getOrgId(); const fields: Record = {}; if (summary) fields.summary = summary; if (description) fields.description = textToAdf(description); - if (assigneeAccountId) fields.assignee = { accountId: assigneeAccountId }; + if (assigneeAccountId) + fields.assignee = { accountId: assigneeAccountId }; if (priority) fields.priority = { name: priority }; if (labels) fields.labels = labels; - const data = await jiraApi(orgId, "PUT", `/issue/${issueKey}`, { fields }); + const data = await jiraApi(orgId, "PUT", `/issue/${issueKey}`, { + fields, + }); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -335,14 +406,22 @@ async function getJiraMcpHandler() { "Add a comment to a Jira issue", { issueKey: z.string().min(1).describe("Issue key (e.g., PROJ-123)"), - body: z.string().min(1).describe("Comment text (plain text, converted to ADF)"), + body: z + .string() + .min(1) + .describe("Comment text (plain text, converted to ADF)"), }, async ({ issueKey, body }) => { try { const orgId = getOrgId(); - const data = await jiraApi(orgId, "POST", `/issue/${issueKey}/comment`, { - body: textToAdf(body), - }); + const data = await jiraApi( + orgId, + "POST", + `/issue/${issueKey}/comment`, + { + body: textToAdf(body), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -365,7 +444,11 @@ async function getJiraMcpHandler() { if (maxResults) params.set("maxResults", String(maxResults)); if (startAt) params.set("startAt", String(startAt)); const qs = params.toString() ? `?${params.toString()}` : ""; - const data = await jiraApi(orgId, "GET", `/issue/${issueKey}/comment${qs}`); + const data = await jiraApi( + orgId, + "GET", + `/issue/${issueKey}/comment${qs}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -421,7 +504,11 @@ async function getJiraMcpHandler() { async ({ issueKey }) => { try { const orgId = getOrgId(); - const data = await jiraApi(orgId, "GET", `/issue/${issueKey}/transitions`); + const data = await jiraApi( + orgId, + "GET", + `/issue/${issueKey}/transitions`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -434,8 +521,14 @@ async function getJiraMcpHandler() { "Transition a Jira issue to a new status (use jira_get_transitions to find valid transition IDs)", { issueKey: z.string().min(1).describe("Issue key (e.g., PROJ-123)"), - transitionId: z.string().min(1).describe("Transition ID (from jira_get_transitions)"), - comment: z.string().optional().describe("Optional comment to add with the transition"), + transitionId: z + .string() + .min(1) + .describe("Transition ID (from jira_get_transitions)"), + comment: z + .string() + .optional() + .describe("Optional comment to add with the transition"), }, async ({ issueKey, transitionId, comment }) => { try { @@ -448,7 +541,12 @@ async function getJiraMcpHandler() { comment: [{ add: { body: textToAdf(comment) } }], }; } - const data = await jiraApi(orgId, "POST", `/issue/${issueKey}/transitions`, body); + const data = await jiraApi( + orgId, + "POST", + `/issue/${issueKey}/transitions`, + body, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -461,14 +559,21 @@ async function getJiraMcpHandler() { "Assign a Jira issue to a user", { issueKey: z.string().min(1).describe("Issue key (e.g., PROJ-123)"), - accountId: z.string().describe("Assignee account ID (empty string to unassign)"), + accountId: z + .string() + .describe("Assignee account ID (empty string to unassign)"), }, async ({ issueKey, accountId }) => { try { const orgId = getOrgId(); - const data = await jiraApi(orgId, "PUT", `/issue/${issueKey}/assignee`, { - accountId: accountId || null, - }); + const data = await jiraApi( + orgId, + "PUT", + `/issue/${issueKey}/assignee`, + { + accountId: accountId || null, + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -488,7 +593,11 @@ async function getJiraMcpHandler() { const orgId = getOrgId(); const params = new URLSearchParams({ query }); if (maxResults) params.set("maxResults", String(maxResults)); - const data = await jiraApi(orgId, "GET", `/user/search?${params.toString()}`); + const data = await jiraApi( + orgId, + "GET", + `/user/search?${params.toString()}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -496,18 +605,27 @@ async function getJiraMcpHandler() { }, ); - server.tool("jira_get_myself", "Get the currently authenticated Jira user", {}, async () => { - try { - const orgId = getOrgId(); - const data = await jiraApi(orgId, "GET", "/myself"); - return jsonResult(data); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "jira_get_myself", + "Get the currently authenticated Jira user", + {}, + async () => { + try { + const orgId = getOrgId(); + const data = await jiraApi(orgId, "GET", "/myself"); + return jsonResult(data); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); }, { capabilities: { tools: {} } }, - { streamableHttpEndpoint: "/api/mcps/jira/streamable-http", disableSse: true, maxDuration: 60 }, + { + streamableHttpEndpoint: "/api/mcps/jira/streamable-http", + disableSse: true, + maxDuration: 60, + }, ); return mcpHandler; @@ -520,7 +638,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -535,7 +655,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getJiraMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/linear/[transport]/route.ts b/app/api/mcps/linear/[transport]/route.ts index fa273ddde..79edd313b 100644 --- a/app/api/mcps/linear/[transport]/route.ts +++ b/app/api/mcps/linear/[transport]/route.ts @@ -44,7 +44,11 @@ async function getLinearMcpHandler() { return result.accessToken; } - async function linearGraphQL(orgId: string, query: string, variables?: Record) { + async function linearGraphQL( + orgId: string, + query: string, + variables?: Record, + ) { const token = await getLinearToken(orgId); const response = await fetch("https://api.linear.app/graphql", { method: "POST", @@ -66,11 +70,14 @@ async function getLinearMcpHandler() { try { data = JSON.parse(text); } catch { - throw new Error(`Linear API returned invalid JSON: ${text.slice(0, 200)}`); + throw new Error( + `Linear API returned invalid JSON: ${text.slice(0, 200)}`, + ); } if (data?.errors?.length) { throw new Error( - data.errors.map((e: { message: string }) => e.message).join("; ") || "Linear API error", + data.errors.map((e: { message: string }) => e.message).join("; ") || + "Linear API error", ); } return data?.data; @@ -94,38 +101,50 @@ async function getLinearMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } return createMcpHandler( (server) => { - server.tool("linear_status", "Check Linear OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "linear", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) { - const expired = connections.find((c) => c.status === "expired"); - if (expired) { - return jsonResult({ - connected: false, - status: "expired", - message: "Linear connection expired. Please reconnect in Settings > Connections.", - }); + server.tool( + "linear_status", + "Check Linear OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "linear", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) { + const expired = connections.find((c) => c.status === "expired"); + if (expired) { + return jsonResult({ + connected: false, + status: "expired", + message: + "Linear connection expired. Please reconnect in Settings > Connections.", + }); + } + return jsonResult({ connected: false }); } - return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); } - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + }, + ); server.tool( "linear_list_issues", @@ -431,7 +450,10 @@ async function getLinearMcpHandler() { server.tool( "linear_list_teams", "List teams", - { first: z.number().int().min(1).max(100).optional(), after: z.string().optional() }, + { + first: z.number().int().min(1).max(100).optional(), + after: z.string().optional(), + }, async ({ first, after }) => { try { const orgId = getOrgId(); @@ -688,7 +710,10 @@ async function getLinearMcpHandler() { server.tool( "linear_list_users", "List workspace users", - { first: z.number().int().min(1).max(100).optional(), after: z.string().optional() }, + { + first: z.number().int().min(1).max(100).optional(), + after: z.string().optional(), + }, async ({ first, after }) => { try { const orgId = getOrgId(); @@ -806,7 +831,11 @@ async function getLinearMcpHandler() { server.tool( "linear_create_attachment", "Create attachment", - { issueId: z.string().min(1), url: z.string().min(1), title: z.string().optional() }, + { + issueId: z.string().min(1), + url: z.string().min(1), + title: z.string().optional(), + }, async ({ issueId, url, title }) => { try { const orgId = getOrgId(); @@ -864,7 +893,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -879,7 +910,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getLinearMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/linkedin/[transport]/route.ts b/app/api/mcps/linkedin/[transport]/route.ts index 1caa12377..0bcda1964 100644 --- a/app/api/mcps/linkedin/[transport]/route.ts +++ b/app/api/mcps/linkedin/[transport]/route.ts @@ -66,7 +66,11 @@ async function getLinkedInMcpHandler() { return ctx.user; } - async function linkedinFetch(orgId: string, path: string, options: RequestInit = {}) { + async function linkedinFetch( + orgId: string, + path: string, + options: RequestInit = {}, + ) { const token = await getLinkedInToken(orgId); const url = path.startsWith("http") ? path : `${LINKEDIN_REST_BASE}${path}`; @@ -84,7 +88,9 @@ async function getLinkedInMcpHandler() { if (!response.ok) { const error = await response.json().catch(() => ({})); const msg = - error?.message || error?.serviceErrorCode || `LinkedIn API error: ${response.status}`; + error?.message || + error?.serviceErrorCode || + `LinkedIn API error: ${response.status}`; throw new Error(msg); } @@ -101,7 +107,8 @@ async function getLinkedInMcpHandler() { const response = await fetch(LINKEDIN_USERINFO_URL, { headers: { Authorization: `Bearer ${token}` }, }); - if (!response.ok) throw new Error(`LinkedIn userinfo error: ${response.status}`); + if (!response.ok) + throw new Error(`LinkedIn userinfo error: ${response.status}`); return response.json(); } @@ -111,7 +118,9 @@ async function getLinkedInMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -119,21 +128,30 @@ async function getLinkedInMcpHandler() { mcpHandler = createMcpHandler( (server) => { // --- Connection status --- - server.tool("linkedin_status", "Check LinkedIn OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "linkedin", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) return jsonResult({ connected: false }); - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "linkedin_status", + "Check LinkedIn OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "linkedin", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); // --- Get current user profile --- server.tool( @@ -155,7 +173,9 @@ async function getLinkedInMcpHandler() { emailVerified: data.email_verified, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get profile"); + return errorResult( + e instanceof Error ? e.message : "Failed to get profile", + ); } }, ); @@ -206,7 +226,9 @@ async function getLinkedInMcpHandler() { visibility, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to create post"); + return errorResult( + e instanceof Error ? e.message : "Failed to create post", + ); } }, ); @@ -221,7 +243,9 @@ async function getLinkedInMcpHandler() { { postUrn: z .string() - .describe("The post URN to delete (e.g., urn:li:share:12345 or urn:li:ugcPost:12345)"), + .describe( + "The post URN to delete (e.g., urn:li:share:12345 or urn:li:ugcPost:12345)", + ), }, async ({ postUrn }) => { try { @@ -235,7 +259,9 @@ async function getLinkedInMcpHandler() { return jsonResult({ success: true, deleted: postUrn }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to delete post"); + return errorResult( + e instanceof Error ? e.message : "Failed to delete post", + ); } }, ); @@ -258,7 +284,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -273,7 +301,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getLinkedInMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/microsoft/[transport]/route.ts b/app/api/mcps/microsoft/[transport]/route.ts index b424d63ba..fcdb4f398 100644 --- a/app/api/mcps/microsoft/[transport]/route.ts +++ b/app/api/mcps/microsoft/[transport]/route.ts @@ -60,7 +60,9 @@ async function getMicrosoftMcpHandler() { }); if (!response.ok && response.status !== 204) { const error = await response.json().catch(() => ({})); - throw new Error(error.error?.message || `Microsoft Graph API error: ${response.status}`); + throw new Error( + error.error?.message || `Microsoft Graph API error: ${response.status}`, + ); } return response; } @@ -83,37 +85,50 @@ async function getMicrosoftMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } mcpHandler = createMcpHandler( (server) => { - server.tool("microsoft_status", "Check Microsoft OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "microsoft", - }); - const active = connections.find((c) => c.status === "active"); - return jsonResult( - active - ? { connected: true, email: active.email, scopes: active.scopes } - : { connected: false }, - ); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "microsoft_status", + "Check Microsoft OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "microsoft", + }); + const active = connections.find((c) => c.status === "active"); + return jsonResult( + active + ? { + connected: true, + email: active.email, + scopes: active.scopes, + } + : { connected: false }, + ); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); server.tool( "outlook_send", "Send email via Outlook", { - to: z.string().describe("Recipient(s) - comma separated for multiple"), + to: z + .string() + .describe("Recipient(s) - comma separated for multiple"), subject: z.string().describe("Email subject"), body: z.string().describe("Email body"), isHtml: z.boolean().optional().default(false), @@ -132,21 +147,32 @@ async function getMicrosoftMcpHandler() { ...(cc && { ccRecipients: cc .split(",") - .map((email) => ({ emailAddress: { address: email.trim() } })), + .map((email) => ({ + emailAddress: { address: email.trim() }, + })), }), ...(bcc && { bccRecipients: bcc .split(",") - .map((email) => ({ emailAddress: { address: email.trim() } })), + .map((email) => ({ + emailAddress: { address: email.trim() }, + })), }), }; - const _res = await graphFetch(orgId, "https://graph.microsoft.com/v1.0/me/sendMail", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message }), - }); + const _res = await graphFetch( + orgId, + "https://graph.microsoft.com/v1.0/me/sendMail", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message }), + }, + ); logger.info("[MicrosoftMCP] Email sent", { to }); - return jsonResult({ success: true, message: "Email sent successfully" }); + return jsonResult({ + success: true, + message: "Email sent successfully", + }); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); } @@ -186,15 +212,21 @@ async function getMicrosoftMcpHandler() { `https://graph.microsoft.com/v1.0/me/mailFolders/${folder}/messages?${params}`, ); const data = await res.json(); - const messages = (data.value || []).map((m: Record) => ({ - id: m.id, - subject: m.subject, - from: (m.from as Record)?.emailAddress, - receivedDateTime: m.receivedDateTime, - preview: m.bodyPreview, - isRead: m.isRead, - })); - return jsonResult({ success: true, messages, count: messages.length }); + const messages = (data.value || []).map( + (m: Record) => ({ + id: m.id, + subject: m.subject, + from: (m.from as Record)?.emailAddress, + receivedDateTime: m.receivedDateTime, + preview: m.bodyPreview, + isRead: m.isRead, + }), + ); + return jsonResult({ + success: true, + messages, + count: messages.length, + }); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); } @@ -220,7 +252,9 @@ async function getMicrosoftMcpHandler() { id: msg.id, subject: msg.subject, from: msg.from?.emailAddress, - to: msg.toRecipients?.map((r: Record) => r.emailAddress), + to: msg.toRecipients?.map( + (r: Record) => r.emailAddress, + ), receivedDateTime: msg.receivedDateTime, body: msg.body?.content, bodyType: msg.body?.contentType, @@ -236,7 +270,10 @@ async function getMicrosoftMcpHandler() { "List upcoming calendar events", { top: z.number().int().min(1).max(50).optional().default(10), - startDateTime: z.string().optional().describe("Start time (ISO 8601)"), + startDateTime: z + .string() + .optional() + .describe("Start time (ISO 8601)"), endDateTime: z.string().optional().describe("End time (ISO 8601)"), }, async ({ top = 10, startDateTime, endDateTime }) => { @@ -249,22 +286,26 @@ async function getMicrosoftMcpHandler() { $orderby: "start/dateTime", startDateTime: startDateTime || now, endDateTime: - endDateTime || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + endDateTime || + new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), }); const res = await graphFetch( orgId, `https://graph.microsoft.com/v1.0/me/calendarView?${params}`, ); const data = await res.json(); - const events = (data.value || []).map((e: Record) => ({ - id: e.id, - subject: e.subject, - start: (e.start as Record)?.dateTime, - end: (e.end as Record)?.dateTime, - location: (e.location as Record)?.displayName, - organizer: (e.organizer as Record>)?.emailAddress - ?.address, - })); + const events = (data.value || []).map( + (e: Record) => ({ + id: e.id, + subject: e.subject, + start: (e.start as Record)?.dateTime, + end: (e.end as Record)?.dateTime, + location: (e.location as Record)?.displayName, + organizer: ( + e.organizer as Record> + )?.emailAddress?.address, + }), + ); return jsonResult({ success: true, events, count: events.length }); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -281,7 +322,10 @@ async function getMicrosoftMcpHandler() { end: z.string().describe("End time (ISO 8601)"), body: z.string().optional().describe("Event description"), location: z.string().optional().describe("Event location"), - attendees: z.string().optional().describe("Attendee emails (comma separated)"), + attendees: z + .string() + .optional() + .describe("Attendee emails (comma separated)"), }, async ({ subject, start, end, body, location, attendees }) => { try { @@ -299,14 +343,22 @@ async function getMicrosoftMcpHandler() { })), }), }; - const res = await graphFetch(orgId, "https://graph.microsoft.com/v1.0/me/events", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(event), - }); + const res = await graphFetch( + orgId, + "https://graph.microsoft.com/v1.0/me/events", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(event), + }, + ); const result = await res.json(); logger.info("[MicrosoftMCP] Event created", { eventId: result.id }); - return jsonResult({ success: true, eventId: result.id, webLink: result.webLink }); + return jsonResult({ + success: true, + eventId: result.id, + webLink: result.webLink, + }); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); } @@ -322,9 +374,13 @@ async function getMicrosoftMcpHandler() { async ({ eventId }) => { try { const orgId = getOrgId(); - await graphFetch(orgId, `https://graph.microsoft.com/v1.0/me/events/${eventId}`, { - method: "DELETE", - }); + await graphFetch( + orgId, + `https://graph.microsoft.com/v1.0/me/events/${eventId}`, + { + method: "DELETE", + }, + ); logger.info("[MicrosoftMCP] Event deleted", { eventId }); return jsonResult({ success: true, message: "Event deleted" }); } catch (e) { @@ -336,7 +392,10 @@ async function getMicrosoftMcpHandler() { server.tool("user_profile", "Get current user profile", {}, async () => { try { const orgId = getOrgId(); - const res = await graphFetch(orgId, "https://graph.microsoft.com/v1.0/me"); + const res = await graphFetch( + orgId, + "https://graph.microsoft.com/v1.0/me", + ); const profile = await res.json(); return jsonResult({ success: true, @@ -369,7 +428,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -384,7 +445,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getMicrosoftMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/notion/[transport]/route.ts b/app/api/mcps/notion/[transport]/route.ts index 279ad9140..7a5b1ca47 100644 --- a/app/api/mcps/notion/[transport]/route.ts +++ b/app/api/mcps/notion/[transport]/route.ts @@ -44,7 +44,11 @@ async function getNotionMcpHandler() { return result.accessToken; } - async function notionFetch(orgId: string, endpoint: string, options: RequestInit = {}) { + async function notionFetch( + orgId: string, + endpoint: string, + options: RequestInit = {}, + ) { const token = await getNotionToken(orgId); const response = await fetch(`https://api.notion.com${endpoint}`, { ...options, @@ -85,38 +89,50 @@ async function getNotionMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } return createMcpHandler( (server) => { - server.tool("notion_status", "Check Notion OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "notion", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) { - const expired = connections.find((c) => c.status === "expired"); - if (expired) { - return jsonResult({ - connected: false, - status: "expired", - message: "Notion connection expired. Please reconnect in Settings > Connections.", - }); + server.tool( + "notion_status", + "Check Notion OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "notion", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) { + const expired = connections.find((c) => c.status === "expired"); + if (expired) { + return jsonResult({ + connected: false, + status: "expired", + message: + "Notion connection expired. Please reconnect in Settings > Connections.", + }); + } + return jsonResult({ connected: false }); } - return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); } - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + }, + ); server.tool( "notion_search", @@ -133,7 +149,13 @@ async function getNotionMcpHandler() { const orgId = getOrgId(); const data = await notionFetch(orgId, "/v1/search", { method: "POST", - body: JSON.stringify({ query, filter, sort, start_cursor, page_size }), + body: JSON.stringify({ + query, + filter, + sort, + start_cursor, + page_size, + }), }); return jsonResult(data); } catch (e) { @@ -241,7 +263,10 @@ async function getNotionMcpHandler() { if (start_cursor) params.set("start_cursor", start_cursor); if (page_size) params.set("page_size", String(page_size)); const suffix = params.toString() ? `?${params.toString()}` : ""; - const data = await notionFetch(orgId, `/v1/blocks/${id}/children${suffix}`); + const data = await notionFetch( + orgId, + `/v1/blocks/${id}/children${suffix}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -292,7 +317,9 @@ async function getNotionMcpHandler() { async ({ id }) => { try { const orgId = getOrgId(); - const data = await notionFetch(orgId, `/v1/blocks/${id}`, { method: "DELETE" }); + const data = await notionFetch(orgId, `/v1/blocks/${id}`, { + method: "DELETE", + }); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -373,10 +400,14 @@ async function getNotionMcpHandler() { async ({ id, body }) => { try { const orgId = getOrgId(); - const data = await notionFetch(orgId, `/v1/data_sources/${id}/query`, { - method: "POST", - body: JSON.stringify(body || {}), - }); + const data = await notionFetch( + orgId, + `/v1/data_sources/${id}/query`, + { + method: "POST", + body: JSON.stringify(body || {}), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -391,10 +422,14 @@ async function getNotionMcpHandler() { async ({ id, body }) => { try { const orgId = getOrgId(); - const data = await notionFetch(orgId, `/v1/data_sources/${id}/properties`, { - method: "PATCH", - body: JSON.stringify(body), - }); + const data = await notionFetch( + orgId, + `/v1/data_sources/${id}/properties`, + { + method: "PATCH", + body: JSON.stringify(body), + }, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -453,7 +488,10 @@ async function getNotionMcpHandler() { const params = new URLSearchParams({ block_id }); if (start_cursor) params.set("start_cursor", start_cursor); if (page_size) params.set("page_size", String(page_size)); - const data = await notionFetch(orgId, `/v1/comments?${params.toString()}`); + const data = await notionFetch( + orgId, + `/v1/comments?${params.toString()}`, + ); return jsonResult(data); } catch (e) { return errorResult(e instanceof Error ? e.message : "Failed"); @@ -495,7 +533,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -510,7 +550,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getNotionMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/salesforce/[transport]/route.ts b/app/api/mcps/salesforce/[transport]/route.ts index df29a318f..939beaded 100644 --- a/app/api/mcps/salesforce/[transport]/route.ts +++ b/app/api/mcps/salesforce/[transport]/route.ts @@ -61,16 +61,24 @@ async function getSalesforceMcpHandler() { * Resolve the Salesforce instance URL for an org. * Calls the userinfo endpoint and extracts the custom_domain or profile base URL. */ - async function resolveInstanceUrl(token: string, orgId: string): Promise { + async function resolveInstanceUrl( + token: string, + orgId: string, + ): Promise { const cached = instanceUrlCache.get(orgId); if (cached && cached.expiresAt > Date.now()) return cached.url; - const res = await fetch("https://login.salesforce.com/services/oauth2/userinfo", { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await fetch( + "https://login.salesforce.com/services/oauth2/userinfo", + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); if (!res.ok) { - throw new Error(`Failed to resolve Salesforce instance URL: ${res.status}`); + throw new Error( + `Failed to resolve Salesforce instance URL: ${res.status}`, + ); } const data = await res.json(); @@ -85,17 +93,26 @@ async function getSalesforceMcpHandler() { } if (!instanceUrl) { - throw new Error("Could not determine Salesforce instance URL from userinfo response"); + throw new Error( + "Could not determine Salesforce instance URL from userinfo response", + ); } // Remove trailing slash instanceUrl = instanceUrl.replace(/\/$/, ""); - instanceUrlCache.set(orgId, { url: instanceUrl, expiresAt: Date.now() + INSTANCE_URL_TTL_MS }); + instanceUrlCache.set(orgId, { + url: instanceUrl, + expiresAt: Date.now() + INSTANCE_URL_TTL_MS, + }); return instanceUrl; } - async function salesforceFetch(orgId: string, path: string, options: RequestInit = {}) { + async function salesforceFetch( + orgId: string, + path: string, + options: RequestInit = {}, + ) { const token = await getSalesforceToken(orgId); const instanceUrl = await resolveInstanceUrl(token, orgId); const url = `${instanceUrl}${path}`; @@ -142,7 +159,9 @@ async function getSalesforceMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -150,21 +169,30 @@ async function getSalesforceMcpHandler() { mcpHandler = createMcpHandler( (server) => { // --- Connection status --- - server.tool("salesforce_status", "Check Salesforce OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "salesforce", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) return jsonResult({ connected: false }); - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "salesforce_status", + "Check Salesforce OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "salesforce", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); // --- SOQL Query (most flexible tool) --- server.tool( @@ -195,7 +223,9 @@ async function getSalesforceMcpHandler() { nextRecordsUrl: data.nextRecordsUrl, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to execute query"); + return errorResult( + e instanceof Error ? e.message : "Failed to execute query", + ); } }, ); @@ -224,7 +254,9 @@ async function getSalesforceMcpHandler() { nextRecordsUrl: data.nextRecordsUrl, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to fetch next page"); + return errorResult( + e instanceof Error ? e.message : "Failed to fetch next page", + ); } }, ); @@ -250,7 +282,9 @@ async function getSalesforceMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to search"); + return errorResult( + e instanceof Error ? e.message : "Failed to search", + ); } }, ); @@ -267,18 +301,22 @@ async function getSalesforceMcpHandler() { orgId, `/services/data/${SALESFORCE_API_VERSION}/sobjects`, ); - const objects = data.sobjects?.map((obj: Record) => ({ - name: obj.name, - label: obj.label, - queryable: obj.queryable, - createable: obj.createable, - updateable: obj.updateable, - deletable: obj.deletable, - custom: obj.custom, - })); + const objects = data.sobjects?.map( + (obj: Record) => ({ + name: obj.name, + label: obj.label, + queryable: obj.queryable, + createable: obj.createable, + updateable: obj.updateable, + deletable: obj.deletable, + custom: obj.custom, + }), + ); return jsonResult({ objects, count: objects?.length }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to list objects"); + return errorResult( + e instanceof Error ? e.message : "Failed to list objects", + ); } }, ); @@ -315,7 +353,10 @@ async function getSalesforceMcpHandler() { .filter((v: Record) => v.active) .map((v: Record) => v.value) : undefined, - referenceTo: (f.referenceTo as string[])?.length > 0 ? f.referenceTo : undefined, + referenceTo: + (f.referenceTo as string[])?.length > 0 + ? f.referenceTo + : undefined, })); return jsonResult({ name: data.name, @@ -326,7 +367,9 @@ async function getSalesforceMcpHandler() { ), }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to describe object"); + return errorResult( + e instanceof Error ? e.message : "Failed to describe object", + ); } }, ); @@ -336,17 +379,27 @@ async function getSalesforceMcpHandler() { "salesforce_get_record", "Get a single Salesforce record by its ID. Optionally specify which fields to return.", { - objectName: z.string().min(1).describe("API name of the SObject, e.g. Account, Contact"), - recordId: z.string().min(1).describe("The 15 or 18-character Salesforce record ID"), + objectName: z + .string() + .min(1) + .describe("API name of the SObject, e.g. Account, Contact"), + recordId: z + .string() + .min(1) + .describe("The 15 or 18-character Salesforce record ID"), fields: z .array(z.string()) .optional() - .describe("Specific fields to return. If omitted, returns all accessible fields."), + .describe( + "Specific fields to return. If omitted, returns all accessible fields.", + ), }, async ({ objectName, recordId, fields }) => { try { const orgId = getOrgId(); - const fieldParam = fields?.length ? `?fields=${fields.join(",")}` : ""; + const fieldParam = fields?.length + ? `?fields=${fields.join(",")}` + : ""; const data = await salesforceFetch( orgId, `/services/data/${SALESFORCE_API_VERSION}/sobjects/${encodeURIComponent(objectName)}/${recordId}${fieldParam}`, @@ -354,7 +407,9 @@ async function getSalesforceMcpHandler() { const { attributes, ...record } = data; return jsonResult(record); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get record"); + return errorResult( + e instanceof Error ? e.message : "Failed to get record", + ); } }, ); @@ -387,7 +442,9 @@ async function getSalesforceMcpHandler() { ); return jsonResult({ success: data.success, id: data.id }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to create record"); + return errorResult( + e instanceof Error ? e.message : "Failed to create record", + ); } }, ); @@ -397,11 +454,16 @@ async function getSalesforceMcpHandler() { "salesforce_update_record", "Update fields on an existing Salesforce record. Only include the fields you want to change.", { - objectName: z.string().min(1).describe("API name of the SObject, e.g. Account, Contact"), + objectName: z + .string() + .min(1) + .describe("API name of the SObject, e.g. Account, Contact"), recordId: z.string().min(1).describe("The record ID to update"), fields: z .record(z.any()) - .describe("Fields to update, e.g. { Industry: 'Finance', Phone: '555-1234' }"), + .describe( + "Fields to update, e.g. { Industry: 'Finance', Phone: '555-1234' }", + ), }, async ({ objectName, recordId, fields }) => { try { @@ -416,7 +478,9 @@ async function getSalesforceMcpHandler() { ); return jsonResult({ success: true, id: recordId }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to update record"); + return errorResult( + e instanceof Error ? e.message : "Failed to update record", + ); } }, ); @@ -426,7 +490,10 @@ async function getSalesforceMcpHandler() { "salesforce_delete_record", "Delete a Salesforce record by its ID.", { - objectName: z.string().min(1).describe("API name of the SObject, e.g. Account, Contact"), + objectName: z + .string() + .min(1) + .describe("API name of the SObject, e.g. Account, Contact"), recordId: z.string().min(1).describe("The record ID to delete"), }, async ({ objectName, recordId }) => { @@ -441,7 +508,9 @@ async function getSalesforceMcpHandler() { ); return jsonResult({ success: true, deleted: recordId }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to delete record"); + return errorResult( + e instanceof Error ? e.message : "Failed to delete record", + ); } }, ); @@ -454,7 +523,9 @@ async function getSalesforceMcpHandler() { objectName: z .string() .min(1) - .describe("API name of the SObject, e.g. Account, Contact, Opportunity"), + .describe( + "API name of the SObject, e.g. Account, Contact, Opportunity", + ), limit: z .number() .int() @@ -472,7 +543,9 @@ async function getSalesforceMcpHandler() { ); return jsonResult(data); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get recent records"); + return errorResult( + e instanceof Error ? e.message : "Failed to get recent records", + ); } }, ); @@ -499,7 +572,9 @@ async function handleRequest(req: NextRequest): Promise { if (rateLimited) return rateLimited; const handler = await getSalesforceMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { @@ -528,7 +603,9 @@ async function withTransportValidation( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } diff --git a/app/api/mcps/time/[transport]/route.ts b/app/api/mcps/time/[transport]/route.ts index 2a5153a95..772a09d94 100644 --- a/app/api/mcps/time/[transport]/route.ts +++ b/app/api/mcps/time/[transport]/route.ts @@ -90,7 +90,9 @@ function createHandler() { .string() .optional() .default("UTC") - .describe("IANA timezone (e.g., 'America/New_York') or alias (e.g., 'PST', 'JST')"), + .describe( + "IANA timezone (e.g., 'America/New_York') or alias (e.g., 'PST', 'JST')", + ), format: z .enum(["iso", "unix", "readable", "all"]) .optional() @@ -174,11 +176,13 @@ function createHandler() { weekday: "long", }).format(now); result.dayOfYear = Math.floor( - (now.getTime() - new Date(now.getFullYear(), 0, 0).getTime()) / 86400000, + (now.getTime() - new Date(now.getFullYear(), 0, 0).getTime()) / + 86400000, ); result.weekNumber = Math.ceil((result.dayOfYear as number) / 7); result.isLeapYear = - (now.getFullYear() % 4 === 0 && now.getFullYear() % 100 !== 0) || + (now.getFullYear() % 4 === 0 && + now.getFullYear() % 100 !== 0) || now.getFullYear() % 400 === 0 ? 1 : 0; @@ -199,7 +203,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to get time", + error: + error instanceof Error + ? error.message + : "Failed to get time", }, null, 2, @@ -221,9 +228,15 @@ function createHandler() { { time: z .string() - .describe("Time to convert (ISO format, e.g., '2024-01-15T14:30:00' or 'now')"), - fromTimezone: z.string().describe("Source timezone (IANA format or alias)"), - toTimezone: z.string().describe("Target timezone (IANA format or alias)"), + .describe( + "Time to convert (ISO format, e.g., '2024-01-15T14:30:00' or 'now')", + ), + fromTimezone: z + .string() + .describe("Source timezone (IANA format or alias)"), + toTimezone: z + .string() + .describe("Target timezone (IANA format or alias)"), }, async ({ time, fromTimezone, toTimezone }) => { try { @@ -262,7 +275,8 @@ function createHandler() { }; } - const date = time.toLowerCase() === "now" ? new Date() : new Date(time); + const date = + time.toLowerCase() === "now" ? new Date() : new Date(time); if (isNaN(date.getTime())) { return { @@ -272,7 +286,8 @@ function createHandler() { text: JSON.stringify( { error: "Invalid time format", - suggestion: "Use ISO format (e.g., '2024-01-15T14:30:00') or 'now'", + suggestion: + "Use ISO format (e.g., '2024-01-15T14:30:00') or 'now'", }, null, 2, @@ -349,7 +364,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to convert timezone", + error: + error instanceof Error + ? error.message + : "Failed to convert timezone", }, null, 2, @@ -374,20 +392,31 @@ function createHandler() { .string() .optional() .default("en-US") - .describe("Locale for formatting (e.g., 'en-US', 'de-DE', 'ja-JP', 'zh-CN')"), - timezone: z.string().optional().default("UTC").describe("Timezone for display"), + .describe( + "Locale for formatting (e.g., 'en-US', 'de-DE', 'ja-JP', 'zh-CN')", + ), + timezone: z + .string() + .optional() + .default("UTC") + .describe("Timezone for display"), }, async ({ date, locale = "en-US", timezone = "UTC" }) => { try { const tz = resolveTimezone(timezone); - const dateObj = date.toLowerCase() === "now" ? new Date() : new Date(date); + const dateObj = + date.toLowerCase() === "now" ? new Date() : new Date(date); if (isNaN(dateObj.getTime())) { return { content: [ { type: "text" as const, - text: JSON.stringify({ error: "Invalid date format" }, null, 2), + text: JSON.stringify( + { error: "Invalid date format" }, + null, + 2, + ), }, ], isError: true, @@ -420,7 +449,9 @@ function createHandler() { // Relative time const now = new Date(); const diffMs = dateObj.getTime() - now.getTime(); - const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); + const rtf = new Intl.RelativeTimeFormat(locale, { + numeric: "auto", + }); let relativeTime: string; const diffDays = Math.round(diffMs / 86400000); @@ -470,7 +501,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to format date", + error: + error instanceof Error + ? error.message + : "Failed to format date", }, null, 2, @@ -490,20 +524,30 @@ function createHandler() { "calculate_time_diff", "Calculate the difference between two dates/times with detailed breakdown.", { - startDate: z.string().describe("Start date/time (ISO format or 'now')"), + startDate: z + .string() + .describe("Start date/time (ISO format or 'now')"), endDate: z.string().describe("End date/time (ISO format or 'now')"), }, async ({ startDate, endDate }) => { try { - const start = startDate.toLowerCase() === "now" ? new Date() : new Date(startDate); - const end = endDate.toLowerCase() === "now" ? new Date() : new Date(endDate); + const start = + startDate.toLowerCase() === "now" + ? new Date() + : new Date(startDate); + const end = + endDate.toLowerCase() === "now" ? new Date() : new Date(endDate); if (isNaN(start.getTime()) || isNaN(end.getTime())) { return { content: [ { type: "text" as const, - text: JSON.stringify({ error: "Invalid date format" }, null, 2), + text: JSON.stringify( + { error: "Invalid date format" }, + null, + 2, + ), }, ], isError: true, @@ -563,17 +607,28 @@ function createHandler() { // Build human-readable string const parts: string[] = []; if (breakdown.years) - parts.push(`${breakdown.years} year${breakdown.years !== 1 ? "s" : ""}`); + parts.push( + `${breakdown.years} year${breakdown.years !== 1 ? "s" : ""}`, + ); if (breakdown.months) - parts.push(`${breakdown.months} month${breakdown.months !== 1 ? "s" : ""}`); + parts.push( + `${breakdown.months} month${breakdown.months !== 1 ? "s" : ""}`, + ); if (breakdown.days) - parts.push(`${breakdown.days} day${breakdown.days !== 1 ? "s" : ""}`); + parts.push( + `${breakdown.days} day${breakdown.days !== 1 ? "s" : ""}`, + ); if (breakdown.hours) - parts.push(`${breakdown.hours} hour${breakdown.hours !== 1 ? "s" : ""}`); + parts.push( + `${breakdown.hours} hour${breakdown.hours !== 1 ? "s" : ""}`, + ); if (breakdown.minutes) - parts.push(`${breakdown.minutes} minute${breakdown.minutes !== 1 ? "s" : ""}`); + parts.push( + `${breakdown.minutes} minute${breakdown.minutes !== 1 ? "s" : ""}`, + ); - const humanReadable = parts.slice(0, 3).join(", ") || "less than a minute"; + const humanReadable = + parts.slice(0, 3).join(", ") || "less than a minute"; return { content: [ @@ -615,7 +670,9 @@ function createHandler() { text: JSON.stringify( { error: - error instanceof Error ? error.message : "Failed to calculate difference", + error instanceof Error + ? error.message + : "Failed to calculate difference", }, null, 2, @@ -638,7 +695,9 @@ function createHandler() { filter: z .string() .optional() - .describe("Filter timezones by region (e.g., 'America', 'Europe', 'Asia')"), + .describe( + "Filter timezones by region (e.g., 'America', 'Europe', 'Asia')", + ), }, async ({ filter }) => { try { @@ -647,7 +706,9 @@ function createHandler() { let timezones = COMMON_TIMEZONES; if (filter) { const filterLower = filter.toLowerCase(); - timezones = timezones.filter((tz) => tz.toLowerCase().includes(filterLower)); + timezones = timezones.filter((tz) => + tz.toLowerCase().includes(filterLower), + ); } const results = timezones.map((tz) => { @@ -701,7 +762,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to list timezones", + error: + error instanceof Error + ? error.message + : "Failed to list timezones", }, null, 2, diff --git a/app/api/mcps/twitter/[transport]/route.ts b/app/api/mcps/twitter/[transport]/route.ts index d1305865f..47e1dbda8 100644 --- a/app/api/mcps/twitter/[transport]/route.ts +++ b/app/api/mcps/twitter/[transport]/route.ts @@ -89,7 +89,9 @@ async function getTwitterMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -105,7 +107,9 @@ async function getTwitterMcpHandler() { if (err.data?.detail) parts.push(err.data.detail); if (err.code) parts.push(`code: ${err.code}`); if (err.rateLimit?.remaining === 0 && err.rateLimit?.reset) { - parts.push(`rate limit resets at ${new Date(err.rateLimit.reset * 1000).toISOString()}`); + parts.push( + `rate limit resets at ${new Date(err.rateLimit.reset * 1000).toISOString()}`, + ); } return parts.join(" — "); } @@ -131,32 +135,40 @@ async function getTwitterMcpHandler() { const oldestKey = userIdCache.keys().next().value; if (oldestKey) userIdCache.delete(oldestKey); } - userIdCache.set(orgId, { id: me.data.id, expiry: Date.now() + USER_ID_CACHE_TTL_MS }); + userIdCache.set(orgId, { + id: me.data.id, + expiry: Date.now() + USER_ID_CACHE_TTL_MS, + }); return me.data.id; } mcpHandler = createMcpHandler( (server) => { // --- Connection status --- - server.tool("twitter_status", "Check Twitter/X OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "twitter", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) return jsonResult({ connected: false }); - return jsonResult({ - connected: true, - username: active.displayName, - scopes: active.scopes, - }); - } catch (e) { - return errorResult(errMsg(e, "Failed")); - } - }); + server.tool( + "twitter_status", + "Check Twitter/X OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "twitter", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + username: active.displayName, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(errMsg(e, "Failed")); + } + }, + ); // --- Get authenticated user profile --- server.tool( @@ -201,7 +213,9 @@ async function getTwitterMcpHandler() { "twitter_get_user", "Get a Twitter/X user's profile by their username (handle)", { - username: z.string().describe("The Twitter username/handle (without @)"), + username: z + .string() + .describe("The Twitter username/handle (without @)"), }, async ({ username }) => { try { @@ -242,7 +256,11 @@ async function getTwitterMcpHandler() { "twitter_create_tweet", "Post a new tweet on Twitter/X. Supports text tweets and replies.", { - text: z.string().min(1).max(280).describe("The tweet text content (max 280 characters)"), + text: z + .string() + .min(1) + .max(280) + .describe("The tweet text content (max 280 characters)"), replyToTweetId: z .string() .regex(/^\d+$/) @@ -285,14 +303,21 @@ async function getTwitterMcpHandler() { "twitter_delete_tweet", "Delete a tweet by its ID. Only works for tweets by the authenticated user.", { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to delete"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to delete"), }, async ({ tweetId }) => { try { const orgId = getOrgId(); const client = await getTwitterClient(orgId); const result = await client.v2.deleteTweet(tweetId); - return jsonResult({ success: true, deleted: result.data.deleted, tweetId }); + return jsonResult({ + success: true, + deleted: result.data.deleted, + tweetId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to delete tweet")); } @@ -304,7 +329,10 @@ async function getTwitterMcpHandler() { "twitter_get_tweet", "Get a specific tweet by its ID with full details", { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to retrieve"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to retrieve"), }, async ({ tweetId }) => { try { @@ -345,7 +373,9 @@ async function getTwitterMcpHandler() { "twitter_search_tweets", "Search for recent tweets matching a query. Uses Twitter API v2 recent search (last 7 days).", { - query: z.string().describe("The search query (supports Twitter search operators)"), + query: z + .string() + .describe("The search query (supports Twitter search operators)"), maxResults: z .number() .min(10) @@ -359,7 +389,12 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const results = await client.v2.search(query, { max_results: maxResults, - "tweet.fields": ["created_at", "public_metrics", "author_id", "entities"], + "tweet.fields": [ + "created_at", + "public_metrics", + "author_id", + "entities", + ], expansions: ["author_id"], "user.fields": ["username", "name"], }); @@ -402,7 +437,12 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const timeline = await client.v2.userTimeline(userId, { max_results: maxResults, - "tweet.fields": ["created_at", "public_metrics", "entities", "referenced_tweets"], + "tweet.fields": [ + "created_at", + "public_metrics", + "entities", + "referenced_tweets", + ], }); const tweets = timeline.data?.data || []; @@ -428,7 +468,10 @@ async function getTwitterMcpHandler() { "twitter_like_tweet", "Like a tweet on behalf of the authenticated user", { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to like"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to like"), }, async ({ tweetId }) => { try { @@ -436,7 +479,11 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const userId = await getAuthenticatedUserId(client, orgId); const result = await client.v2.like(userId, tweetId); - return jsonResult({ success: true, liked: result.data.liked, tweetId }); + return jsonResult({ + success: true, + liked: result.data.liked, + tweetId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to like tweet")); } @@ -448,7 +495,10 @@ async function getTwitterMcpHandler() { "twitter_unlike_tweet", "Remove a like from a tweet", { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to unlike"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to unlike"), }, async ({ tweetId }) => { try { @@ -456,7 +506,11 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const userId = await getAuthenticatedUserId(client, orgId); const result = await client.v2.unlike(userId, tweetId); - return jsonResult({ success: true, liked: result.data.liked, tweetId }); + return jsonResult({ + success: true, + liked: result.data.liked, + tweetId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to unlike tweet")); } @@ -468,7 +522,10 @@ async function getTwitterMcpHandler() { "twitter_retweet", "Retweet a tweet on behalf of the authenticated user", { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to retweet"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to retweet"), }, async ({ tweetId }) => { try { @@ -476,7 +533,11 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const userId = await getAuthenticatedUserId(client, orgId); const result = await client.v2.retweet(userId, tweetId); - return jsonResult({ success: true, retweeted: result.data.retweeted, tweetId }); + return jsonResult({ + success: true, + retweeted: result.data.retweeted, + tweetId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to retweet")); } @@ -488,7 +549,10 @@ async function getTwitterMcpHandler() { "twitter_unretweet", "Remove a retweet from a tweet", { - tweetId: z.string().regex(/^\d+$/).describe("The ID of the tweet to unretweet"), + tweetId: z + .string() + .regex(/^\d+$/) + .describe("The ID of the tweet to unretweet"), }, async ({ tweetId }) => { try { @@ -496,7 +560,11 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const userId = await getAuthenticatedUserId(client, orgId); const result = await client.v2.unretweet(userId, tweetId); - return jsonResult({ success: true, retweeted: result.data.retweeted, tweetId }); + return jsonResult({ + success: true, + retweeted: result.data.retweeted, + tweetId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to unretweet")); } @@ -508,7 +576,10 @@ async function getTwitterMcpHandler() { "twitter_get_followers", "Get a list of users who follow the specified user", { - userId: z.string().regex(/^\d+$/).describe("The Twitter user ID to get followers for"), + userId: z + .string() + .regex(/^\d+$/) + .describe("The Twitter user ID to get followers for"), maxResults: z .number() .min(1) @@ -522,7 +593,12 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const followers = await client.v2.followers(userId, { max_results: maxResults, - "user.fields": ["description", "public_metrics", "profile_image_url", "verified"], + "user.fields": [ + "description", + "public_metrics", + "profile_image_url", + "verified", + ], }); const users = followers.data?.data || []; @@ -566,7 +642,12 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const following = await client.v2.following(userId, { max_results: maxResults, - "user.fields": ["description", "public_metrics", "profile_image_url", "verified"], + "user.fields": [ + "description", + "public_metrics", + "profile_image_url", + "verified", + ], }); const users = following.data?.data || []; @@ -593,7 +674,10 @@ async function getTwitterMcpHandler() { "twitter_follow_user", "Follow a user on Twitter/X", { - targetUserId: z.string().regex(/^\d+$/).describe("The user ID of the account to follow"), + targetUserId: z + .string() + .regex(/^\d+$/) + .describe("The user ID of the account to follow"), }, async ({ targetUserId }) => { try { @@ -629,7 +713,11 @@ async function getTwitterMcpHandler() { const client = await getTwitterClient(orgId); const userId = await getAuthenticatedUserId(client, orgId); const result = await client.v2.unfollow(userId, targetUserId); - return jsonResult({ success: true, following: result.data.following, targetUserId }); + return jsonResult({ + success: true, + following: result.data.following, + targetUserId, + }); } catch (e) { return errorResult(errMsg(e, "Failed to unfollow user")); } @@ -654,7 +742,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -669,7 +759,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getTwitterMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/mcps/weather/[transport]/route.ts b/app/api/mcps/weather/[transport]/route.ts index e7dcbd3d8..615e953e8 100644 --- a/app/api/mcps/weather/[transport]/route.ts +++ b/app/api/mcps/weather/[transport]/route.ts @@ -271,7 +271,9 @@ function getWindDirection(degrees: number): string { return directions[index]; } -function formatLocation(result: NonNullable[0]): string { +function formatLocation( + result: NonNullable[0], +): string { const parts = [result.name]; if (result.admin1) parts.push(result.admin1); parts.push(result.country); @@ -292,7 +294,11 @@ function createHandler() { "get_current_weather", "Get real-time current weather conditions for any location worldwide. Data from Open-Meteo.", { - city: z.string().describe("City name (e.g., 'New York', 'London', 'Tokyo', 'Paris')"), + city: z + .string() + .describe( + "City name (e.g., 'New York', 'London', 'Tokyo', 'Paris')", + ), units: z .enum(["fahrenheit", "celsius"]) .optional() @@ -311,7 +317,8 @@ function createHandler() { text: JSON.stringify( { error: `City '${city}' not found`, - suggestion: "Try a more specific location or check spelling", + suggestion: + "Try a more specific location or check spelling", }, null, 2, @@ -323,8 +330,14 @@ function createHandler() { } const location = locations[0]; - const weather = await getCurrentWeather(location.latitude, location.longitude, units); - const { description, icon } = getWeatherDescription(weather.current.weather_code); + const weather = await getCurrentWeather( + location.latitude, + location.longitude, + units, + ); + const { description, icon } = getWeatherDescription( + weather.current.weather_code, + ); return { content: [ @@ -342,7 +355,9 @@ function createHandler() { }, current: { temperature: Math.round(weather.current.temperature_2m), - feelsLike: Math.round(weather.current.apparent_temperature), + feelsLike: Math.round( + weather.current.apparent_temperature, + ), humidity: weather.current.relative_humidity_2m, precipitation: weather.current.precipitation, cloudCover: weather.current.cloud_cover, @@ -354,7 +369,9 @@ function createHandler() { wind: { speed: Math.round(weather.current.wind_speed_10m), gusts: Math.round(weather.current.wind_gusts_10m), - direction: getWindDirection(weather.current.wind_direction_10m), + direction: getWindDirection( + weather.current.wind_direction_10m, + ), degrees: weather.current.wind_direction_10m, }, units: { @@ -379,7 +396,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to get weather", + error: + error instanceof Error + ? error.message + : "Failed to get weather", }, null, 2, @@ -422,7 +442,11 @@ function createHandler() { content: [ { type: "text" as const, - text: JSON.stringify({ error: `City '${city}' not found` }, null, 2), + text: JSON.stringify( + { error: `City '${city}' not found` }, + null, + 2, + ), }, ], isError: true, @@ -430,10 +454,17 @@ function createHandler() { } const location = locations[0]; - const forecast = await getForecast(location.latitude, location.longitude, days, units); + const forecast = await getForecast( + location.latitude, + location.longitude, + days, + units, + ); const dailyForecast = forecast.daily.time.map((date, i) => { - const { description, icon } = getWeatherDescription(forecast.daily.weather_code[i]); + const { description, icon } = getWeatherDescription( + forecast.daily.weather_code[i], + ); return { date, dayName: new Date(date).toLocaleDateString("en-US", { @@ -490,7 +521,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to get forecast", + error: + error instanceof Error + ? error.message + : "Failed to get forecast", }, null, 2, @@ -536,7 +570,9 @@ function createHandler() { location.longitude, units, ); - const { description, icon } = getWeatherDescription(weather.current.weather_code); + const { description, icon } = getWeatherDescription( + weather.current.weather_code, + ); return { city: formatLocation(location), @@ -564,7 +600,9 @@ function createHandler() { icon: string; } - const validResults = results.filter((r): r is ValidWeatherResult => !("error" in r)); + const validResults = results.filter( + (r): r is ValidWeatherResult => !("error" in r), + ); validResults.sort((a, b) => b.temperature - a.temperature); return { @@ -595,7 +633,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Failed to compare weather", + error: + error instanceof Error + ? error.message + : "Failed to compare weather", }, null, 2, @@ -629,7 +670,8 @@ function createHandler() { text: JSON.stringify( { error: `No locations found for '${query}'`, - suggestion: "Try a different spelling or more specific location", + suggestion: + "Try a different spelling or more specific location", }, null, 2, @@ -676,7 +718,10 @@ function createHandler() { type: "text" as const, text: JSON.stringify( { - error: error instanceof Error ? error.message : "Search failed", + error: + error instanceof Error + ? error.message + : "Search failed", }, null, 2, diff --git a/app/api/mcps/zoom/[transport]/route.ts b/app/api/mcps/zoom/[transport]/route.ts index 897e1ae41..d10625c19 100644 --- a/app/api/mcps/zoom/[transport]/route.ts +++ b/app/api/mcps/zoom/[transport]/route.ts @@ -62,7 +62,11 @@ async function getZoomMcpHandler() { return ctx.user; } - async function zoomFetch(orgId: string, path: string, options: RequestInit = {}) { + async function zoomFetch( + orgId: string, + path: string, + options: RequestInit = {}, + ) { const token = await getZoomToken(orgId); const url = `${ZOOM_API_BASE}${path}`; @@ -93,7 +97,9 @@ async function getZoomMcpHandler() { function errorResult(msg: string) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }], + content: [ + { type: "text" as const, text: JSON.stringify({ error: msg }) }, + ], isError: true, }; } @@ -101,21 +107,30 @@ async function getZoomMcpHandler() { mcpHandler = createMcpHandler( (server) => { // --- Connection status --- - server.tool("zoom_status", "Check Zoom OAuth connection status", {}, async () => { - try { - const orgId = getOrgId(); - const connections = await oauthService.listConnections({ - organizationId: orgId, - userId: getAuthUser().id, - platform: "zoom", - }); - const active = connections.find((c) => c.status === "active"); - if (!active) return jsonResult({ connected: false }); - return jsonResult({ connected: true, email: active.email, scopes: active.scopes }); - } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed"); - } - }); + server.tool( + "zoom_status", + "Check Zoom OAuth connection status", + {}, + async () => { + try { + const orgId = getOrgId(); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId: getAuthUser().id, + platform: "zoom", + }); + const active = connections.find((c) => c.status === "active"); + if (!active) return jsonResult({ connected: false }); + return jsonResult({ + connected: true, + email: active.email, + scopes: active.scopes, + }); + } catch (e) { + return errorResult(e instanceof Error ? e.message : "Failed"); + } + }, + ); // --- Get current user --- server.tool( @@ -139,7 +154,9 @@ async function getZoomMcpHandler() { personalMeetingUrl: data.personal_meeting_url, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get user"); + return errorResult( + e instanceof Error ? e.message : "Failed to get user", + ); } }, ); @@ -150,7 +167,13 @@ async function getZoomMcpHandler() { "List meetings for the current Zoom user. Returns upcoming, past, or all meetings depending on type parameter.", { type: z - .enum(["scheduled", "live", "upcoming", "upcoming_meetings", "previous_meetings"]) + .enum([ + "scheduled", + "live", + "upcoming", + "upcoming_meetings", + "previous_meetings", + ]) .optional() .describe( "Meeting type filter. Default: 'scheduled'. Use 'upcoming' for future meetings, 'previous_meetings' for past ones.", @@ -170,7 +193,10 @@ async function getZoomMcpHandler() { async ({ type = "scheduled", page_size = 30, next_page_token }) => { try { const orgId = getOrgId(); - const params = new URLSearchParams({ type, page_size: String(page_size) }); + const params = new URLSearchParams({ + type, + page_size: String(page_size), + }); if (next_page_token) params.set("next_page_token", next_page_token); const data = await zoomFetch(orgId, `/users/me/meetings?${params}`); @@ -190,7 +216,9 @@ async function getZoomMcpHandler() { nextPageToken: data.next_page_token, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to list meetings"); + return errorResult( + e instanceof Error ? e.message : "Failed to list meetings", + ); } }, ); @@ -200,7 +228,9 @@ async function getZoomMcpHandler() { "zoom_get_meeting", "Get detailed information about a specific Zoom meeting including settings, recurrence, and join URL", { - meetingId: z.union([z.string(), z.number()]).describe("The meeting ID to retrieve"), + meetingId: z + .union([z.string(), z.number()]) + .describe("The meeting ID to retrieve"), }, async ({ meetingId }) => { try { @@ -231,7 +261,9 @@ async function getZoomMcpHandler() { recurrence: data.recurrence, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to get meeting"); + return errorResult( + e instanceof Error ? e.message : "Failed to get meeting", + ); } }, ); @@ -257,17 +289,42 @@ async function getZoomMcpHandler() { .describe( "Meeting start time in ISO 8601 format (e.g. 2024-01-15T10:00:00Z). Required for scheduled meetings.", ), - duration: z.number().int().optional().describe("Meeting duration in minutes"), - timezone: z.string().optional().describe("Timezone (e.g. America/New_York, UTC)"), + duration: z + .number() + .int() + .optional() + .describe("Meeting duration in minutes"), + timezone: z + .string() + .optional() + .describe("Timezone (e.g. America/New_York, UTC)"), agenda: z.string().optional().describe("Meeting description/agenda"), - password: z.string().optional().describe("Meeting password (max 10 chars)"), + password: z + .string() + .optional() + .describe("Meeting password (max 10 chars)"), settings: z .object({ - host_video: z.boolean().optional().describe("Start with host video on"), - participant_video: z.boolean().optional().describe("Start with participant video on"), - join_before_host: z.boolean().optional().describe("Allow joining before host"), - mute_upon_entry: z.boolean().optional().describe("Mute participants on entry"), - waiting_room: z.boolean().optional().describe("Enable waiting room"), + host_video: z + .boolean() + .optional() + .describe("Start with host video on"), + participant_video: z + .boolean() + .optional() + .describe("Start with participant video on"), + join_before_host: z + .boolean() + .optional() + .describe("Allow joining before host"), + mute_upon_entry: z + .boolean() + .optional() + .describe("Mute participants on entry"), + waiting_room: z + .boolean() + .optional() + .describe("Enable waiting room"), auto_recording: z .enum(["local", "cloud", "none"]) .optional() @@ -276,7 +333,16 @@ async function getZoomMcpHandler() { .optional() .describe("Meeting settings"), }, - async ({ topic, type = 2, start_time, duration, timezone, agenda, password, settings }) => { + async ({ + topic, + type = 2, + start_time, + duration, + timezone, + agenda, + password, + settings, + }) => { try { const orgId = getOrgId(); const body: Record = { topic, type }; @@ -304,7 +370,9 @@ async function getZoomMcpHandler() { status: data.status, }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to create meeting"); + return errorResult( + e instanceof Error ? e.message : "Failed to create meeting", + ); } }, ); @@ -314,10 +382,19 @@ async function getZoomMcpHandler() { "zoom_update_meeting", "Update an existing Zoom meeting's details, time, or settings", { - meetingId: z.union([z.string(), z.number()]).describe("The meeting ID to update"), + meetingId: z + .union([z.string(), z.number()]) + .describe("The meeting ID to update"), topic: z.string().optional().describe("New meeting topic"), - start_time: z.string().optional().describe("New start time in ISO 8601 format"), - duration: z.number().int().optional().describe("New duration in minutes"), + start_time: z + .string() + .optional() + .describe("New start time in ISO 8601 format"), + duration: z + .number() + .int() + .optional() + .describe("New duration in minutes"), timezone: z.string().optional().describe("New timezone"), agenda: z.string().optional().describe("New agenda/description"), password: z.string().optional().describe("New password"), @@ -361,7 +438,9 @@ async function getZoomMcpHandler() { return jsonResult({ success: true, meetingId }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to update meeting"); + return errorResult( + e instanceof Error ? e.message : "Failed to update meeting", + ); } }, ); @@ -371,21 +450,31 @@ async function getZoomMcpHandler() { "zoom_delete_meeting", "Delete a Zoom meeting permanently", { - meetingId: z.union([z.string(), z.number()]).describe("The meeting ID to delete"), + meetingId: z + .union([z.string(), z.number()]) + .describe("The meeting ID to delete"), }, async ({ meetingId }) => { try { const orgId = getOrgId(); - await zoomFetch(orgId, `/meetings/${meetingId}`, { method: "DELETE" }); + await zoomFetch(orgId, `/meetings/${meetingId}`, { + method: "DELETE", + }); return jsonResult({ success: true, deleted: meetingId }); } catch (e) { - return errorResult(e instanceof Error ? e.message : "Failed to delete meeting"); + return errorResult( + e instanceof Error ? e.message : "Failed to delete meeting", + ); } }, ); }, { capabilities: { tools: {} } }, - { streamableHttpEndpoint: "/api/mcps/zoom/streamable-http", disableSse: true, maxDuration: 60 }, + { + streamableHttpEndpoint: "/api/mcps/zoom/streamable-http", + disableSse: true, + maxDuration: 60, + }, ); return mcpHandler; @@ -398,7 +487,9 @@ async function handleRequest( const { transport } = await params; if (transport !== "streamable-http") { return new Response( - JSON.stringify({ error: `Transport "${transport}" not supported. Use streamable-http.` }), + JSON.stringify({ + error: `Transport "${transport}" not supported. Use streamable-http.`, + }), { status: 405, headers: { "Content-Type": "application/json" } }, ); } @@ -413,7 +504,9 @@ async function handleRequest( if (rateLimited) return rateLimited; const handler = await getZoomMcpHandler(); - const mcpResponse = await authContextStorage.run(authResult, () => handler(req as Request)); + const mcpResponse = await authContextStorage.run(authResult, () => + handler(req as Request), + ); if (!mcpResponse || !isMcpHandlerResponse(mcpResponse)) { return new Response(JSON.stringify({ error: "invalid_response" }), { diff --git a/app/api/my-agents/characters/[id]/clone/route.ts b/app/api/my-agents/characters/[id]/clone/route.ts index 84cb0783d..5539b1c56 100644 --- a/app/api/my-agents/characters/[id]/clone/route.ts +++ b/app/api/my-agents/characters/[id]/clone/route.ts @@ -14,7 +14,10 @@ export const dynamic = "force-dynamic"; * @param params - Route parameters containing the character ID to clone. * @returns Cloned character details. */ -export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -36,7 +39,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Get the original character const original = await charactersService.getById(id); if (!original) { - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } // Determine the clone's name @@ -84,7 +90,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ logger.error("[My Agents API] Error cloning character:", error); // Handle username validation errors specially - const errorMessage = error instanceof Error ? error.message : "Failed to clone character"; + const errorMessage = + error instanceof Error ? error.message : "Failed to clone character"; const isValidationError = errorMessage.includes("username") || errorMessage.includes("Username"); diff --git a/app/api/my-agents/characters/[id]/route.ts b/app/api/my-agents/characters/[id]/route.ts index 14c0e211c..9d93432ad 100644 --- a/app/api/my-agents/characters/[id]/route.ts +++ b/app/api/my-agents/characters/[id]/route.ts @@ -11,14 +11,20 @@ export const dynamic = "force-dynamic"; * Get a specific character by ID. * Supports both Privy session and API key authentication. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; const character = await charactersService.getByIdForUser(id, user.id); if (!character) { - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } return NextResponse.json({ success: true, data: { character } }); diff --git a/app/api/my-agents/characters/[id]/share/route.ts b/app/api/my-agents/characters/[id]/share/route.ts index 5a8cb448f..1d417a214 100644 --- a/app/api/my-agents/characters/[id]/share/route.ts +++ b/app/api/my-agents/characters/[id]/share/route.ts @@ -16,7 +16,10 @@ const ShareSchema = z.object({ * Get the current sharing status of a character. * Supports both Privy session and API key authentication. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -24,16 +27,22 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const character = await charactersService.getByIdForUser(id, user.id); if (!character) { - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; return NextResponse.json({ success: true, data: { isPublic: character.is_public, - shareUrl: character.is_public ? `${baseUrl}/chat/${character.id}` : null, + shareUrl: character.is_public + ? `${baseUrl}/chat/${character.id}` + : null, // Additional info for shared characters shareInfo: character.is_public ? { @@ -70,7 +79,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ * - Only "shared" knowledge items are accessible to public users * - User billing is based on who chats (not the character owner) */ -export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -125,7 +137,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ revalidatePath("/dashboard/my-agents"); revalidatePath("/dashboard/build"); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; return NextResponse.json({ success: true, @@ -151,7 +164,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to update share status", + error: + error instanceof Error + ? error.message + : "Failed to update share status", }, { status: 500 }, ); diff --git a/app/api/my-agents/characters/[id]/stats/route.ts b/app/api/my-agents/characters/[id]/stats/route.ts index c47c72355..36c09e5a4 100644 --- a/app/api/my-agents/characters/[id]/stats/route.ts +++ b/app/api/my-agents/characters/[id]/stats/route.ts @@ -9,14 +9,20 @@ export const dynamic = "force-dynamic"; * Get statistics for a character. * Supports both Privy session and API key authentication. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; const character = await charactersService.getByIdForUser(id, user.id); if (!character) { - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } // Return basic stats. These are placeholders and follow the @@ -33,6 +39,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }, }); } catch (_error) { - return NextResponse.json({ success: false, error: "Failed to get stats" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Failed to get stats" }, + { status: 500 }, + ); } } diff --git a/app/api/my-agents/characters/[id]/track-interaction/route.ts b/app/api/my-agents/characters/[id]/track-interaction/route.ts index 3b7777d4f..6061a9711 100644 --- a/app/api/my-agents/characters/[id]/track-interaction/route.ts +++ b/app/api/my-agents/characters/[id]/track-interaction/route.ts @@ -9,7 +9,10 @@ export const dynamic = "force-dynamic"; * Tracks an interaction with a character. * The marketplace tracking backend was removed, so this endpoint is gone. */ -export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function POST( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { await requireAuthWithOrg(); const { id } = await params; @@ -20,7 +23,8 @@ export async function POST(_request: NextRequest, { params }: { params: Promise< return NextResponse.json( { success: false, - error: "Character interaction tracking was removed with the marketplace service", + error: + "Character interaction tracking was removed with the marketplace service", }, { status: 410 }, ); diff --git a/app/api/my-agents/characters/[id]/track-view/route.ts b/app/api/my-agents/characters/[id]/track-view/route.ts index 2dc87dc8c..d7a201993 100644 --- a/app/api/my-agents/characters/[id]/track-view/route.ts +++ b/app/api/my-agents/characters/[id]/track-view/route.ts @@ -8,19 +8,28 @@ export const dynamic = "force-dynamic"; * Tracks a view of a character. * The marketplace tracking backend was removed, so this endpoint is gone. */ -export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function POST( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { id } = await params; - logger.warn("[My Agents API] Rejecting removed track-view route", { characterId: id }); + logger.warn("[My Agents API] Rejecting removed track-view route", { + characterId: id, + }); return NextResponse.json( { success: false, - error: "Character view tracking was removed with the marketplace service", + error: + "Character view tracking was removed with the marketplace service", }, { status: 410 }, ); } catch (_error) { - return NextResponse.json({ success: false, error: "Failed to track view" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Failed to track view" }, + { status: 500 }, + ); } } diff --git a/app/api/my-agents/characters/route.ts b/app/api/my-agents/characters/route.ts index 4d75736a2..094bb6ed0 100644 --- a/app/api/my-agents/characters/route.ts +++ b/app/api/my-agents/characters/route.ts @@ -51,7 +51,10 @@ export async function GET(request: NextRequest) { // Pagination const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); - const limit = Math.min(1000, Math.max(1, parseInt(searchParams.get("limit") || "30", 10))); + const limit = Math.min( + 1000, + Math.max(1, parseInt(searchParams.get("limit") || "30", 10)), + ); logger.debug("[My Agents API] Search request:", { userId: user.id, @@ -72,8 +75,10 @@ export async function GET(request: NextRequest) { characters = characters.filter( (char) => char.name.toLowerCase().includes(query) || - (typeof char.bio === "string" && char.bio.toLowerCase().includes(query)) || - (Array.isArray(char.bio) && char.bio.some((b) => b.toLowerCase().includes(query))), + (typeof char.bio === "string" && + char.bio.toLowerCase().includes(query)) || + (Array.isArray(char.bio) && + char.bio.some((b) => b.toLowerCase().includes(query))), ); } diff --git a/app/api/my-agents/claim-affiliate-characters/route.ts b/app/api/my-agents/claim-affiliate-characters/route.ts index 1f297de4f..fe92b7051 100644 --- a/app/api/my-agents/claim-affiliate-characters/route.ts +++ b/app/api/my-agents/claim-affiliate-characters/route.ts @@ -33,7 +33,9 @@ export async function POST(request: NextRequest) { try { const user = await requireAuthWithOrg(); - logger.info(`[Claim Affiliate Chars] Starting claim process for user ${user.id}`); + logger.info( + `[Claim Affiliate Chars] Starting claim process for user ${user.id}`, + ); // Parse request body for session token let sessionToken: string | undefined; @@ -54,12 +56,16 @@ export async function POST(request: NextRequest) { }> = []; // Get all rooms the user participates in - const userRoomIds = await participantsRepository.findRoomsByEntityId(user.id); + const userRoomIds = await participantsRepository.findRoomsByEntityId( + user.id, + ); if (userRoomIds.length > 0) { // Get the rooms with their agentIds (characterIds) const rooms = await roomsRepository.findByIds(userRoomIds); - const characterIds = [...new Set(rooms.map((r) => r.agentId).filter(Boolean))] as string[]; + const characterIds = [ + ...new Set(rooms.map((r) => r.agentId).filter(Boolean)), + ] as string[]; if (characterIds.length > 0) { // Get characters and check their owners @@ -75,7 +81,8 @@ export async function POST(request: NextRequest) { if ( owner && (owner.is_anonymous === true || - (owner.email?.includes("@anonymous.elizacloud.ai") && !owner.privy_user_id)) + (owner.email?.includes("@anonymous.elizacloud.ai") && + !owner.privy_user_id)) ) { const room = rooms.find((r) => r.agentId === char.id); claimableCharacters.push({ @@ -91,7 +98,9 @@ export async function POST(request: NextRequest) { // Also find characters via session token if provided if (sessionToken) { - logger.info(`[Claim Affiliate Chars] Session token provided, looking up session...`); + logger.info( + `[Claim Affiliate Chars] Session token provided, looking up session...`, + ); const session = await anonymousSessionsService.getByToken(sessionToken); @@ -103,10 +112,14 @@ export async function POST(request: NextRequest) { sessionOwner.is_anonymous && sessionOwner.email?.includes("@anonymous.elizacloud.ai") ) { - logger.info(`[Claim Affiliate Chars] Found affiliate session owner: ${sessionOwner.id}`); + logger.info( + `[Claim Affiliate Chars] Found affiliate session owner: ${sessionOwner.id}`, + ); // Find characters owned by this anonymous user - const sessionCharacters = await userCharactersRepository.listByUser(sessionOwner.id); + const sessionCharacters = await userCharactersRepository.listByUser( + sessionOwner.id, + ); for (const char of sessionCharacters) { // Only add if not already in the list and owned by the session owner @@ -120,19 +133,25 @@ export async function POST(request: NextRequest) { ownerId: sessionOwner.id, roomId: "", // No room association, but we'll claim via session }); - logger.info(`[Claim Affiliate Chars] Added character from session: ${char.name}`); + logger.info( + `[Claim Affiliate Chars] Added character from session: ${char.name}`, + ); } } // Mark session as converted to prevent future claims await anonymousSessionsService.markConverted(session.id); - logger.info(`[Claim Affiliate Chars] Marked session as converted: ${session.id}`); + logger.info( + `[Claim Affiliate Chars] Marked session as converted: ${session.id}`, + ); } } } if (claimableCharacters.length === 0) { - logger.info(`[Claim Affiliate Chars] No claimable characters found for user ${user.id}`); + logger.info( + `[Claim Affiliate Chars] No claimable characters found for user ${user.id}`, + ); return NextResponse.json({ success: true, claimed: [], @@ -166,7 +185,9 @@ export async function POST(request: NextRequest) { id: char.characterId, name: char.characterName, }); - logger.info(`[Claim Affiliate Chars] ✅ Claimed character: ${char.characterName}`); + logger.info( + `[Claim Affiliate Chars] ✅ Claimed character: ${char.characterName}`, + ); } else { failedClaims.push({ id: char.characterId, reason: result.message }); logger.warn( @@ -189,7 +210,8 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to claim characters", + error: + error instanceof Error ? error.message : "Failed to claim characters", }, { status: 500 }, ); diff --git a/app/api/my-agents/saved/[id]/route.ts b/app/api/my-agents/saved/[id]/route.ts index 5ee9ca628..db831fcf8 100644 --- a/app/api/my-agents/saved/[id]/route.ts +++ b/app/api/my-agents/saved/[id]/route.ts @@ -16,7 +16,10 @@ export const dynamic = "force-dynamic"; * @param params - Route params containing the agent ID. * @returns Agent details with conversation stats. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: agentId } = await params; @@ -26,7 +29,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ agentId, }); - const result = await charactersService.getSavedAgentDetails(user.id, agentId); + const result = await charactersService.getSavedAgentDetails( + user.id, + agentId, + ); if (!result) { return NextResponse.json( diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index d75726399..0f4183705 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -15,7 +15,8 @@ const _BRAND_ACCENT_GRADIENT = `linear-gradient(135deg, ${BRAND_ORANGE} 0%, #FF7 // System monospace font stack - works reliably for OG image rendering // Optimized for technical/code aesthetic matching platform -const MONO_FONT = "'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace"; +const MONO_FONT = + "'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace"; /** * GET /api/og @@ -38,7 +39,8 @@ export async function GET(request: NextRequest) { const type = searchParams.get("type") || "default"; const title = searchParams.get("title") || "Eliza Cloud"; - const description = searchParams.get("description") || "AI Agent Development Platform"; + const description = + searchParams.get("description") || "AI Agent Development Platform"; const name = searchParams.get("name"); const characterName = searchParams.get("characterName"); diff --git a/app/api/openapi.json/route.ts b/app/api/openapi.json/route.ts index 860606c2f..c98993a69 100644 --- a/app/api/openapi.json/route.ts +++ b/app/api/openapi.json/route.ts @@ -42,7 +42,8 @@ function tagForPath(routePath: string) { } export async function GET() { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const discovered = await discoverPublicApiRoutes(); const discoveredPaths: Record = {}; diff --git a/app/api/organizations/invites/[inviteId]/route.ts b/app/api/organizations/invites/[inviteId]/route.ts index 7b60a7425..da70cc1b9 100644 --- a/app/api/organizations/invites/[inviteId]/route.ts +++ b/app/api/organizations/invites/[inviteId]/route.ts @@ -31,7 +31,10 @@ async function handleDELETE( } if (!context?.params) { - return NextResponse.json({ success: false, error: "Invalid request" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid request" }, + { status: 400 }, + ); } const { inviteId } = await context.params; @@ -45,7 +48,8 @@ async function handleDELETE( } catch (error) { logger.error("Error revoking invite:", error); - const errorMessage = error instanceof Error ? error.message : "Failed to revoke invitation"; + const errorMessage = + error instanceof Error ? error.message : "Failed to revoke invitation"; return NextResponse.json( { diff --git a/app/api/organizations/invites/route.ts b/app/api/organizations/invites/route.ts index 27ff369fd..a3bf5486f 100644 --- a/app/api/organizations/invites/route.ts +++ b/app/api/organizations/invites/route.ts @@ -7,9 +7,11 @@ import { logger } from "@/lib/utils/logger"; const createInviteSchema = z.object({ email: z.string().email("Invalid email address"), - role: z.enum(["admin", "member"]).refine((val) => val === "admin" || val === "member", { - message: "Role must be 'admin' or 'member'", - }), + role: z + .enum(["admin", "member"]) + .refine((val) => val === "admin" || val === "member", { + message: "Role must be 'admin' or 'member'", + }), }); /** @@ -69,7 +71,8 @@ async function handlePOST(request: NextRequest) { ); } - const errorMessage = error instanceof Error ? error.message : "Failed to create invitation"; + const errorMessage = + error instanceof Error ? error.message : "Failed to create invitation"; return NextResponse.json( { @@ -78,7 +81,8 @@ async function handlePOST(request: NextRequest) { }, { status: - errorMessage.includes("already a member") || errorMessage.includes("already pending") + errorMessage.includes("already a member") || + errorMessage.includes("already pending") ? 409 : 500, }, @@ -107,7 +111,9 @@ async function handleGET(_request: NextRequest) { ); } - const invites = await invitesService.listByOrganization(user.organization_id!); + const invites = await invitesService.listByOrganization( + user.organization_id!, + ); /** * Type for invite with inviter relation. @@ -151,7 +157,10 @@ async function handleGET(_request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch invitations", + error: + error instanceof Error + ? error.message + : "Failed to fetch invitations", }, { status: 500 }, ); diff --git a/app/api/organizations/members/[userId]/route.ts b/app/api/organizations/members/[userId]/route.ts index 577915dce..0270e8545 100644 --- a/app/api/organizations/members/[userId]/route.ts +++ b/app/api/organizations/members/[userId]/route.ts @@ -7,9 +7,11 @@ import { usersService } from "@/lib/services/users"; import { logger } from "@/lib/utils/logger"; const updateMemberSchema = z.object({ - role: z.enum(["admin", "member"]).refine((val) => val === "admin" || val === "member", { - message: "Role must be 'admin' or 'member'", - }), + role: z + .enum(["admin", "member"]) + .refine((val) => val === "admin" || val === "member", { + message: "Role must be 'admin' or 'member'", + }), }); /** @@ -39,7 +41,10 @@ async function handlePATCH( } if (!context?.params) { - return NextResponse.json({ success: false, error: "Invalid request" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid request" }, + { status: 400 }, + ); } const { userId } = await context.params; @@ -133,7 +138,8 @@ async function handlePATCH( return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to update member", + error: + error instanceof Error ? error.message : "Failed to update member", }, { status: 500 }, ); @@ -167,7 +173,10 @@ async function handleDELETE( } if (!context?.params) { - return NextResponse.json({ success: false, error: "Invalid request" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid request" }, + { status: 400 }, + ); } const { userId } = await context.params; @@ -238,7 +247,8 @@ async function handleDELETE( return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to remove member", + error: + error instanceof Error ? error.message : "Failed to remove member", }, { status: 500 }, ); diff --git a/app/api/organizations/members/route.ts b/app/api/organizations/members/route.ts index ee363f9b4..6556601cf 100644 --- a/app/api/organizations/members/route.ts +++ b/app/api/organizations/members/route.ts @@ -25,7 +25,9 @@ async function handleGET(request: NextRequest) { ); } - const members = await usersService.listByOrganization(user.organization_id!); + const members = await usersService.listByOrganization( + user.organization_id!, + ); return NextResponse.json({ success: true, @@ -47,7 +49,8 @@ async function handleGET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch members", + error: + error instanceof Error ? error.message : "Failed to fetch members", }, { status: 500 }, ); diff --git a/app/api/privy/webhook/route.ts b/app/api/privy/webhook/route.ts index d025a30c5..40c8eeb54 100644 --- a/app/api/privy/webhook/route.ts +++ b/app/api/privy/webhook/route.ts @@ -2,7 +2,10 @@ import crypto from "crypto"; import { cookies, headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import { getSignupMethod } from "@/lib/analytics/posthog"; -import { identifyServerUser, trackServerEvent } from "@/lib/analytics/posthog-server"; +import { + identifyServerUser, + trackServerEvent, +} from "@/lib/analytics/posthog-server"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { type SyncOptions, syncUserFromPrivy } from "@/lib/privy-sync"; import { anonymousSessionsService } from "@/lib/services/anonymous-sessions"; @@ -29,10 +32,16 @@ async function verifyWebhookSignature( const signedPayload = `v1:${timestamp}:${payload}`; // Calculate expected signature - const expectedSignature = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex"); + const expectedSignature = crypto + .createHmac("sha256", secret) + .update(signedPayload) + .digest("hex"); // Compare signatures - return crypto.timingSafeEqual(Buffer.from(providedSignature), Buffer.from(expectedSignature)); + return crypto.timingSafeEqual( + Buffer.from(providedSignature), + Buffer.from(expectedSignature), + ); } /** @@ -56,19 +65,32 @@ async function handlePrivyWebhook(request: NextRequest) { const signature = headersList.get("privy-webhook-signature"); if (!signature) { - return NextResponse.json({ error: "Missing webhook signature" }, { status: 401 }); + return NextResponse.json( + { error: "Missing webhook signature" }, + { status: 401 }, + ); } // Verify webhook signature const webhookSecret = process.env.PRIVY_WEBHOOK_SECRET; if (!webhookSecret) { logger.error("PRIVY_WEBHOOK_SECRET not configured"); - return NextResponse.json({ error: "Webhook not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Webhook not configured" }, + { status: 500 }, + ); } - const isValid = await verifyWebhookSignature(body, signature, webhookSecret); + const isValid = await verifyWebhookSignature( + body, + signature, + webhookSecret, + ); if (!isValid) { - return NextResponse.json({ error: "Invalid webhook signature" }, { status: 401 }); + return NextResponse.json( + { error: "Invalid webhook signature" }, + { status: 401 }, + ); } // Parse the webhook payload @@ -77,7 +99,8 @@ async function handlePrivyWebhook(request: NextRequest) { // Extract IP address from headers (for abuse tracking) const forwardedFor = headersList.get("x-forwarded-for"); const realIp = headersList.get("x-real-ip"); - const ipAddress = forwardedFor?.split(",")[0]?.trim() || realIp || undefined; + const ipAddress = + forwardedFor?.split(",")[0]?.trim() || realIp || undefined; const userAgent = headersList.get("user-agent") || undefined; // Handle different webhook events @@ -100,13 +123,16 @@ async function handlePrivyWebhook(request: NextRequest) { try { await referralsService.checkAndQualifyReferral(user.id); } catch (qualificationError) { - logger.error("[Privy Webhook] Failed to process referral qualification", { - userId: user.id, - error: - qualificationError instanceof Error - ? qualificationError.message - : String(qualificationError), - }); + logger.error( + "[Privy Webhook] Failed to process referral qualification", + { + userId: user.id, + error: + qualificationError instanceof Error + ? qualificationError.message + : String(qualificationError), + }, + ); } } @@ -117,9 +143,14 @@ async function handlePrivyWebhook(request: NextRequest) { // Identify user in PostHog using internal UUID for consistent tracking identifyServerUser(user.id, { - email: privyUser.email?.address || privyUser.google?.email || privyUser.discord?.email, + email: + privyUser.email?.address || + privyUser.google?.email || + privyUser.discord?.email, name: - privyUser.google?.name || privyUser.discord?.username || privyUser.github?.username, + privyUser.google?.name || + privyUser.discord?.username || + privyUser.github?.username, wallet_address: privyUser.wallet?.address, signup_method: signupMethod, created_at: new Date().toISOString(), @@ -143,12 +174,16 @@ async function handlePrivyWebhook(request: NextRequest) { const anonSessionToken = cookieStore.get("eliza-anon-session")?.value; if (anonSessionToken) { - logger.info("[Privy Webhook] Anonymous session detected, initiating migration...", { - tokenPreview: anonSessionToken.slice(0, 8) + "...", - }); + logger.info( + "[Privy Webhook] Anonymous session detected, initiating migration...", + { + tokenPreview: anonSessionToken.slice(0, 8) + "...", + }, + ); try { - const anonSession = await anonymousSessionsService.getByToken(anonSessionToken); + const anonSession = + await anonymousSessionsService.getByToken(anonSessionToken); if (anonSession) { const migrationResult = await migrateAnonymousSession( @@ -164,7 +199,9 @@ async function handlePrivyWebhook(request: NextRequest) { ...migrationResult.mergedData, }); } else { - logger.debug("[Privy Webhook] Anonymous session token not found in DB"); + logger.debug( + "[Privy Webhook] Anonymous session token not found in DB", + ); } } catch (migrationError) { logger.error("[Privy Webhook] Migration failed:", migrationError); @@ -191,7 +228,10 @@ async function handlePrivyWebhook(request: NextRequest) { break; } - return NextResponse.json({ success: true, message: "Webhook processed" }, { status: 200 }); + return NextResponse.json( + { success: true, message: "Webhook processed" }, + { status: 200 }, + ); } catch (error) { logger.error("Webhook processing error:", error); @@ -208,4 +248,7 @@ async function handlePrivyWebhook(request: NextRequest) { } // Export rate-limited handler -export const POST = withRateLimit(handlePrivyWebhook, RateLimitPresets.AGGRESSIVE); +export const POST = withRateLimit( + handlePrivyWebhook, + RateLimitPresets.AGGRESSIVE, +); diff --git a/app/api/quotas/usage/route.ts b/app/api/quotas/usage/route.ts index a1c4095ab..b8a5a259b 100644 --- a/app/api/quotas/usage/route.ts +++ b/app/api/quotas/usage/route.ts @@ -15,7 +15,9 @@ async function handleGET(req: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(req); - const usage = await usageQuotasService.getCurrentUsage(user.organization_id); + const usage = await usageQuotasService.getCurrentUsage( + user.organization_id, + ); return NextResponse.json({ success: true, @@ -27,7 +29,10 @@ async function handleGET(req: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch quota usage", + error: + error instanceof Error + ? error.message + : "Failed to fetch quota usage", }, { status: 500 }, ); diff --git a/app/api/sessions/current/route.ts b/app/api/sessions/current/route.ts index 7c3483560..21b6ec739 100644 --- a/app/api/sessions/current/route.ts +++ b/app/api/sessions/current/route.ts @@ -42,7 +42,10 @@ async function handleGET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch session stats", + error: + error instanceof Error + ? error.message + : "Failed to fetch session stats", }, { status: 500 }, ); diff --git a/app/api/set-anonymous-session/route.ts b/app/api/set-anonymous-session/route.ts index bd8b0fe45..5514058e2 100644 --- a/app/api/set-anonymous-session/route.ts +++ b/app/api/set-anonymous-session/route.ts @@ -40,10 +40,16 @@ async function handlePOST(request: NextRequest) { if (!sessionToken || typeof sessionToken !== "string") { logger.warn("[Set Session] Missing or invalid session token in request"); - return NextResponse.json({ error: "Session token is required" }, { status: 400 }); + return NextResponse.json( + { error: "Session token is required" }, + { status: 400 }, + ); } - logger.info("[Set Session] Looking up session:", sessionToken.substring(0, 8) + "..."); + logger.info( + "[Set Session] Looking up session:", + sessionToken.substring(0, 8) + "...", + ); // Validate that the session exists const session = await anonymousSessionsService.getByToken(sessionToken); @@ -79,7 +85,10 @@ async function handlePOST(request: NextRequest) { if (!user) { // User doesn't exist - create a real anonymous user and update the session - logger.info("[Set Session] User not found, creating anonymous user for session:", session.id); + logger.info( + "[Set Session] User not found, creating anonymous user for session:", + session.id, + ); // Create anonymous user const [newUser] = await dbWrite diff --git a/app/api/signup-code/redeem/route.ts b/app/api/signup-code/redeem/route.ts index 8d3dd1dbf..8ca713bdc 100644 --- a/app/api/signup-code/redeem/route.ts +++ b/app/api/signup-code/redeem/route.ts @@ -27,7 +27,10 @@ async function handlePOST(request: NextRequest) { user = await requireAuthWithOrg(); } catch (error) { const message = error instanceof Error ? error.message : String(error); - return NextResponse.json({ error: message }, { status: 401, headers: NO_CACHE_HEADERS }); + return NextResponse.json( + { error: message }, + { status: 401, headers: NO_CACHE_HEADERS }, + ); } const organizationId = user.organization_id!; diff --git a/app/api/stats/account/route.ts b/app/api/stats/account/route.ts index 545e1a9f9..977dc56d7 100644 --- a/app/api/stats/account/route.ts +++ b/app/api/stats/account/route.ts @@ -24,8 +24,10 @@ async function handleGET(req: NextRequest) { usageService.getStatsByOrganization(organizationId, twentyFourHoursAgo), ]); - const imageCount = generationStats.byType.find((t) => t.type === "image")?.count || 0; - const videoCount = generationStats.byType.find((t) => t.type === "video")?.count || 0; + const imageCount = + generationStats.byType.find((t) => t.type === "image")?.count || 0; + const videoCount = + generationStats.byType.find((t) => t.type === "video")?.count || 0; return NextResponse.json({ success: true, diff --git a/app/api/stripe/create-checkout-session/route.ts b/app/api/stripe/create-checkout-session/route.ts index e0922c232..0a77fa1f0 100644 --- a/app/api/stripe/create-checkout-session/route.ts +++ b/app/api/stripe/create-checkout-session/route.ts @@ -91,13 +91,19 @@ async function handleCheckoutSession(req: NextRequest) { // Validate organization_id is present (guaranteed by requireAuthWithOrg) const organizationId = user.organization_id; if (!organizationId) { - return NextResponse.json({ error: "Organization not found" }, { status: 400 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 400 }, + ); } if (creditPackId) { const creditPack = await creditsService.getCreditPackById(creditPackId); if (!creditPack || !creditPack.is_active) { - return NextResponse.json({ error: "Invalid or inactive credit pack" }, { status: 404 }); + return NextResponse.json( + { error: "Invalid or inactive credit pack" }, + { status: 404 }, + ); } lineItems = [ @@ -176,7 +182,8 @@ async function handleCheckoutSession(req: NextRequest) { // Secure URL construction - validate origin to prevent open redirect vulnerabilities const envAppUrl = process.env.NEXT_PUBLIC_APP_URL; const requestOrigin = - req.headers.get("origin") || req.headers.get("referer")?.split("/").slice(0, 3).join("/"); + req.headers.get("origin") || + req.headers.get("referer")?.split("/").slice(0, 3).join("/"); // Only use request origin if it's in the allowed list let baseUrl: string; @@ -186,7 +193,9 @@ async function handleCheckoutSession(req: NextRequest) { baseUrl = requestOrigin; } else { if (requestOrigin) { - logger.warn(`[Stripe Checkout] Untrusted origin rejected: ${requestOrigin}`); + logger.warn( + `[Stripe Checkout] Untrusted origin rejected: ${requestOrigin}`, + ); } baseUrl = "http://localhost:3000"; } @@ -246,9 +255,15 @@ async function handleCheckoutSession(req: NextRequest) { logger.error("[Stripe Checkout] Error creating checkout session:", error); // Don't expose internal details - log them but return generic message - return NextResponse.json({ error: "Failed to create checkout session" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to create checkout session" }, + { status: 500 }, + ); } } // Export rate-limited handler with standard preset -export const POST = withRateLimit(handleCheckoutSession, RateLimitPresets.STRICT); +export const POST = withRateLimit( + handleCheckoutSession, + RateLimitPresets.STRICT, +); diff --git a/app/api/stripe/credit-packs/route.ts b/app/api/stripe/credit-packs/route.ts index e313b3f14..667b9c11b 100644 --- a/app/api/stripe/credit-packs/route.ts +++ b/app/api/stripe/credit-packs/route.ts @@ -17,7 +17,10 @@ async function handleGET(_request: NextRequest) { return NextResponse.json({ creditPacks }, { status: 200 }); } catch (error) { logger.error("Error fetching credit packs:", error); - return NextResponse.json({ error: "Failed to fetch credit packs" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch credit packs" }, + { status: 500 }, + ); } } diff --git a/app/api/stripe/webhook/route.ts b/app/api/stripe/webhook/route.ts index 4dc50a5ac..23f223ae5 100644 --- a/app/api/stripe/webhook/route.ts +++ b/app/api/stripe/webhook/route.ts @@ -54,18 +54,27 @@ async function handleStripeWebhook(req: NextRequest) { const signature = headersList.get("stripe-signature"); if (!signature) { - return NextResponse.json({ error: "No signature provided" }, { status: 400 }); + return NextResponse.json( + { error: "No signature provided" }, + { status: 400 }, + ); } const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; if (!webhookSecret) { logger.error("[Stripe Webhook] STRIPE_WEBHOOK_SECRET is not set"); - return NextResponse.json({ error: "Webhook configuration error" }, { status: 500 }); + return NextResponse.json( + { error: "Webhook configuration error" }, + { status: 500 }, + ); } if (!isStripeConfigured()) { logger.error("[Stripe Webhook] STRIPE_SECRET_KEY is not set"); - return NextResponse.json({ error: "Stripe configuration error" }, { status: 500 }); + return NextResponse.json( + { error: "Stripe configuration error" }, + { status: 500 }, + ); } const stripe = requireStripe(); @@ -75,7 +84,10 @@ async function handleStripeWebhook(req: NextRequest) { event = stripe.webhooks.constructEvent(body, signature, webhookSecret); } catch { logger.error("[Stripe Webhook] Signature verification failed"); - return NextResponse.json({ error: "Webhook signature verification failed" }, { status: 400 }); + return NextResponse.json( + { error: "Webhook signature verification failed" }, + { status: 400 }, + ); } logger.info(`[Stripe Webhook] Received event: ${event.type} (${event.id})`); @@ -97,7 +109,8 @@ async function handleStripeWebhook(req: NextRequest) { const appId = session.metadata?.app_id; // Check if this is an app-specific purchase - const isAppPurchase = purchaseSource === "miniapp_app" && appId && userId; + const isAppPurchase = + purchaseSource === "miniapp_app" && appId && userId; if (!organizationId || !credits) { logger.warn( @@ -129,7 +142,9 @@ async function handleStripeWebhook(req: NextRequest) { } const existingTransaction = - await creditsService.getTransactionByStripePaymentIntent(paymentIntentId); + await creditsService.getTransactionByStripePaymentIntent( + paymentIntentId, + ); const isDuplicate = !!existingTransaction; if (isDuplicate) { @@ -205,13 +220,21 @@ async function handleStripeWebhook(req: NextRequest) { // Invalidate org rate limit tier cache on app purchase too invalidateOrgTierCache(organizationId).catch((err) => - logger.warn("[Stripe Webhook] Failed to invalidate org tier cache", { - error: err instanceof Error ? err.message : String(err), - }), + logger.warn( + "[Stripe Webhook] Failed to invalidate org tier cache", + { + error: err instanceof Error ? err.message : String(err), + }, + ), ); } catch (appError) { - logger.error("[Stripe Webhook] Error processing app credit purchase", appError); - throw appError instanceof Error ? appError : new Error(String(appError)); + logger.error( + "[Stripe Webhook] Error processing app credit purchase", + appError, + ); + throw appError instanceof Error + ? appError + : new Error(String(appError)); } } else if (!isDuplicate) { // Regular credit purchase (not app-specific) — skip if duplicate (credits already added) @@ -228,13 +251,18 @@ async function handleStripeWebhook(req: NextRequest) { stripePaymentIntentId: paymentIntentId, }); - logger.info(`[Stripe Webhook] Credits added: ${credits} to org ${organizationId}`); + logger.info( + `[Stripe Webhook] Credits added: ${credits} to org ${organizationId}`, + ); // Invalidate org rate limit tier cache so it recalculates on next request invalidateOrgTierCache(organizationId).catch((err) => - logger.warn("[Stripe Webhook] Failed to invalidate org tier cache", { - error: err instanceof Error ? err.message : String(err), - }), + logger.warn( + "[Stripe Webhook] Failed to invalidate org tier cache", + { + error: err instanceof Error ? err.message : String(err), + }, + ), ); if (userId) { @@ -260,7 +288,10 @@ async function handleStripeWebhook(req: NextRequest) { // Revenue splits: run even when isDuplicate so retries can complete failed splits; dedupeBySourceId keeps payout inserts idempotent if (!isAppPurchase && userId) { - const { splits } = await referralsService.calculateRevenueSplits(userId, credits); + const { splits } = await referralsService.calculateRevenueSplits( + userId, + credits, + ); if (splits.length > 0) { logger.info( `[Stripe Webhook] Processing revenue splits for $${credits.toFixed(2)} purchase by user ${userId}`, @@ -271,7 +302,9 @@ async function handleStripeWebhook(req: NextRequest) { for (const split of splits) { if (split.amount <= 0) continue; const source = - split.role === "app_owner" ? "app_owner_revenue_share" : "creator_revenue_share"; + split.role === "app_owner" + ? "app_owner_revenue_share" + : "creator_revenue_share"; try { await redeemableEarningsService.addEarnings({ userId: split.userId, @@ -294,7 +327,10 @@ async function handleStripeWebhook(req: NextRequest) { logger.error( `[Stripe Webhook] Failed to credit split to ${split.role} (${split.userId}) - retry webhook or reconcile manually`, { - error: splitError instanceof Error ? splitError.message : String(splitError), + error: + splitError instanceof Error + ? splitError.message + : String(splitError), amount: split.amount, paymentIntentId, sourceId: `revenue_split:${paymentIntentId}:${split.userId}`, @@ -306,7 +342,10 @@ async function handleStripeWebhook(req: NextRequest) { split_user_id: split.userId, split_role: split.role, split_amount: split.amount, - error: splitError instanceof Error ? splitError.message : String(splitError), + error: + splitError instanceof Error + ? splitError.message + : String(splitError), }); return NextResponse.json( { @@ -322,7 +361,9 @@ async function handleStripeWebhook(req: NextRequest) { if (!isDuplicate) { organizationsRepository.findById(organizationId).then((org) => { - const user = userId ? usersRepository.findById(userId) : Promise.resolve(null); + const user = userId + ? usersRepository.findById(userId) + : Promise.resolve(null); user.then((userData) => { discordService .logPaymentReceived({ @@ -335,12 +376,18 @@ async function handleStripeWebhook(req: NextRequest) { userId: userId || undefined, userName: userData?.name || userData?.email, paymentMethod: "stripe", - paymentType: purchaseType === "credit_pack" ? "Credit Pack" : "Balance Top-up", + paymentType: + purchaseType === "credit_pack" + ? "Credit Pack" + : "Balance Top-up", }) .catch((err) => { - logger.error("[Stripe Webhook] Failed to log payment to Discord", { - error: err, - }); + logger.error( + "[Stripe Webhook] Failed to log payment to Discord", + { + error: err, + }, + ); }); }); }); @@ -348,9 +395,8 @@ async function handleStripeWebhook(req: NextRequest) { if (!isDuplicate) { try { - const existingInvoice = await invoicesService.getByStripeInvoiceId( - `cs_${session.id}`, - ); + const existingInvoice = + await invoicesService.getByStripeInvoiceId(`cs_${session.id}`); if (!existingInvoice) { const amountTotal = session.amount_total @@ -379,7 +425,9 @@ async function handleStripeWebhook(req: NextRequest) { paid_at: new Date(), }); - logger.debug(`[Stripe Webhook] Invoice created for checkout session ${session.id}`); + logger.debug( + `[Stripe Webhook] Invoice created for checkout session ${session.id}`, + ); } else { logger.debug( `[Stripe Webhook] Invoice already exists for checkout session ${session.id}`, @@ -398,7 +446,9 @@ async function handleStripeWebhook(req: NextRequest) { case "payment_intent.succeeded": { const paymentIntent = event.data.object; - logger.debug(`[Stripe Webhook] Payment intent succeeded: ${paymentIntent.id}`); + logger.debug( + `[Stripe Webhook] Payment intent succeeded: ${paymentIntent.id}`, + ); // One-time and auto-top-up use PaymentIntent directly (no checkout session). // WHY no revenue splits here: Auto top-up has no user_id in metadata by design; @@ -434,11 +484,16 @@ async function handleStripeWebhook(req: NextRequest) { } const affiliateFeeStr = paymentIntent.metadata?.affiliate_fee_amount; - const affiliateFeeAmount = affiliateFeeStr ? Number.parseFloat(affiliateFeeStr) : 0; + const affiliateFeeAmount = affiliateFeeStr + ? Number.parseFloat(affiliateFeeStr) + : 0; const affiliateOwnerId = paymentIntent.metadata?.affiliate_owner_id; const affiliateCodeId = paymentIntent.metadata?.affiliate_code_id; - if (affiliateFeeStr && (!Number.isFinite(affiliateFeeAmount) || affiliateFeeAmount <= 0)) { + if ( + affiliateFeeStr && + (!Number.isFinite(affiliateFeeAmount) || affiliateFeeAmount <= 0) + ) { logger.warn( `[Stripe Webhook] Permanent failure - Invalid affiliate metadata in payment intent ${paymentIntent.id}`, { affiliateFeeStr }, @@ -454,9 +509,10 @@ async function handleStripeWebhook(req: NextRequest) { } // Check for duplicate transaction - const existingTransaction = await creditsService.getTransactionByStripePaymentIntent( - paymentIntent.id, - ); + const existingTransaction = + await creditsService.getTransactionByStripePaymentIntent( + paymentIntent.id, + ); const isDuplicate = !!existingTransaction; if (isDuplicate) { @@ -490,9 +546,12 @@ async function handleStripeWebhook(req: NextRequest) { // Invalidate org rate limit tier cache so it recalculates on next request invalidateOrgTierCache(organizationId).catch((err) => - logger.warn("[Stripe Webhook] Failed to invalidate org tier cache", { - error: err instanceof Error ? err.message : String(err), - }), + logger.warn( + "[Stripe Webhook] Failed to invalidate org tier cache", + { + error: err instanceof Error ? err.message : String(err), + }, + ), ); // Log payment to Discord (fire and forget) @@ -506,10 +565,16 @@ async function handleStripeWebhook(req: NextRequest) { organizationId, organizationName: org?.name, paymentMethod: "stripe", - paymentType: purchaseType === "auto_top_up" ? "Auto Top-up" : "One-time Purchase", + paymentType: + purchaseType === "auto_top_up" + ? "Auto Top-up" + : "One-time Purchase", }) .catch((err) => { - logger.error("[Stripe Webhook] Failed to log payment to Discord", { error: err }); + logger.error( + "[Stripe Webhook] Failed to log payment to Discord", + { error: err }, + ); }); }); } @@ -557,7 +622,9 @@ async function handleStripeWebhook(req: NextRequest) { `[Stripe Webhook] Failed to credit auto top-up affiliate payout for ${paymentIntent.id}`, { error: - affiliateError instanceof Error ? affiliateError.message : String(affiliateError), + affiliateError instanceof Error + ? affiliateError.message + : String(affiliateError), affiliateOwnerId, affiliateCodeId, }, @@ -573,7 +640,10 @@ async function handleStripeWebhook(req: NextRequest) { } if (isDuplicate) { - return NextResponse.json({ received: true, duplicate: true }, { status: 200 }); + return NextResponse.json( + { received: true, duplicate: true }, + { status: 200 }, + ); } try { @@ -590,7 +660,8 @@ async function handleStripeWebhook(req: NextRequest) { ? invoiceIdOrObject.id : invoiceIdOrObject; - const existingInvoice = await invoicesService.getByStripeInvoiceId(invoiceId); + const existingInvoice = + await invoicesService.getByStripeInvoiceId(invoiceId); if (!existingInvoice) { const stripeInvoice = await stripe.invoices.retrieve(invoiceId); @@ -607,7 +678,8 @@ async function handleStripeWebhook(req: NextRequest) { invoice_type: purchaseType || "one_time_purchase", invoice_number: stripeInvoice.number || undefined, invoice_pdf: stripeInvoice.invoice_pdf || undefined, - hosted_invoice_url: stripeInvoice.hosted_invoice_url || undefined, + hosted_invoice_url: + stripeInvoice.hosted_invoice_url || undefined, credits_added: credits.toString(), metadata: { type: purchaseType, @@ -660,7 +732,10 @@ async function handleStripeWebhook(req: NextRequest) { } catch (invoiceError) { // Invoice creation failure is not critical - log but don't fail the webhook // The credits were already added successfully - logger.error("[Stripe Webhook] Non-critical error creating invoice record", invoiceError); + logger.error( + "[Stripe Webhook] Non-critical error creating invoice record", + invoiceError, + ); } break; @@ -668,7 +743,9 @@ async function handleStripeWebhook(req: NextRequest) { case "payment_intent.payment_failed": { const paymentIntent = event.data.object; - logger.warn(`[Stripe Webhook] Payment intent failed: ${paymentIntent.id}`); + logger.warn( + `[Stripe Webhook] Payment intent failed: ${paymentIntent.id}`, + ); // Track payment failure in PostHog const orgId = paymentIntent.metadata?.organization_id; @@ -681,7 +758,10 @@ async function handleStripeWebhook(req: NextRequest) { // Get error reason from the payment intent const lastPaymentError = paymentIntent.last_payment_error; - const errorReason = lastPaymentError?.message || lastPaymentError?.code || "Payment failed"; + const errorReason = + lastPaymentError?.message || + lastPaymentError?.code || + "Payment failed"; // Use org-prefixed ID as fallback when user ID is missing (matches auto-top-up pattern) const trackingId = userId || (orgId ? `org:${orgId}` : null); @@ -698,12 +778,15 @@ async function handleStripeWebhook(req: NextRequest) { }); } else { // Log warning when metadata is missing - creates blind spot in failure analytics - logger.warn(`[Stripe Webhook] Cannot track checkout_failed - missing metadata`, { - paymentIntentId: paymentIntent.id, - hasUserId: !!userId, - hasOrgId: !!orgId, - errorReason, - }); + logger.warn( + `[Stripe Webhook] Cannot track checkout_failed - missing metadata`, + { + paymentIntentId: paymentIntent.id, + hasUserId: !!userId, + hasOrgId: !!orgId, + errorReason, + }, + ); } break; @@ -715,7 +798,8 @@ async function handleStripeWebhook(req: NextRequest) { return NextResponse.json({ received: true }, { status: 200 }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error( `[Stripe Webhook] Error processing event ${event.type} (${event.id}):`, @@ -738,7 +822,9 @@ async function handleStripeWebhook(req: NextRequest) { if (isPermanentError) { // Return 200 for permanent errors to prevent retries - logger.warn("[Stripe Webhook] Permanent error detected, returning 200 to prevent retries"); + logger.warn( + "[Stripe Webhook] Permanent error detected, returning 200 to prevent retries", + ); return NextResponse.json( { received: true, @@ -753,7 +839,9 @@ async function handleStripeWebhook(req: NextRequest) { // Return 500 for transient errors to trigger Stripe retry logic // (database issues, network issues, temporary service unavailability) - logger.warn("[Stripe Webhook] Transient error detected, returning 500 to trigger retry"); + logger.warn( + "[Stripe Webhook] Transient error detected, returning 500 to trigger retry", + ); return NextResponse.json( { error: "Transient error - will retry", @@ -767,4 +855,7 @@ async function handleStripeWebhook(req: NextRequest) { } // Export rate-limited handler -export const POST = withRateLimit(handleStripeWebhook, RateLimitPresets.AGGRESSIVE); +export const POST = withRateLimit( + handleStripeWebhook, + RateLimitPresets.AGGRESSIVE, +); diff --git a/app/api/test/auth/session/route.ts b/app/api/test/auth/session/route.ts index 3fe50aa85..5d5c03c14 100644 --- a/app/api/test/auth/session/route.ts +++ b/app/api/test/auth/session/route.ts @@ -50,11 +50,17 @@ export async function POST(request: NextRequest) { const user = await usersService.getWithOrganization(apiKey.user_id); if (!user || !user.organization_id || !user.organization) { - return NextResponse.json({ error: "User organization not found" }, { status: 403 }); + return NextResponse.json( + { error: "User organization not found" }, + { status: 403 }, + ); } if (!user.is_active || !user.organization.is_active) { - return NextResponse.json({ error: "User or organization is inactive" }, { status: 403 }); + return NextResponse.json( + { error: "User or organization is inactive" }, + { status: 403 }, + ); } const token = createPlaywrightTestSessionToken(user.id, user.organization_id); diff --git a/app/api/training/trajectories/export/route.ts b/app/api/training/trajectories/export/route.ts index fd5a20f82..2a40e2629 100644 --- a/app/api/training/trajectories/export/route.ts +++ b/app/api/training/trajectories/export/route.ts @@ -1,6 +1,9 @@ import type { NextRequest } from "next/server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { llmTrajectoryService, type TrajectoryExportOptions } from "@/lib/services/llm-trajectory"; +import { + llmTrajectoryService, + type TrajectoryExportOptions, +} from "@/lib/services/llm-trajectory"; export const dynamic = "force-dynamic"; @@ -10,18 +13,25 @@ function parseDate(value: string | null): Date | undefined { return Number.isNaN(parsed.getTime()) ? undefined : parsed; } -function resolveExportOptions(input: Record): TrajectoryExportOptions { +function resolveExportOptions( + input: Record, +): TrajectoryExportOptions { return { model: typeof input.model === "string" ? input.model : undefined, purpose: typeof input.purpose === "string" ? input.purpose : undefined, - startDate: typeof input.startDate === "string" ? parseDate(input.startDate) : undefined, - endDate: typeof input.endDate === "string" ? parseDate(input.endDate) : undefined, + startDate: + typeof input.startDate === "string" + ? parseDate(input.startDate) + : undefined, + endDate: + typeof input.endDate === "string" ? parseDate(input.endDate) : undefined, limit: typeof input.limit === "number" ? input.limit : undefined, }; } function buildJsonlResponse(jsonl: string) { - const lineCount = jsonl.trim().length > 0 ? jsonl.trim().split("\n").length : 0; + const lineCount = + jsonl.trim().length > 0 ? jsonl.trim().split("\n").length : 0; return Response.json({ jsonl, lineCount, @@ -37,14 +47,22 @@ export async function GET(request: NextRequest) { purpose: searchParams.get("purpose"), startDate: searchParams.get("startDate"), endDate: searchParams.get("endDate"), - limit: searchParams.get("limit") ? Number(searchParams.get("limit")) : undefined, + limit: searchParams.get("limit") + ? Number(searchParams.get("limit")) + : undefined, }); - const jsonl = await llmTrajectoryService.exportAsTrainingJSONL(user.organization_id, options); + const jsonl = await llmTrajectoryService.exportAsTrainingJSONL( + user.organization_id, + options, + ); return buildJsonlResponse(jsonl); } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : "Failed to export trajectories", + error: + error instanceof Error + ? error.message + : "Failed to export trajectories", }, { status: 500 }, ); @@ -54,14 +72,23 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); - const body = ((await request.json().catch(() => ({}))) ?? {}) as Record; + const body = ((await request.json().catch(() => ({}))) ?? {}) as Record< + string, + unknown + >; const options = resolveExportOptions(body); - const jsonl = await llmTrajectoryService.exportAsTrainingJSONL(user.organization_id, options); + const jsonl = await llmTrajectoryService.exportAsTrainingJSONL( + user.organization_id, + options, + ); return buildJsonlResponse(jsonl); } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : "Failed to export trajectories", + error: + error instanceof Error + ? error.message + : "Failed to export trajectories", }, { status: 500 }, ); diff --git a/app/api/training/vertex/assignments/route.ts b/app/api/training/vertex/assignments/route.ts index 6e81ce425..91f449af4 100644 --- a/app/api/training/vertex/assignments/route.ts +++ b/app/api/training/vertex/assignments/route.ts @@ -13,7 +13,9 @@ function parseScope(value: unknown): "global" | "organization" | "user" { async function ensureGlobalAccess(request: NextRequest): Promise { const admin = await requireAdmin(request); if (admin.role !== "super_admin") { - throw new Error("Global tuned-model assignments require super-admin access."); + throw new Error( + "Global tuned-model assignments require super-admin access.", + ); } } @@ -41,7 +43,10 @@ export async function GET(request: NextRequest) { } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : "Failed to list tuned-model assignments", + error: + error instanceof Error + ? error.message + : "Failed to list tuned-model assignments", }, { status: 500 }, ); @@ -51,7 +56,10 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); - const body = ((await request.json().catch(() => ({}))) ?? {}) as Record; + const body = ((await request.json().catch(() => ({}))) ?? {}) as Record< + string, + unknown + >; const scope = parseScope(body.scope); if (scope === "global") { @@ -59,7 +67,8 @@ export async function POST(request: NextRequest) { } const slot = typeof body.slot === "string" ? body.slot : undefined; - const tunedModelId = typeof body.tunedModelId === "string" ? body.tunedModelId : undefined; + const tunedModelId = + typeof body.tunedModelId === "string" ? body.tunedModelId : undefined; if (!slot || !tunedModelId) { return Response.json( @@ -78,7 +87,9 @@ export async function POST(request: NextRequest) { userId: scope === "user" ? user.id : undefined, assignedByUserId: user.id, metadata: - body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) + body.metadata && + typeof body.metadata === "object" && + !Array.isArray(body.metadata) ? (body.metadata as Record) : undefined, }); @@ -86,7 +97,9 @@ export async function POST(request: NextRequest) { return Response.json({ assignment }, { status: 201 }); } catch (error) { const message = - error instanceof Error ? error.message : "Failed to activate tuned-model assignment"; + error instanceof Error + ? error.message + : "Failed to activate tuned-model assignment"; return Response.json( { error: message, @@ -99,7 +112,10 @@ export async function POST(request: NextRequest) { export async function DELETE(request: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); - const body = ((await request.json().catch(() => ({}))) ?? {}) as Record; + const body = ((await request.json().catch(() => ({}))) ?? {}) as Record< + string, + unknown + >; const scope = parseScope(body.scope); if (scope === "global") { @@ -116,17 +132,20 @@ export async function DELETE(request: NextRequest) { ); } - const deactivatedCount = await vertexModelRegistryService.deactivateAssignment({ - scope, - slot: slot as any, - organizationId: scope === "global" ? undefined : user.organization_id, - userId: scope === "user" ? user.id : undefined, - }); + const deactivatedCount = + await vertexModelRegistryService.deactivateAssignment({ + scope, + slot: slot as any, + organizationId: scope === "global" ? undefined : user.organization_id, + userId: scope === "user" ? user.id : undefined, + }); return Response.json({ deactivatedCount }); } catch (error) { const message = - error instanceof Error ? error.message : "Failed to deactivate tuned-model assignment"; + error instanceof Error + ? error.message + : "Failed to deactivate tuned-model assignment"; return Response.json( { error: message, diff --git a/app/api/training/vertex/jobs/route.ts b/app/api/training/vertex/jobs/route.ts index 4eaa23268..3f63c682b 100644 --- a/app/api/training/vertex/jobs/route.ts +++ b/app/api/training/vertex/jobs/route.ts @@ -1,7 +1,10 @@ import type { NextRequest } from "next/server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { vertexModelRegistryService } from "@/lib/services/vertex-model-registry"; -import { getTuningJobStatus, listTuningJobs } from "@/lib/services/vertex-tuning"; +import { + getTuningJobStatus, + listTuningJobs, +} from "@/lib/services/vertex-tuning"; export const dynamic = "force-dynamic"; @@ -9,7 +12,8 @@ export async function GET(request: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { searchParams } = new URL(request.url); - const projectId = searchParams.get("projectId") || process.env.GOOGLE_CLOUD_PROJECT; + const projectId = + searchParams.get("projectId") || process.env.GOOGLE_CLOUD_PROJECT; const region = searchParams.get("region") || "us-central1"; const jobName = searchParams.get("name"); const jobId = searchParams.get("jobId"); @@ -18,7 +22,10 @@ export async function GET(request: NextRequest) { if (jobId) { const synced = await vertexModelRegistryService.syncJobStatus({ jobId }); if (!synced) { - return Response.json({ error: "Tracked Vertex job not found" }, { status: 404 }); + return Response.json( + { error: "Tracked Vertex job not found" }, + { status: 404 }, + ); } return Response.json({ @@ -65,7 +72,10 @@ export async function GET(request: NextRequest) { } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : "Failed to query Vertex jobs", + error: + error instanceof Error + ? error.message + : "Failed to query Vertex jobs", }, { status: 500 }, ); diff --git a/app/api/training/vertex/models/route.ts b/app/api/training/vertex/models/route.ts index 03acdc58a..291783491 100644 --- a/app/api/training/vertex/models/route.ts +++ b/app/api/training/vertex/models/route.ts @@ -52,7 +52,10 @@ export async function GET(request: NextRequest) { } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : "Failed to list tuned models", + error: + error instanceof Error + ? error.message + : "Failed to list tuned models", }, { status: 500 }, ); diff --git a/app/api/training/vertex/tune/route.ts b/app/api/training/vertex/tune/route.ts index d40fc18d1..e719bcbf5 100644 --- a/app/api/training/vertex/tune/route.ts +++ b/app/api/training/vertex/tune/route.ts @@ -5,7 +5,10 @@ import type { NextRequest } from "next/server"; import { requireAdmin, requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { llmTrajectoryService } from "@/lib/services/llm-trajectory"; import { vertexModelRegistryService } from "@/lib/services/vertex-model-registry"; -import { normalizeVertexBaseModel, orchestrateVertexTuning } from "@/lib/services/vertex-tuning"; +import { + normalizeVertexBaseModel, + orchestrateVertexTuning, +} from "@/lib/services/vertex-tuning"; export const dynamic = "force-dynamic"; @@ -28,10 +31,15 @@ export async function POST(request: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); - const body = ((await request.json().catch(() => ({}))) ?? {}) as Record; + const body = ((await request.json().catch(() => ({}))) ?? {}) as Record< + string, + unknown + >; const scope = typeof body.scope === "string" && - (body.scope === "global" || body.scope === "organization" || body.scope === "user") + (body.scope === "global" || + body.scope === "organization" || + body.scope === "user") ? body.scope : "organization"; @@ -48,7 +56,8 @@ export async function POST(request: NextRequest) { } const projectId = - (typeof body.projectId === "string" && body.projectId) || process.env.GOOGLE_CLOUD_PROJECT; + (typeof body.projectId === "string" && body.projectId) || + process.env.GOOGLE_CLOUD_PROJECT; const gcsBucket = (typeof body.gcsBucket === "string" && body.gcsBucket) || process.env.GOOGLE_CLOUD_TUNING_BUCKET; @@ -63,22 +72,34 @@ export async function POST(request: NextRequest) { ); } - const slot = (typeof body.slot === "string" && body.slot) || "should_respond"; + const slot = + (typeof body.slot === "string" && body.slot) || "should_respond"; const displayName = (typeof body.displayName === "string" && body.displayName) || `eliza-cloud-${slot.replace(/_/g, "-")}-${Date.now()}`; let trainingDataPath = - typeof body.trainingDataPath === "string" ? body.trainingDataPath : undefined; + typeof body.trainingDataPath === "string" + ? body.trainingDataPath + : undefined; const validationDataPath = - typeof body.validationDataPath === "string" ? body.validationDataPath : undefined; + typeof body.validationDataPath === "string" + ? body.validationDataPath + : undefined; let generatedFromTrajectories = false; if (!trainingDataPath) { - const jsonl = await llmTrajectoryService.exportAsTrainingJSONL(user.organization_id, { - purpose: typeof body.purpose === "string" ? body.purpose : defaultPurposeForSlot(slot), - model: typeof body.modelFilter === "string" ? body.modelFilter : undefined, - limit: typeof body.limit === "number" ? body.limit : 5000, - }); + const jsonl = await llmTrajectoryService.exportAsTrainingJSONL( + user.organization_id, + { + purpose: + typeof body.purpose === "string" + ? body.purpose + : defaultPurposeForSlot(slot), + model: + typeof body.modelFilter === "string" ? body.modelFilter : undefined, + limit: typeof body.limit === "number" ? body.limit : 5000, + }, + ); if (!jsonl.trim()) { return Response.json( @@ -90,7 +111,9 @@ export async function POST(request: NextRequest) { ); } - const tempDir = await mkdtemp(path.join(os.tmpdir(), "eliza-cloud-vertex-")); + const tempDir = await mkdtemp( + path.join(os.tmpdir(), "eliza-cloud-vertex-"), + ); tempDirs.push(tempDir); trainingDataPath = path.join(tempDir, "training.jsonl"); await writeFile(trainingDataPath, `${jsonl.trim()}\n`); @@ -112,8 +135,13 @@ export async function POST(request: NextRequest) { slot: slot as any, scope: scope as any, ownerId: - scope === "user" ? user.id : scope === "organization" ? user.organization_id : undefined, - accessToken: typeof body.accessToken === "string" ? body.accessToken : undefined, + scope === "user" + ? user.id + : scope === "organization" + ? user.organization_id + : undefined, + accessToken: + typeof body.accessToken === "string" ? body.accessToken : undefined, }); const persisted = await vertexModelRegistryService.recordSubmittedJob({ @@ -154,11 +182,16 @@ export async function POST(request: NextRequest) { } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : "Failed to submit Vertex tuning job", + error: + error instanceof Error + ? error.message + : "Failed to submit Vertex tuning job", }, { status: 500 }, ); } finally { - await Promise.all(tempDirs.map((tempDir) => rm(tempDir, { recursive: true, force: true }))); + await Promise.all( + tempDirs.map((tempDir) => rm(tempDir, { recursive: true, force: true })), + ); } } diff --git a/app/api/v1/admin/ai-pricing/route.ts b/app/api/v1/admin/ai-pricing/route.ts index 4b6e59f3b..f92add2d4 100644 --- a/app/api/v1/admin/ai-pricing/route.ts +++ b/app/api/v1/admin/ai-pricing/route.ts @@ -13,10 +13,25 @@ import { } from "@/lib/services/ai-pricing"; const OverrideSchema = z.object({ - billingSource: z.enum(["gateway", "openrouter", "openai", "groq", "fal", "elevenlabs"]), + billingSource: z.enum([ + "gateway", + "openrouter", + "openai", + "groq", + "fal", + "elevenlabs", + ]), provider: z.string().min(1), model: z.string().min(1), - productFamily: z.enum(["language", "embedding", "image", "video", "tts", "stt", "voice_clone"]), + productFamily: z.enum([ + "language", + "embedding", + "image", + "video", + "tts", + "stt", + "voice_clone", + ]), chargeType: z.string().min(1), unit: z.enum([ "token", @@ -30,17 +45,25 @@ const OverrideSchema = z.object({ ]), unitPrice: z.number().positive(), dimensions: z - .record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])) + .record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.null()]), + ) .optional(), reason: z.string().min(1), }); const RefreshSchema = z.object({ - sources: z.array(z.enum(["gateway", "openrouter", "fal", "elevenlabs"])).optional(), + sources: z + .array(z.enum(["gateway", "openrouter", "fal", "elevenlabs"])) + .optional(), }); export async function GET(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] AI pricing auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] AI pricing auth error", + ); if (authResult instanceof NextResponse) { return authResult; } @@ -70,7 +93,10 @@ export async function GET(request: NextRequest) { } export async function POST(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] AI pricing auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] AI pricing auth error", + ); if (authResult instanceof NextResponse) { return authResult; } @@ -82,7 +108,10 @@ export async function POST(request: NextRequest) { } export async function PUT(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] AI pricing auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] AI pricing auth error", + ); if (authResult instanceof NextResponse) { return authResult; } diff --git a/app/api/v1/admin/docker-containers/audit/route.ts b/app/api/v1/admin/docker-containers/audit/route.ts index 846c8d07c..e8b88e644 100644 --- a/app/api/v1/admin/docker-containers/audit/route.ts +++ b/app/api/v1/admin/docker-containers/audit/route.ts @@ -60,7 +60,10 @@ export async function POST(request: NextRequest) { nodeId: node.node_id, hostname: node.hostname, ghostContainers: [] as string[], - orphanRecords: [] as Array<{ id: string; containerName: string | null }>, + orphanRecords: [] as Array<{ + id: string; + containerName: string | null; + }>, error: undefined as string | undefined, }; @@ -73,7 +76,10 @@ export async function POST(request: NextRequest) { }) .from(miladySandboxes) .where( - and(eq(miladySandboxes.node_id, node.node_id), ne(miladySandboxes.status, "stopped")), + and( + eq(miladySandboxes.node_id, node.node_id), + ne(miladySandboxes.status, "stopped"), + ), ); // Get actual running containers on the node via SSH @@ -104,7 +110,9 @@ export async function POST(request: NextRequest) { const actualSet = new Set(actualContainers); const dbNameSet = new Set( - dbContainers.map((c) => c.containerName).filter((n): n is string => n !== null), + dbContainers + .map((c) => c.containerName) + .filter((n): n is string => n !== null), ); // Ghost containers: running on node but not in DB @@ -150,7 +158,9 @@ export async function POST(request: NextRequest) { } else { const node = nodes[i]; const errorMsg = - settled.reason instanceof Error ? settled.reason.message : "Failed to audit node"; + settled.reason instanceof Error + ? settled.reason.message + : "Failed to audit node"; logger.warn("[Admin Docker Audit] Node audit failed", { nodeId: node.node_id, error: errorMsg, @@ -172,8 +182,15 @@ export async function POST(request: NextRequest) { }); // Build flat arrays matching the UI AuditResult interface - const ghostContainers: Array<{ nodeId: string; hostname: string; names: string[] }> = []; - const allOrphanRecords: Array<{ id: string; containerName: string | null }> = []; + const ghostContainers: Array<{ + nodeId: string; + hostname: string; + names: string[]; + }> = []; + const allOrphanRecords: Array<{ + id: string; + containerName: string | null; + }> = []; for (const result of auditResults) { if (result.ghostContainers.length > 0) { @@ -202,6 +219,9 @@ export async function POST(request: NextRequest) { logger.error("[Admin Docker Audit] Audit failed", { error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ success: false, error: "Container audit failed" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Container audit failed" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/admin/docker-containers/logs-since.ts b/app/api/v1/admin/docker-containers/logs-since.ts index ff7f45b1a..9948839a1 100644 --- a/app/api/v1/admin/docker-containers/logs-since.ts +++ b/app/api/v1/admin/docker-containers/logs-since.ts @@ -6,7 +6,8 @@ const RELATIVE_SINCE_RE = /^\d+[smhdw]$/; * YYYY-MM-DDTHH:MM:SS.sssZ * Rejects locale-dependent strings like "yesterday" or "March 9, 2026". */ -const ISO8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/; +const ISO8601_RE = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/; export function isValidDockerLogsSince(value: string): boolean { if (!value) return false; diff --git a/app/api/v1/admin/docker-containers/route.ts b/app/api/v1/admin/docker-containers/route.ts index 89505948e..cf7235816 100644 --- a/app/api/v1/admin/docker-containers/route.ts +++ b/app/api/v1/admin/docker-containers/route.ts @@ -12,7 +12,10 @@ import { and, desc, eq, isNotNull, type SQL, sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; import { dbRead } from "@/db/helpers"; -import { type MiladySandboxStatus, miladySandboxes } from "@/db/schemas/milady-sandboxes"; +import { + type MiladySandboxStatus, + miladySandboxes, +} from "@/db/schemas/milady-sandboxes"; import { requireAdmin } from "@/lib/auth"; import { getStewardAgent } from "@/lib/services/steward-client"; import { logger } from "@/lib/utils/logger"; @@ -78,7 +81,9 @@ export async function GET(request: NextRequest) { { status: 400 }, ); } - conditions.push(eq(miladySandboxes.status, statusFilter as MiladySandboxStatus)); + conditions.push( + eq(miladySandboxes.status, statusFilter as MiladySandboxStatus), + ); } if (nodeFilter) { diff --git a/app/api/v1/admin/docker-nodes/[nodeId]/health-check/route.ts b/app/api/v1/admin/docker-nodes/[nodeId]/health-check/route.ts index ee357b7ec..f915bb4f1 100644 --- a/app/api/v1/admin/docker-nodes/[nodeId]/health-check/route.ts +++ b/app/api/v1/admin/docker-nodes/[nodeId]/health-check/route.ts @@ -128,7 +128,9 @@ async function runNodeHealthCheck(node: { try { // 2. Docker daemon check - const dockerVersion = await ssh.exec("docker version --format '{{.Server.Version}}'"); + const dockerVersion = await ssh.exec( + "docker version --format '{{.Server.Version}}'", + ); checks.docker = { ok: true, version: dockerVersion.trim(), @@ -160,7 +162,9 @@ async function runNodeHealthCheck(node: { try { // 4. Container count - const psOutput = await ssh.exec("docker ps -a --format '{{.State}}' 2>/dev/null || true"); + const psOutput = await ssh.exec( + "docker ps -a --format '{{.State}}' 2>/dev/null || true", + ); const lines = psOutput.trim().split("\n").filter(Boolean); const running = lines.filter((l) => l === "running").length; checks.containers = { running, total: lines.length }; @@ -171,7 +175,8 @@ async function runNodeHealthCheck(node: { await cleanupSSH(ssh); // Determine overall status - const isDegraded = (checks.diskUsage && !checks.diskUsage.ok) || !checks.docker.ok; + const isDegraded = + (checks.diskUsage && !checks.diskUsage.ok) || !checks.docker.ok; return { status: isDegraded ? "degraded" : "healthy", diff --git a/app/api/v1/admin/docker-nodes/[nodeId]/route.ts b/app/api/v1/admin/docker-nodes/[nodeId]/route.ts index 120eeed7e..5dbd5fc7c 100644 --- a/app/api/v1/admin/docker-nodes/[nodeId]/route.ts +++ b/app/api/v1/admin/docker-nodes/[nodeId]/route.ts @@ -35,8 +35,12 @@ export async function GET(request: NextRequest, { params }: RouteParams) { ); } } catch (error) { - const message = error instanceof Error ? error.message : "Admin access required"; - return NextResponse.json({ success: false, error: message }, { status: 403 }); + const message = + error instanceof Error ? error.message : "Admin access required"; + return NextResponse.json( + { success: false, error: message }, + { status: 403 }, + ); } const { nodeId } = await params; @@ -126,8 +130,12 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { ); } } catch (error) { - const message = error instanceof Error ? error.message : "Admin access required"; - return NextResponse.json({ success: false, error: message }, { status: 403 }); + const message = + error instanceof Error ? error.message : "Admin access required"; + return NextResponse.json( + { success: false, error: message }, + { status: 403 }, + ); } const { nodeId } = await params; @@ -136,7 +144,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON body" }, + { status: 400 }, + ); } const parsed = updateNodeSchema.safeParse(body); @@ -160,8 +171,15 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { ); } - const { hostname, enabled, capacity, sshPort, sshUser, hostKeyFingerprint, metadata } = - parsed.data; + const { + hostname, + enabled, + capacity, + sshPort, + sshUser, + hostKeyFingerprint, + metadata, + } = parsed.data; const updateData: Record = {}; if (hostname !== undefined) updateData.hostname = hostname; @@ -169,7 +187,8 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { if (capacity !== undefined) updateData.capacity = capacity; if (sshPort !== undefined) updateData.ssh_port = sshPort; if (sshUser !== undefined) updateData.ssh_user = sshUser; - if (hostKeyFingerprint !== undefined) updateData.host_key_fingerprint = hostKeyFingerprint; + if (hostKeyFingerprint !== undefined) + updateData.host_key_fingerprint = hostKeyFingerprint; if (metadata !== undefined) updateData.metadata = metadata; const updated = await dockerNodesRepository.update(existing.id, updateData); @@ -224,8 +243,12 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { ); } } catch (error) { - const message = error instanceof Error ? error.message : "Admin access required"; - return NextResponse.json({ success: false, error: message }, { status: 403 }); + const message = + error instanceof Error ? error.message : "Admin access required"; + return NextResponse.json( + { success: false, error: message }, + { status: 403 }, + ); } const { nodeId } = await params; @@ -243,7 +266,12 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const activeContainers = await dbRead .select({ id: miladySandboxes.id }) .from(miladySandboxes) - .where(and(eq(miladySandboxes.node_id, nodeId), ne(miladySandboxes.status, "stopped"))); + .where( + and( + eq(miladySandboxes.node_id, nodeId), + ne(miladySandboxes.status, "stopped"), + ), + ); if (activeContainers.length > 0) { return NextResponse.json( diff --git a/app/api/v1/admin/docker-nodes/route.ts b/app/api/v1/admin/docker-nodes/route.ts index 2e0e990f6..9e3ba6870 100644 --- a/app/api/v1/admin/docker-nodes/route.ts +++ b/app/api/v1/admin/docker-nodes/route.ts @@ -117,7 +117,10 @@ export async function POST(request: NextRequest) { try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON body" }, + { status: 400 }, + ); } const parsed = createNodeSchema.safeParse(body); @@ -132,7 +135,8 @@ export async function POST(request: NextRequest) { ); } - const { nodeId, hostname, sshPort, capacity, sshUser, hostKeyFingerprint } = parsed.data; + const { nodeId, hostname, sshPort, capacity, sshUser, hostKeyFingerprint } = + parsed.data; try { // Check for duplicate nodeId diff --git a/app/api/v1/admin/headscale/route.ts b/app/api/v1/admin/headscale/route.ts index 0a2379e9e..c91e1dc3c 100644 --- a/app/api/v1/admin/headscale/route.ts +++ b/app/api/v1/admin/headscale/route.ts @@ -18,7 +18,8 @@ export const dynamic = "force-dynamic"; // Headscale API configuration // --------------------------------------------------------------------------- -const HEADSCALE_API_URL = process.env.HEADSCALE_API_URL || "http://localhost:8081"; +const HEADSCALE_API_URL = + process.env.HEADSCALE_API_URL || "http://localhost:8081"; const HEADSCALE_API_KEY = process.env.HEADSCALE_API_KEY || ""; const HEADSCALE_USER = process.env.HEADSCALE_USER || "milady"; @@ -40,7 +41,8 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: "Headscale not configured: HEADSCALE_API_KEY environment variable is missing", + error: + "Headscale not configured: HEADSCALE_API_KEY environment variable is missing", }, { status: 503 }, ); @@ -160,7 +162,10 @@ export async function GET(request: NextRequest) { ); } if (error instanceof ForbiddenError) { - return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Forbidden" }, + { status: 403 }, + ); } // Distinguish network errors from other failures diff --git a/app/api/v1/admin/infrastructure/containers/actions/route.ts b/app/api/v1/admin/infrastructure/containers/actions/route.ts index fa8806389..46724c1b3 100644 --- a/app/api/v1/admin/infrastructure/containers/actions/route.ts +++ b/app/api/v1/admin/infrastructure/containers/actions/route.ts @@ -29,7 +29,13 @@ const SSH_DEFAULT_TIMEOUT_MS = 30_000; const SSH_INSPECT_TIMEOUT_MS = 15_000; const SSH_PULL_TIMEOUT_MS = 120_000; -type ContainerAction = "logs" | "restart" | "stop" | "start" | "inspect" | "pull-image"; +type ContainerAction = + | "logs" + | "restart" + | "stop" + | "start" + | "inspect" + | "pull-image"; const VALID_ACTIONS = new Set([ "logs", @@ -64,7 +70,10 @@ export async function POST(request: NextRequest) { try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON body" }, + { status: 400 }, + ); } const { action, nodeId, containerName, lines = 200, since } = body; @@ -85,12 +94,18 @@ export async function POST(request: NextRequest) { // Validate nodeId (prevent enumeration with arbitrary strings) if (!/^[a-zA-Z0-9_.-]+$/.test(nodeId)) { - return NextResponse.json({ success: false, error: "Invalid node ID" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid node ID" }, + { status: 400 }, + ); } // Validate containerName (prevent injection) if (!/^[a-zA-Z0-9_.-]+$/.test(containerName)) { - return NextResponse.json({ success: false, error: "Invalid container name" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid container name" }, + { status: 400 }, + ); } const node = await dockerNodesRepository.findByNodeId(nodeId); @@ -271,7 +286,10 @@ export async function POST(request: NextRequest) { if (!image || !/^[a-zA-Z0-9_./:@-]+$/.test(image)) { return NextResponse.json( - { success: false, error: "Could not determine a valid container image" }, + { + success: false, + error: "Could not determine a valid container image", + }, { status: 400 }, ); } @@ -319,7 +337,10 @@ export async function POST(request: NextRequest) { }); // Don't expose internal SSH error details (hostnames, IPs, key fingerprints) to client return NextResponse.json( - { success: false, error: "Action failed. Check server logs for details." }, + { + success: false, + error: "Action failed. Check server logs for details.", + }, { status: 500 }, ); } finally { diff --git a/app/api/v1/admin/infrastructure/route.ts b/app/api/v1/admin/infrastructure/route.ts index 834eaa723..6ce94cfd3 100644 --- a/app/api/v1/admin/infrastructure/route.ts +++ b/app/api/v1/admin/infrastructure/route.ts @@ -30,12 +30,18 @@ export async function GET(request: NextRequest) { ); } if (error instanceof ForbiddenError) { - return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Forbidden" }, + { status: 403 }, + ); } - logger.error("[Admin Infrastructure] Failed to build infrastructure snapshot", { - error: error instanceof Error ? error.message : String(error), - }); + logger.error( + "[Admin Infrastructure] Failed to build infrastructure snapshot", + { + error: error instanceof Error ? error.message : String(error), + }, + ); return NextResponse.json( { success: false, error: "Failed to load infrastructure snapshot" }, diff --git a/app/api/v1/admin/metrics/route.ts b/app/api/v1/admin/metrics/route.ts index 76387bebd..e3ca1e958 100644 --- a/app/api/v1/admin/metrics/route.ts +++ b/app/api/v1/admin/metrics/route.ts @@ -24,7 +24,8 @@ async function handleGetMetrics(request: NextRequest): Promise { try { auth = await requireAdmin(request); } catch (error) { - const message = error instanceof Error ? error.message : "Admin access required"; + const message = + error instanceof Error ? error.message : "Admin access required"; return NextResponse.json({ error: message }, { status: 403 }); } @@ -47,13 +48,19 @@ async function handleGetMetrics(request: NextRequest): Promise { try { switch (view) { case "overview": - return NextResponse.json(await userMetricsService.getMetricsOverview(rangeDays)); + return NextResponse.json( + await userMetricsService.getMetricsOverview(rangeDays), + ); case "daily": - return NextResponse.json(await userMetricsService.getDailyMetrics(startDate, now)); + return NextResponse.json( + await userMetricsService.getDailyMetrics(startDate, now), + ); case "retention": - return NextResponse.json(await userMetricsService.getRetentionCohorts(startDate, now)); + return NextResponse.json( + await userMetricsService.getRetentionCohorts(startDate, now), + ); case "active": { const activeRangeMap: Record = { @@ -62,17 +69,26 @@ async function handleGetMetrics(request: NextRequest): Promise { "90d": "30d", }; const range = activeRangeMap[timeRange] ?? "day"; - return NextResponse.json(await userMetricsService.getActiveUsers(range)); + return NextResponse.json( + await userMetricsService.getActiveUsers(range), + ); } case "signups": - return NextResponse.json(await userMetricsService.getNewSignups(startDate, now)); + return NextResponse.json( + await userMetricsService.getNewSignups(startDate, now), + ); case "oauth": - return NextResponse.json(await userMetricsService.getOAuthConnectionRate()); + return NextResponse.json( + await userMetricsService.getOAuthConnectionRate(), + ); default: - return NextResponse.json({ error: "Unknown view parameter" }, { status: 400 }); + return NextResponse.json( + { error: "Unknown view parameter" }, + { status: 400 }, + ); } } catch (error) { logger.error("[Admin Metrics API] Query failed", { @@ -80,7 +96,10 @@ async function handleGetMetrics(request: NextRequest): Promise { timeRange, error: error instanceof Error ? error.message : String(error), }); - return NextResponse.json({ error: "Failed to fetch metrics" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch metrics" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/admin/moderation/route.ts b/app/api/v1/admin/moderation/route.ts index 360babc53..df6c67b22 100644 --- a/app/api/v1/admin/moderation/route.ts +++ b/app/api/v1/admin/moderation/route.ts @@ -28,7 +28,10 @@ import { logger } from "@/lib/utils/logger"; * - userId: For user-detail view */ export async function GET(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] Moderation GET auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Moderation GET auth error", + ); if (authResult instanceof NextResponse) { return authResult; } @@ -36,24 +39,30 @@ export async function GET(request: NextRequest) { try { const url = new URL(request.url); const view = url.searchParams.get("view") || "overview"; - const limit = Math.min(parseInt(url.searchParams.get("limit") || "100", 10), 1000); + const limit = Math.min( + parseInt(url.searchParams.get("limit") || "100", 10), + 1000, + ); const userId = url.searchParams.get("userId"); const { user, role } = authResult; switch (view) { case "overview": { - const [violations, flaggedUsers, bannedUsers, admins] = await Promise.all([ - adminService.getRecentViolations(10), - adminService.getUsersFlaggedForReview(), - adminService.getBannedUsers(), - adminService.listAdmins(), - ]); + const [violations, flaggedUsers, bannedUsers, admins] = + await Promise.all([ + adminService.getRecentViolations(10), + adminService.getUsersFlaggedForReview(), + adminService.getBannedUsers(), + adminService.listAdmins(), + ]); return NextResponse.json({ recentViolations: violations.map((v) => ({ ...v, messageText: - v.messageText.length > 100 ? v.messageText.slice(0, 100) + "..." : v.messageText, + v.messageText.length > 100 + ? v.messageText.slice(0, 100) + "..." + : v.messageText, })), totalViolations: violations.length, flaggedUsers: flaggedUsers.length, @@ -71,7 +80,9 @@ export async function GET(request: NextRequest) { return NextResponse.json({ violations: violations.map((v) => ({ ...v, - messageText: v.messageText.slice(0, 200) + (v.messageText.length > 200 ? "..." : ""), + messageText: + v.messageText.slice(0, 200) + + (v.messageText.length > 200 ? "..." : ""), })), total: violations.length, }); @@ -113,14 +124,18 @@ export async function GET(request: NextRequest) { default: return NextResponse.json( { - error: "Invalid view. Must be: overview, violations, users, admins, user-detail", + error: + "Invalid view. Must be: overview, violations, users, admins, user-detail", }, { status: 400 }, ); } } catch (error) { logger.error("[Admin] Moderation GET error", { error }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } @@ -157,7 +172,10 @@ const ActionSchema = z.object({ * - notes: Additional notes */ export async function POST(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] Moderation POST auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Moderation POST auth error", + ); if (authResult instanceof NextResponse) { return authResult; } @@ -179,13 +197,20 @@ export async function POST(request: NextRequest) { ); } - const action = parsed.data.action === "clear_flags" ? "clear_status" : parsed.data.action; + const action = + parsed.data.action === "clear_flags" + ? "clear_status" + : parsed.data.action; const userId = parsed.data.userId ?? parsed.data.targetUserId; - const walletAddress = parsed.data.walletAddress ?? parsed.data.targetWalletAddress; + const walletAddress = + parsed.data.walletAddress ?? parsed.data.targetWalletAddress; const { role, reason, notes } = parsed.data; // Check permissions for admin management - if ((action === "add_admin" || action === "revoke_admin") && adminRole !== "super_admin") { + if ( + (action === "add_admin" || action === "revoke_admin") && + adminRole !== "super_admin" + ) { return NextResponse.json( { error: "Only super_admin can manage other admins" }, { status: 403 }, @@ -203,7 +228,10 @@ export async function POST(request: NextRequest) { switch (action) { case "ban": { if (!userId) { - return NextResponse.json({ error: "userId required" }, { status: 400 }); + return NextResponse.json( + { error: "userId required" }, + { status: 400 }, + ); } await adminService.banUser({ userId, @@ -215,7 +243,10 @@ export async function POST(request: NextRequest) { case "unban": { if (!userId) { - return NextResponse.json({ error: "userId required" }, { status: 400 }); + return NextResponse.json( + { error: "userId required" }, + { status: 400 }, + ); } await adminService.unbanUser(userId, user.id); return NextResponse.json({ success: true, message: "User unbanned" }); @@ -223,7 +254,10 @@ export async function POST(request: NextRequest) { case "mark_spammer": { if (!userId) { - return NextResponse.json({ error: "userId required" }, { status: 400 }); + return NextResponse.json( + { error: "userId required" }, + { status: 400 }, + ); } await adminService.markUserAs({ userId, @@ -239,7 +273,10 @@ export async function POST(request: NextRequest) { case "mark_scammer": { if (!userId) { - return NextResponse.json({ error: "userId required" }, { status: 400 }); + return NextResponse.json( + { error: "userId required" }, + { status: 400 }, + ); } await adminService.markUserAs({ userId, @@ -255,7 +292,10 @@ export async function POST(request: NextRequest) { case "clear_status": { if (!userId) { - return NextResponse.json({ error: "userId required" }, { status: 400 }); + return NextResponse.json( + { error: "userId required" }, + { status: 400 }, + ); } await adminService.unbanUser(userId, user.id); return NextResponse.json({ @@ -266,7 +306,10 @@ export async function POST(request: NextRequest) { case "add_admin": { if (!walletAddress) { - return NextResponse.json({ error: "walletAddress required" }, { status: 400 }); + return NextResponse.json( + { error: "walletAddress required" }, + { status: 400 }, + ); } const admin = await adminService.promoteToAdmin({ walletAddress, @@ -287,17 +330,25 @@ export async function POST(request: NextRequest) { case "revoke_admin": { if (!walletAddress) { - return NextResponse.json({ error: "walletAddress required" }, { status: 400 }); + return NextResponse.json( + { error: "walletAddress required" }, + { status: 400 }, + ); } - if (walletAddress.toLowerCase() === user.wallet_address?.toLowerCase()) { + if ( + walletAddress.toLowerCase() === user.wallet_address?.toLowerCase() + ) { return NextResponse.json( { error: "Cannot revoke your own admin privileges" }, { status: 400 }, ); } - await adminService.revokeAdmin(walletAddress, user.wallet_address ?? undefined); + await adminService.revokeAdmin( + walletAddress, + user.wallet_address ?? undefined, + ); return NextResponse.json({ success: true, message: "Admin privileges revoked", @@ -309,7 +360,10 @@ export async function POST(request: NextRequest) { } } catch (error) { logger.error("[Admin] Moderation POST error", { error }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } @@ -350,7 +404,9 @@ export async function HEAD(request: NextRequest) { } // Single cached call instead of two separate DB queries - const { isAdmin, role } = await adminService.getAdminStatus(user.wallet_address); + const { isAdmin, role } = await adminService.getAdminStatus( + user.wallet_address, + ); return new NextResponse(null, { status: 200, diff --git a/app/api/v1/admin/orgs/[orgId]/rate-limits/route.ts b/app/api/v1/admin/orgs/[orgId]/rate-limits/route.ts index df944d9f7..bcaa6fbca 100644 --- a/app/api/v1/admin/orgs/[orgId]/rate-limits/route.ts +++ b/app/api/v1/admin/orgs/[orgId]/rate-limits/route.ts @@ -14,12 +14,16 @@ import { z } from "zod"; import { orgRateLimitOverridesRepository } from "@/db/repositories/org-rate-limit-overrides"; import { organizationsRepository } from "@/db/repositories/organizations"; import { requireAdminWithResponse } from "@/lib/api/admin-auth"; -import { getOrgTier, invalidateOrgTierCache } from "@/lib/services/org-rate-limits"; +import { + getOrgTier, + invalidateOrgTierCache, +} from "@/lib/services/org-rate-limits"; import { logger } from "@/lib/utils/logger"; type RouteContext = { params: Promise<{ orgId: string }> }; -const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function validateOrgId(orgId: string): NextResponse | null { if (!UUID_RE.test(orgId)) { @@ -29,7 +33,10 @@ function validateOrgId(orgId: string): NextResponse | null { } export async function GET(request: NextRequest, context: RouteContext) { - const authResult = await requireAdminWithResponse(request, "[Admin] Org rate limits auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Org rate limits auth error", + ); if (authResult instanceof NextResponse) return authResult; const { orgId } = await context.params; @@ -49,7 +56,10 @@ export async function GET(request: NextRequest, context: RouteContext) { }); } catch (error) { logger.error("[Admin] Org rate limits GET error", { error, orgId }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } @@ -63,7 +73,10 @@ const PatchSchema = z.object({ }); export async function PATCH(request: NextRequest, context: RouteContext) { - const authResult = await requireAdminWithResponse(request, "[Admin] Org rate limits auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Org rate limits auth error", + ); if (authResult instanceof NextResponse) return authResult; const { orgId } = await context.params; @@ -103,7 +116,10 @@ export async function PATCH(request: NextRequest, context: RouteContext) { try { const org = await organizationsRepository.findById(orgId); if (!org) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } // Pass values as-is: null clears a field, undefined = not provided (no change) const result = await orgRateLimitOverridesRepository.upsert({ @@ -134,12 +150,18 @@ export async function PATCH(request: NextRequest, context: RouteContext) { return NextResponse.json(result); } catch (error) { logger.error("[Admin] Org rate limits PATCH error", { error, orgId }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } export async function DELETE(request: NextRequest, context: RouteContext) { - const authResult = await requireAdminWithResponse(request, "[Admin] Org rate limits auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Org rate limits auth error", + ); if (authResult instanceof NextResponse) return authResult; const { orgId } = await context.params; @@ -158,6 +180,9 @@ export async function DELETE(request: NextRequest, context: RouteContext) { return new NextResponse(null, { status: 204 }); } catch (error) { logger.error("[Admin] Org rate limits DELETE error", { error, orgId }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/admin/service-pricing/audit/route.ts b/app/api/v1/admin/service-pricing/audit/route.ts index eb9ce73d3..ecf9f6cb9 100644 --- a/app/api/v1/admin/service-pricing/audit/route.ts +++ b/app/api/v1/admin/service-pricing/audit/route.ts @@ -17,10 +17,14 @@ export async function GET(request: NextRequest) { const serviceId = url.searchParams.get("service_id"); const rawLimit = url.searchParams.get("limit"); const parsedLimit = rawLimit ? parseInt(rawLimit, 10) : 50; - const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 500) : 50; + const limit = + Number.isFinite(parsedLimit) && parsedLimit > 0 + ? Math.min(parsedLimit, 500) + : 50; const rawOffset = url.searchParams.get("offset"); const parsedOffset = rawOffset ? parseInt(rawOffset, 10) : 0; - const offset = Number.isFinite(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0; + const offset = + Number.isFinite(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0; if (!serviceId) { return NextResponse.json( @@ -29,7 +33,11 @@ export async function GET(request: NextRequest) { ); } - const history = await servicePricingRepository.listAuditHistory(serviceId, limit, offset); + const history = await servicePricingRepository.listAuditHistory( + serviceId, + limit, + offset, + ); return NextResponse.json({ service_id: serviceId, @@ -52,6 +60,9 @@ export async function GET(request: NextRequest) { }); } catch (error) { logger.error("[Admin] Service pricing audit error", { error }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/admin/service-pricing/route.ts b/app/api/v1/admin/service-pricing/route.ts index e8b1b7555..01defe56c 100644 --- a/app/api/v1/admin/service-pricing/route.ts +++ b/app/api/v1/admin/service-pricing/route.ts @@ -27,7 +27,10 @@ import { invalidateServicePricingCache } from "@/lib/services/proxy/pricing"; import { logger } from "@/lib/utils/logger"; export async function GET(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] Service pricing auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Service pricing auth error", + ); if (authResult instanceof NextResponse) { return authResult; } @@ -36,11 +39,17 @@ export async function GET(request: NextRequest) { const serviceId = url.searchParams.get("service_id"); if (!serviceId) { - return NextResponse.json({ error: "service_id query parameter is required" }, { status: 400 }); + return NextResponse.json( + { error: "service_id query parameter is required" }, + { status: 400 }, + ); } try { - const pricing = await servicePricingRepository.listByService(serviceId, false); + const pricing = await servicePricingRepository.listByService( + serviceId, + false, + ); return NextResponse.json({ service_id: serviceId, @@ -57,7 +66,10 @@ export async function GET(request: NextRequest) { }); } catch (error) { logger.error("[Admin] Service pricing GET error", { error, serviceId }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } @@ -68,7 +80,10 @@ const UpsertSchema = z.object({ reason: z.string(), description: z.string().optional(), metadata: z - .record(z.string().max(100), z.union([z.string().max(1000), z.number(), z.boolean(), z.null()])) + .record( + z.string().max(100), + z.union([z.string().max(1000), z.number(), z.boolean(), z.null()]), + ) .refine((val) => Object.keys(val).length <= 20, { message: "Metadata cannot have more than 20 keys", }) @@ -76,7 +91,10 @@ const UpsertSchema = z.object({ }); export async function PUT(request: NextRequest) { - const authResult = await requireAdminWithResponse(request, "[Admin] Service pricing auth error"); + const authResult = await requireAdminWithResponse( + request, + "[Admin] Service pricing auth error", + ); if (authResult instanceof NextResponse) { return authResult; } @@ -86,7 +104,10 @@ export async function PUT(request: NextRequest) { try { body = await request.json(); } catch { - return NextResponse.json({ error: "Invalid JSON in request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); } const parsed = UpsertSchema.safeParse(body); @@ -97,7 +118,8 @@ export async function PUT(request: NextRequest) { ); } - const { service_id, method, cost, reason, description, metadata } = parsed.data; + const { service_id, method, cost, reason, description, metadata } = + parsed.data; try { let cacheInvalidated = false; @@ -123,7 +145,8 @@ export async function PUT(request: NextRequest) { logger.error("[Admin] CRITICAL: Post-update cache invalidation failed", { service_id, method, - retryError: retryError instanceof Error ? retryError.message : "Unknown", + retryError: + retryError instanceof Error ? retryError.message : "Unknown", }); } @@ -152,6 +175,9 @@ export async function PUT(request: NextRequest) { }); } catch (error) { logger.error("[Admin] Service pricing PUT error", { error }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/admin/users/[userId]/billing/breakdown/route.ts b/app/api/v1/admin/users/[userId]/billing/breakdown/route.ts index 0e2474b19..92a630b83 100644 --- a/app/api/v1/admin/users/[userId]/billing/breakdown/route.ts +++ b/app/api/v1/admin/users/[userId]/billing/breakdown/route.ts @@ -65,7 +65,12 @@ export async function GET( recordCount: sql`COUNT(*)::int`, }) .from(usageRecords) - .where(and(eq(usageRecords.user_id, userId), gte(usageRecords.created_at, periodStart))) + .where( + and( + eq(usageRecords.user_id, userId), + gte(usageRecords.created_at, periodStart), + ), + ) .groupBy(usageRecords.type, usageRecords.provider) .orderBy(usageRecords.type, usageRecords.provider); diff --git a/app/api/v1/advertising/accounts/route.ts b/app/api/v1/advertising/accounts/route.ts index f64554655..d0253f765 100644 --- a/app/api/v1/advertising/accounts/route.ts +++ b/app/api/v1/advertising/accounts/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { type AdPlatform, advertisingService } from "@/lib/services/advertising"; +import { + type AdPlatform, + advertisingService, +} from "@/lib/services/advertising"; import { ConnectAccountSchema } from "@/lib/services/advertising/schemas"; import { logger } from "@/lib/utils/logger"; diff --git a/app/api/v1/advertising/campaigns/[id]/analytics/route.ts b/app/api/v1/advertising/campaigns/[id]/analytics/route.ts index d30b5b70d..88d0c34fb 100644 --- a/app/api/v1/advertising/campaigns/[id]/analytics/route.ts +++ b/app/api/v1/advertising/campaigns/[id]/analytics/route.ts @@ -24,7 +24,8 @@ const DateRangeSchema = z .refine( (data) => { if (data.startDate && data.endDate) { - const range = new Date(data.endDate).getTime() - new Date(data.startDate).getTime(); + const range = + new Date(data.endDate).getTime() - new Date(data.startDate).getTime(); return range <= MAX_DATE_RANGE_MS; } return true; @@ -67,7 +68,11 @@ export async function GET(request: NextRequest, { params }: RouteParams) { } : undefined; - const metrics = await advertisingService.getCampaignMetrics(id, user.organization_id!, dateRange); + const metrics = await advertisingService.getCampaignMetrics( + id, + user.organization_id!, + dateRange, + ); return NextResponse.json({ campaignId: id, diff --git a/app/api/v1/advertising/campaigns/[id]/creatives/route.ts b/app/api/v1/advertising/campaigns/[id]/creatives/route.ts index 7bed56374..7fd6224a3 100644 --- a/app/api/v1/advertising/campaigns/[id]/creatives/route.ts +++ b/app/api/v1/advertising/campaigns/[id]/creatives/route.ts @@ -18,7 +18,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; - const creatives = await advertisingService.listCreatives(id, user.organization_id!); + const creatives = await advertisingService.listCreatives( + id, + user.organization_id!, + ); return NextResponse.json({ creatives: creatives.map((c) => ({ @@ -56,17 +59,20 @@ export async function POST(request: NextRequest, { params }: RouteParams) { ); } - const creative = await advertisingService.createCreative(user.organization_id!, { - campaignId: id, - name: parsed.data.name, - type: parsed.data.type, - headline: parsed.data.headline, - primaryText: parsed.data.primaryText, - description: parsed.data.description, - callToAction: parsed.data.callToAction, - destinationUrl: parsed.data.destinationUrl, - media: parsed.data.media, - }); + const creative = await advertisingService.createCreative( + user.organization_id!, + { + campaignId: id, + name: parsed.data.name, + type: parsed.data.type, + headline: parsed.data.headline, + primaryText: parsed.data.primaryText, + description: parsed.data.description, + callToAction: parsed.data.callToAction, + destinationUrl: parsed.data.destinationUrl, + media: parsed.data.media, + }, + ); logger.info("[Advertising API] Creative created", { creativeId: creative.id, diff --git a/app/api/v1/advertising/campaigns/[id]/pause/route.ts b/app/api/v1/advertising/campaigns/[id]/pause/route.ts index 15535a342..ad59d572c 100644 --- a/app/api/v1/advertising/campaigns/[id]/pause/route.ts +++ b/app/api/v1/advertising/campaigns/[id]/pause/route.ts @@ -17,7 +17,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; - const campaign = await advertisingService.pauseCampaign(id, user.organization_id!); + const campaign = await advertisingService.pauseCampaign( + id, + user.organization_id!, + ); logger.info("[Advertising API] Campaign paused", { campaignId: id }); diff --git a/app/api/v1/advertising/campaigns/[id]/route.ts b/app/api/v1/advertising/campaigns/[id]/route.ts index a29e5aa4e..30b6aef71 100644 --- a/app/api/v1/advertising/campaigns/[id]/route.ts +++ b/app/api/v1/advertising/campaigns/[id]/route.ts @@ -69,13 +69,19 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { ); } - const campaign = await advertisingService.updateCampaign(id, user.organization_id!, { - name: parsed.data.name, - budgetAmount: parsed.data.budgetAmount, - startDate: parsed.data.startDate ? new Date(parsed.data.startDate) : undefined, - endDate: parsed.data.endDate ? new Date(parsed.data.endDate) : undefined, - targeting: parsed.data.targeting, - }); + const campaign = await advertisingService.updateCampaign( + id, + user.organization_id!, + { + name: parsed.data.name, + budgetAmount: parsed.data.budgetAmount, + startDate: parsed.data.startDate + ? new Date(parsed.data.startDate) + : undefined, + endDate: parsed.data.endDate ? new Date(parsed.data.endDate) : undefined, + targeting: parsed.data.targeting, + }, + ); logger.info("[Advertising API] Campaign updated", { campaignId: id }); diff --git a/app/api/v1/advertising/campaigns/[id]/start/route.ts b/app/api/v1/advertising/campaigns/[id]/start/route.ts index 323d55355..f9348f75d 100644 --- a/app/api/v1/advertising/campaigns/[id]/start/route.ts +++ b/app/api/v1/advertising/campaigns/[id]/start/route.ts @@ -17,7 +17,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; - const campaign = await advertisingService.startCampaign(id, user.organization_id!); + const campaign = await advertisingService.startCampaign( + id, + user.organization_id!, + ); logger.info("[Advertising API] Campaign started", { campaignId: id }); diff --git a/app/api/v1/advertising/campaigns/route.ts b/app/api/v1/advertising/campaigns/route.ts index c2f8a57f8..d9d6ea954 100644 --- a/app/api/v1/advertising/campaigns/route.ts +++ b/app/api/v1/advertising/campaigns/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { type AdPlatform, advertisingService } from "@/lib/services/advertising"; +import { + type AdPlatform, + advertisingService, +} from "@/lib/services/advertising"; import { CreateCampaignSchema } from "@/lib/services/advertising/schemas"; import { logger } from "@/lib/utils/logger"; @@ -19,12 +22,15 @@ export async function GET(request: NextRequest) { const status = searchParams.get("status"); const appId = searchParams.get("appId"); - const campaigns = await advertisingService.listCampaigns(user.organization_id!, { - adAccountId: adAccountId || undefined, - platform: platform || undefined, - status: status || undefined, - appId: appId || undefined, - }); + const campaigns = await advertisingService.listCampaigns( + user.organization_id!, + { + adAccountId: adAccountId || undefined, + platform: platform || undefined, + status: status || undefined, + appId: appId || undefined, + }, + ); return NextResponse.json({ campaigns: campaigns.map((c) => ({ @@ -75,7 +81,9 @@ export async function POST(request: NextRequest) { budgetType: parsed.data.budgetType, budgetAmount: parsed.data.budgetAmount, budgetCurrency: parsed.data.budgetCurrency, - startDate: parsed.data.startDate ? new Date(parsed.data.startDate) : undefined, + startDate: parsed.data.startDate + ? new Date(parsed.data.startDate) + : undefined, endDate: parsed.data.endDate ? new Date(parsed.data.endDate) : undefined, targeting: parsed.data.targeting, appId: parsed.data.appId, diff --git a/app/api/v1/affiliates/link/route.ts b/app/api/v1/affiliates/link/route.ts index 64a18bdf0..12bbae624 100644 --- a/app/api/v1/affiliates/link/route.ts +++ b/app/api/v1/affiliates/link/route.ts @@ -1,9 +1,15 @@ import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { ERRORS as AFFILIATE_ERRORS, affiliatesService } from "@/lib/services/affiliates"; +import { + ERRORS as AFFILIATE_ERRORS, + affiliatesService, +} from "@/lib/services/affiliates"; import { getCorsHeaders } from "@/lib/utils/cors"; import { logger } from "@/lib/utils/logger"; @@ -41,7 +47,10 @@ export const POST = withRateLimit(async function POST(request: NextRequest) { ); } - const link = await affiliatesService.linkUserToAffiliateCode(user.id, validation.data.code); + const link = await affiliatesService.linkUserToAffiliateCode( + user.id, + validation.data.code, + ); return NextResponse.json({ success: true, link }, { headers: corsHeaders }); } catch (error: unknown) { @@ -50,15 +59,24 @@ export const POST = withRateLimit(async function POST(request: NextRequest) { error.message === AFFILIATE_ERRORS.INVALID_CODE || error.message === AFFILIATE_ERRORS.CODE_NOT_FOUND ) { - return NextResponse.json({ error: error.message }, { status: 404, headers: corsHeaders }); + return NextResponse.json( + { error: error.message }, + { status: 404, headers: corsHeaders }, + ); } if (error.message === AFFILIATE_ERRORS.SELF_REFERRAL) { - return NextResponse.json({ error: error.message }, { status: 400, headers: corsHeaders }); + return NextResponse.json( + { error: error.message }, + { status: 400, headers: corsHeaders }, + ); } if (error.message === AFFILIATE_ERRORS.ALREADY_LINKED) { - return NextResponse.json({ error: error.message }, { status: 409, headers: corsHeaders }); + return NextResponse.json( + { error: error.message }, + { status: 409, headers: corsHeaders }, + ); } } diff --git a/app/api/v1/affiliates/route.ts b/app/api/v1/affiliates/route.ts index b5f8464c1..a59713138 100644 --- a/app/api/v1/affiliates/route.ts +++ b/app/api/v1/affiliates/route.ts @@ -1,6 +1,9 @@ import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { affiliatesService } from "@/lib/services/affiliates"; @@ -70,7 +73,10 @@ async function handlePUT(request: NextRequest) { return NextResponse.json({ code }, { headers: corsHeaders }); } catch (error) { - if (error instanceof Error && error.message.includes("Affiliate code not found")) { + if ( + error instanceof Error && + error.message.includes("Affiliate code not found") + ) { return NextResponse.json( { error: "No affiliate code. Create one with POST first." }, { status: 404, headers: corsHeaders }, @@ -106,7 +112,10 @@ async function handlePOST(request: NextRequest) { } const { markupPercent } = validation.data; - const code = await affiliatesService.getOrCreateAffiliateCode(user.id, markupPercent); + const code = await affiliatesService.getOrCreateAffiliateCode( + user.id, + markupPercent, + ); return NextResponse.json({ code }, { headers: corsHeaders }); } catch (error) { diff --git a/app/api/v1/agents/[agentId]/monetization/route.ts b/app/api/v1/agents/[agentId]/monetization/route.ts index ac3298437..59ae21bc0 100644 --- a/app/api/v1/agents/[agentId]/monetization/route.ts +++ b/app/api/v1/agents/[agentId]/monetization/route.ts @@ -34,11 +34,17 @@ export async function GET( const agent = await charactersService.getById(agentId); if (!agent) { - return NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ); } // Check ownership - if (agent.user_id !== user.id && agent.organization_id !== user.organization_id) { + if ( + agent.user_id !== user.id && + agent.organization_id !== user.organization_id + ) { return NextResponse.json( { success: false, error: "Not authorized to view this agent" }, { status: 403 }, @@ -89,10 +95,17 @@ export async function PUT( ); } - const result = await agentMonetizationService.updateSettings(agentId, user.id, validation.data); + const result = await agentMonetizationService.updateSettings( + agentId, + user.id, + validation.data, + ); if (!result.success) { - return NextResponse.json({ success: false, error: result.error }, { status: 400 }); + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 }, + ); } logger.info("[Agent Monetization API] Settings updated", { diff --git a/app/api/v1/agents/[agentId]/n8n/[...path]/route.ts b/app/api/v1/agents/[agentId]/n8n/[...path]/route.ts index f2159cfa9..7b0cd96f7 100644 --- a/app/api/v1/agents/[agentId]/n8n/[...path]/route.ts +++ b/app/api/v1/agents/[agentId]/n8n/[...path]/route.ts @@ -25,7 +25,13 @@ interface RouteContext { const PLUGIN_PREFIX = "/n8n-workflow"; -type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; +type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; type JsonRecord = Record; async function handleRequest( @@ -79,19 +85,31 @@ async function handleRequest( return NextResponse.json(data.body, { status: data.status }); } -export async function GET(request: NextRequest, context: RouteContext): Promise { +export async function GET( + request: NextRequest, + context: RouteContext, +): Promise { return handleRequest(request, context, "GET"); } -export async function POST(request: NextRequest, context: RouteContext): Promise { +export async function POST( + request: NextRequest, + context: RouteContext, +): Promise { return handleRequest(request, context, "POST"); } -export async function PUT(request: NextRequest, context: RouteContext): Promise { +export async function PUT( + request: NextRequest, + context: RouteContext, +): Promise { return handleRequest(request, context, "PUT"); } -export async function DELETE(request: NextRequest, context: RouteContext): Promise { +export async function DELETE( + request: NextRequest, + context: RouteContext, +): Promise { return handleRequest(request, context, "DELETE"); } @@ -101,7 +119,11 @@ export async function DELETE(request: NextRequest, context: RouteContext): Promi * Match a request path against registered routes. * Tries literal paths first, then parameterized paths. */ -function matchRoute(routes: Route[], method: string, requestPath: string): Route | undefined { +function matchRoute( + routes: Route[], + method: string, + requestPath: string, +): Route | undefined { const candidates = routes.filter((r) => r.type === method && r.handler); // Exact literal match first @@ -122,20 +144,28 @@ function matchRoute(routes: Route[], method: string, requestPath: string): Route * Check if a parameterized route path matches a request path. * e.g. "/n8n-workflow/workflows/:id/activate" matches "/n8n-workflow/workflows/abc123/activate" */ -function matchParameterizedPath(routePath: string, requestPath: string): boolean { +function matchParameterizedPath( + routePath: string, + requestPath: string, +): boolean { const routeSegments = routePath.split("/"); const requestSegments = requestPath.split("/"); if (routeSegments.length !== requestSegments.length) return false; - return routeSegments.every((seg, i) => seg.startsWith(":") || seg === requestSegments[i]); + return routeSegments.every( + (seg, i) => seg.startsWith(":") || seg === requestSegments[i], + ); } /** * Extract named params from a matched parameterized path. * e.g. "/n8n-workflow/workflows/:id" + "/n8n-workflow/workflows/abc123" → { id: "abc123" } */ -function extractParams(routePath: string, requestPath: string): Record { +function extractParams( + routePath: string, + requestPath: string, +): Record { const routeSegments = routePath.split("/"); const requestSegments = requestPath.split("/"); const params: Record = {}; @@ -160,7 +190,11 @@ async function buildRouteRequest( if (request.method !== "GET" && request.method !== "DELETE") { try { const parsedBody = await request.json(); - if (parsedBody && typeof parsedBody === "object" && !Array.isArray(parsedBody)) { + if ( + parsedBody && + typeof parsedBody === "object" && + !Array.isArray(parsedBody) + ) { body = JSON.parse(JSON.stringify(parsedBody)) as JsonRecord; } } catch { diff --git a/app/api/v1/agents/[agentId]/publish/route.ts b/app/api/v1/agents/[agentId]/publish/route.ts index bd8e285c4..03a8db7fe 100644 --- a/app/api/v1/agents/[agentId]/publish/route.ts +++ b/app/api/v1/agents/[agentId]/publish/route.ts @@ -52,7 +52,10 @@ export async function POST( // Get agent const agent = await charactersService.getById(agentId); if (!agent) { - return NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ); } // Check ownership @@ -90,7 +93,8 @@ export async function POST( // Check if already published if (agent.is_public) { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; return NextResponse.json({ success: true, message: "Agent is already published", @@ -122,7 +126,8 @@ export async function POST( await charactersService.invalidateCache(agentId); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; logger.info("[Agent Publish API] Agent published", { agentId, @@ -159,11 +164,17 @@ export async function DELETE( const agent = await charactersService.getById(agentId); if (!agent) { - return NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ); } if (agent.user_id !== user.id) { - return NextResponse.json({ success: false, error: "Not authorized" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Not authorized" }, + { status: 403 }, + ); } // Make agent private diff --git a/app/api/v1/agents/[agentId]/restart/route.ts b/app/api/v1/agents/[agentId]/restart/route.ts index 5ad4694df..464c4b018 100644 --- a/app/api/v1/agents/[agentId]/restart/route.ts +++ b/app/api/v1/agents/[agentId]/restart/route.ts @@ -18,7 +18,10 @@ async function handlePOST( context?: { params: Promise<{ agentId: string }> }, ) { if (!context) { - return NextResponse.json({ error: "Missing route parameters" }, { status: 400 }); + return NextResponse.json( + { error: "Missing route parameters" }, + { status: 400 }, + ); } let identity; try { @@ -27,7 +30,10 @@ async function handlePOST( if (e instanceof ServiceKeyAuthError) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return NextResponse.json({ error: "Service authentication misconfigured" }, { status: 500 }); + return NextResponse.json( + { error: "Service authentication misconfigured" }, + { status: 500 }, + ); } const { agentId } = await context.params; @@ -35,22 +41,37 @@ async function handlePOST( logger.info("[service-api] Restarting agent", { agentId }); // Shutdown (snapshot + stop) - const shutdownResult = await miladySandboxService.shutdown(agentId, identity.organizationId); + const shutdownResult = await miladySandboxService.shutdown( + agentId, + identity.organizationId, + ); if (!shutdownResult.success) { if (shutdownResult.error === "Agent not found") { - return NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ); } // Non-fatal — agent may already be stopped; continue to provision - logger.warn("[service-api] Shutdown during restart returned error, continuing", { - agentId, - error: shutdownResult.error, - }); + logger.warn( + "[service-api] Shutdown during restart returned error, continuing", + { + agentId, + error: shutdownResult.error, + }, + ); } // Re-provision - const result = await miladySandboxService.provision(agentId, identity.organizationId); + const result = await miladySandboxService.provision( + agentId, + identity.organizationId, + ); if (!result.success) { - return NextResponse.json({ success: false, error: result.error }, { status: 500 }); + return NextResponse.json( + { success: false, error: result.error }, + { status: 500 }, + ); } return NextResponse.json({ success: true }); diff --git a/app/api/v1/agents/[agentId]/resume/route.ts b/app/api/v1/agents/[agentId]/resume/route.ts index 7c20317b1..072780419 100644 --- a/app/api/v1/agents/[agentId]/resume/route.ts +++ b/app/api/v1/agents/[agentId]/resume/route.ts @@ -23,14 +23,20 @@ export async function POST( if (e instanceof ServiceKeyAuthError) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return NextResponse.json({ error: "Service authentication misconfigured" }, { status: 500 }); + return NextResponse.json( + { error: "Service authentication misconfigured" }, + { status: 500 }, + ); } const { agentId } = await params; logger.info("[service-api] Resuming agent", { agentId }); - const result = await miladySandboxService.provision(agentId, identity.organizationId); + const result = await miladySandboxService.provision( + agentId, + identity.organizationId, + ); if (!result.success) { const status = result.error === "Agent not found" @@ -39,7 +45,11 @@ export async function POST( ? 409 : 500; return NextResponse.json( - { success: false, status: result.sandboxRecord?.status ?? "error", error: result.error }, + { + success: false, + status: result.sandboxRecord?.status ?? "error", + error: result.error, + }, { status }, ); } diff --git a/app/api/v1/agents/[agentId]/route.ts b/app/api/v1/agents/[agentId]/route.ts index 222e4a952..f5c1902c3 100644 --- a/app/api/v1/agents/[agentId]/route.ts +++ b/app/api/v1/agents/[agentId]/route.ts @@ -24,7 +24,10 @@ export async function GET( ); if (!agent) { - return NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ); } return NextResponse.json({ success: true, data: agent }); diff --git a/app/api/v1/agents/[agentId]/status/route.ts b/app/api/v1/agents/[agentId]/status/route.ts index 1a7d88ea4..e8d2f7bfd 100644 --- a/app/api/v1/agents/[agentId]/status/route.ts +++ b/app/api/v1/agents/[agentId]/status/route.ts @@ -22,11 +22,17 @@ export async function GET( if (e instanceof ServiceKeyAuthError) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return NextResponse.json({ error: "Service authentication misconfigured" }, { status: 500 }); + return NextResponse.json( + { error: "Service authentication misconfigured" }, + { status: 500 }, + ); } const { agentId } = await params; - const agent = await miladySandboxService.getAgent(agentId, identity.organizationId); + const agent = await miladySandboxService.getAgent( + agentId, + identity.organizationId, + ); if (!agent) { return NextResponse.json({ error: "Agent not found" }, { status: 404 }); diff --git a/app/api/v1/agents/[agentId]/suspend/route.ts b/app/api/v1/agents/[agentId]/suspend/route.ts index 2d32bd222..1dd10c529 100644 --- a/app/api/v1/agents/[agentId]/suspend/route.ts +++ b/app/api/v1/agents/[agentId]/suspend/route.ts @@ -27,17 +27,25 @@ export async function POST( if (e instanceof ServiceKeyAuthError) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return NextResponse.json({ error: "Service authentication misconfigured" }, { status: 500 }); + return NextResponse.json( + { error: "Service authentication misconfigured" }, + { status: 500 }, + ); } const { agentId } = await params; const body = await request.json().catch(() => ({})); const parsed = suspendSchema.safeParse(body); - const reason = parsed.success ? parsed.data.reason : "owner requested suspension"; + const reason = parsed.success + ? parsed.data.reason + : "owner requested suspension"; logger.info("[service-api] Suspending agent", { agentId, reason }); - const result = await miladySandboxService.shutdown(agentId, identity.organizationId); + const result = await miladySandboxService.shutdown( + agentId, + identity.organizationId, + ); if (!result.success) { return NextResponse.json( { success: false, error: result.error }, diff --git a/app/api/v1/agents/[agentId]/usage/route.ts b/app/api/v1/agents/[agentId]/usage/route.ts index 95e870d46..dc9ddbd3b 100644 --- a/app/api/v1/agents/[agentId]/usage/route.ts +++ b/app/api/v1/agents/[agentId]/usage/route.ts @@ -22,11 +22,17 @@ export async function GET( if (e instanceof ServiceKeyAuthError) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return NextResponse.json({ error: "Service authentication misconfigured" }, { status: 500 }); + return NextResponse.json( + { error: "Service authentication misconfigured" }, + { status: 500 }, + ); } const { agentId } = await params; - const agent = await miladySandboxService.getAgent(agentId, identity.organizationId); + const agent = await miladySandboxService.getAgent( + agentId, + identity.organizationId, + ); if (!agent) { return NextResponse.json({ error: "Agent not found" }, { status: 404 }); diff --git a/app/api/v1/agents/by-token/route.ts b/app/api/v1/agents/by-token/route.ts index 86652aac0..f9a9a678f 100644 --- a/app/api/v1/agents/by-token/route.ts +++ b/app/api/v1/agents/by-token/route.ts @@ -34,7 +34,10 @@ export async function GET(request: NextRequest) { // 256 chars is generous headroom. if (address.length > 256) { return NextResponse.json( - { success: false, error: "address parameter exceeds maximum length (256)" }, + { + success: false, + error: "address parameter exceeds maximum length (256)", + }, { status: 400 }, ); } diff --git a/app/api/v1/agents/route.ts b/app/api/v1/agents/route.ts index a0fde1181..e428506f0 100644 --- a/app/api/v1/agents/route.ts +++ b/app/api/v1/agents/route.ts @@ -64,7 +64,10 @@ export async function POST(request: NextRequest) { logger.error("[service-api] Service key config error", { error: e instanceof Error ? e.message : String(e), }); - return NextResponse.json({ error: "Service authentication misconfigured" }, { status: 500 }); + return NextResponse.json( + { error: "Service authentication misconfigured" }, + { status: 500 }, + ); } const body = await request.json().catch(() => null); @@ -85,7 +88,10 @@ export async function POST(request: NextRequest) { const agentName = p.character?.name || p.tokenName; // Normalise the token address so EVM checksum variants are treated as equal. - const normalizedTokenAddress = normalizeTokenAddress(p.tokenContractAddress, p.chain); + const normalizedTokenAddress = normalizeTokenAddress( + p.tokenContractAddress, + p.chain, + ); logger.info("[service-api] Provisioning agent", { token: normalizedTokenAddress, @@ -178,13 +184,17 @@ export async function POST(request: NextRequest) { // permanently "claimed" by a failed provisioning attempt. try { await charactersService.delete(character.id); - logger.info("[service-api] Cleaned up orphaned character after createAgent failure", { - characterId: character.id, - }); + logger.info( + "[service-api] Cleaned up orphaned character after createAgent failure", + { + characterId: character.id, + }, + ); } catch (cleanupErr) { logger.error("[service-api] Failed to clean up orphaned character", { characterId: character.id, - error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr), + error: + cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr), }); } throw createErr; @@ -192,7 +202,10 @@ export async function POST(request: NextRequest) { // ── Sync fallback (legacy) ──────────────────────────────────────── if (sync) { - const result = await miladySandboxService.provision(agent.id, identity.organizationId); + const result = await miladySandboxService.provision( + agent.id, + identity.organizationId, + ); if (!result.success) { logger.error("[service-api] Provision failed", { @@ -245,16 +258,25 @@ export async function POST(request: NextRequest) { // so leaving it is less harmful and avoids cascading deletes.) try { await charactersService.delete(character.id); - logger.info("[service-api] Cleaned up orphaned character after enqueue failure", { - characterId: character.id, - agentId: agent.id, - }); + logger.info( + "[service-api] Cleaned up orphaned character after enqueue failure", + { + characterId: character.id, + agentId: agent.id, + }, + ); } catch (cleanupErr) { - logger.error("[service-api] Failed to clean up orphaned character after enqueue failure", { - characterId: character.id, - agentId: agent.id, - error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr), - }); + logger.error( + "[service-api] Failed to clean up orphaned character after enqueue failure", + { + characterId: character.id, + agentId: agent.id, + error: + cleanupErr instanceof Error + ? cleanupErr.message + : String(cleanupErr), + }, + ); } throw enqueueErr; } diff --git a/app/api/v1/api-keys/[id]/regenerate/route.ts b/app/api/v1/api-keys/[id]/regenerate/route.ts index ee042b35e..61264e627 100644 --- a/app/api/v1/api-keys/[id]/regenerate/route.ts +++ b/app/api/v1/api-keys/[id]/regenerate/route.ts @@ -14,9 +14,15 @@ import { logger } from "@/lib/utils/logger"; * @param params - Route parameters containing the API key ID. * @returns New API key details including the plain key (only shown once). */ -async function handlePOST(request: NextRequest, context: { params: Promise<{ id: string }> }) { +async function handlePOST( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { if (!context) { - return NextResponse.json({ error: "Missing route parameters" }, { status: 400 }); + return NextResponse.json( + { error: "Missing route parameters" }, + { status: 400 }, + ); } try { const { user } = await requireAuthOrApiKeyWithOrg(request); @@ -32,7 +38,11 @@ async function handlePOST(request: NextRequest, context: { params: Promise<{ id: return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } - const { key: newKey, hash: newHash, prefix: newPrefix } = apiKeysService.generateApiKey(); + const { + key: newKey, + hash: newHash, + prefix: newPrefix, + } = apiKeysService.generateApiKey(); const updatedKey = await apiKeysService.update(id, { key: newKey, @@ -42,7 +52,10 @@ async function handlePOST(request: NextRequest, context: { params: Promise<{ id: }); if (!updatedKey) { - return NextResponse.json({ error: "Failed to regenerate API key" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to regenerate API key" }, + { status: 500 }, + ); } return NextResponse.json( @@ -66,7 +79,10 @@ async function handlePOST(request: NextRequest, context: { params: Promise<{ id: const status = getErrorStatusCode(error); return NextResponse.json( { - error: status === 500 ? "Failed to regenerate API key" : getSafeErrorMessage(error), + error: + status === 500 + ? "Failed to regenerate API key" + : getSafeErrorMessage(error), }, { status }, ); diff --git a/app/api/v1/api-keys/[id]/route.ts b/app/api/v1/api-keys/[id]/route.ts index df11c68cd..1206188bf 100644 --- a/app/api/v1/api-keys/[id]/route.ts +++ b/app/api/v1/api-keys/[id]/route.ts @@ -41,7 +41,10 @@ export async function DELETE( const status = getErrorStatusCode(error); return NextResponse.json( { - error: status === 500 ? "Failed to delete API key" : getSafeErrorMessage(error), + error: + status === 500 + ? "Failed to delete API key" + : getSafeErrorMessage(error), }, { status }, ); @@ -57,7 +60,10 @@ export async function DELETE( * @param params - Route parameters containing the API key ID. * @returns Updated API key details. */ -export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -73,8 +79,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< } const body = await request.json(); - const { name, description, permissions, rate_limit, is_active, expires_at } = - updateApiKeySchema.parse(body); + const { + name, + description, + permissions, + rate_limit, + is_active, + expires_at, + } = updateApiKeySchema.parse(body); const updatedKey = await apiKeysService.update(id, { ...(name !== undefined && { name }), @@ -86,7 +98,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< }); if (!updatedKey) { - return NextResponse.json({ error: "Failed to update API key" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to update API key" }, + { status: 500 }, + ); } return NextResponse.json( @@ -117,7 +132,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const status = getErrorStatusCode(error); return NextResponse.json( { - error: status === 500 ? "Failed to update API key" : getSafeErrorMessage(error), + error: + status === 500 + ? "Failed to update API key" + : getSafeErrorMessage(error), }, { status }, ); diff --git a/app/api/v1/api-keys/explorer/route.ts b/app/api/v1/api-keys/explorer/route.ts index 970a8d996..4553f3af6 100644 --- a/app/api/v1/api-keys/explorer/route.ts +++ b/app/api/v1/api-keys/explorer/route.ts @@ -6,8 +6,13 @@ import { logger } from "@/lib/utils/logger"; const EXPLORER_KEY_NAME = "API Explorer Key"; -function isUsableExplorerKey(key: { key: string; is_active: boolean; expires_at: Date | null }) { - const isValidFormat = key.key.startsWith("eliza_") || key.key.startsWith("sk-"); +function isUsableExplorerKey(key: { + key: string; + is_active: boolean; + expires_at: Date | null; +}) { + const isValidFormat = + key.key.startsWith("eliza_") || key.key.startsWith("sk-"); const isExpired = key.expires_at ? key.expires_at < new Date() : false; return key.is_active && isValidFormat && !isExpired; } @@ -27,11 +32,17 @@ export async function GET() { const user = await requireAuthWithOrg(); // Check if user already has an explorer key - const existingKeys = await apiKeysService.listByOrganization(user.organization_id); + const existingKeys = await apiKeysService.listByOrganization( + user.organization_id, + ); const explorerKeys = existingKeys - .filter((key) => key.name === EXPLORER_KEY_NAME && key.user_id === user.id) - .sort((left, right) => right.created_at.getTime() - left.created_at.getTime()); + .filter( + (key) => key.name === EXPLORER_KEY_NAME && key.user_id === user.id, + ) + .sort( + (left, right) => right.created_at.getTime() - left.created_at.getTime(), + ); const explorerKey = explorerKeys.find(isUsableExplorerKey); @@ -102,6 +113,9 @@ export async function GET() { } logger.error("Error getting/creating explorer API key:", error); - return NextResponse.json({ error: "Failed to get API key for explorer" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get API key for explorer" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/api-keys/route.ts b/app/api/v1/api-keys/route.ts index 1de3e4ce5..e7b38bdd5 100644 --- a/app/api/v1/api-keys/route.ts +++ b/app/api/v1/api-keys/route.ts @@ -24,7 +24,12 @@ export async function GET(request: NextRequest) { logger.error("Error fetching API keys:", error); const status = getErrorStatusCode(error); return NextResponse.json( - { error: status === 500 ? "Failed to fetch API keys" : getSafeErrorMessage(error) }, + { + error: + status === 500 + ? "Failed to fetch API keys" + : getSafeErrorMessage(error), + }, { status }, ); } @@ -84,7 +89,12 @@ export async function POST(request: NextRequest) { const status = getErrorStatusCode(error); return NextResponse.json( - { error: status === 500 ? "Failed to create API key" : getSafeErrorMessage(error) }, + { + error: + status === 500 + ? "Failed to create API key" + : getSafeErrorMessage(error), + }, { status }, ); } diff --git a/app/api/v1/api-keys/schemas.ts b/app/api/v1/api-keys/schemas.ts index 25a1c3d92..1c3a13881 100644 --- a/app/api/v1/api-keys/schemas.ts +++ b/app/api/v1/api-keys/schemas.ts @@ -6,7 +6,8 @@ const optionalExpiresAtSchema = z .union([z.string().trim().min(1), z.null()]) .optional() .refine( - (value) => value === undefined || value === null || !Number.isNaN(Date.parse(value)), + (value) => + value === undefined || value === null || !Number.isNaN(Date.parse(value)), "expires_at must be a valid ISO date", ) .transform((value) => { @@ -47,6 +48,9 @@ export const updateApiKeySchema = z is_active: z.boolean().optional(), expires_at: optionalExpiresAtSchema, }) - .refine((value) => Object.values(value).some((field) => field !== undefined), { - message: "At least one field is required", - }); + .refine( + (value) => Object.values(value).some((field) => field !== undefined), + { + message: "At least one field is required", + }, + ); diff --git a/app/api/v1/app-auth/connect/route.ts b/app/api/v1/app-auth/connect/route.ts index 263e93e8e..61ada34a9 100644 --- a/app/api/v1/app-auth/connect/route.ts +++ b/app/api/v1/app-auth/connect/route.ts @@ -13,7 +13,8 @@ export const dynamic = "force-dynamic"; const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id, X-Request-ID", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id, X-Request-ID", "Access-Control-Max-Age": "86400", }; @@ -109,7 +110,13 @@ export async function POST(request: NextRequest) { name: apps.name, }) .from(apps) - .where(and(eq(apps.id, appId), eq(apps.is_active, true), eq(apps.is_approved, true))) + .where( + and( + eq(apps.id, appId), + eq(apps.is_active, true), + eq(apps.is_approved, true), + ), + ) .limit(1); if (!app) { @@ -147,7 +154,8 @@ export async function POST(request: NextRequest) { app_id: appId, user_id: user.id, signup_source: "oauth", - ip_address: request.headers.get("x-forwarded-for")?.split(",")[0] || null, + ip_address: + request.headers.get("x-forwarded-for")?.split(",")[0] || null, user_agent: request.headers.get("user-agent") || null, }); diff --git a/app/api/v1/app-auth/session/route.ts b/app/api/v1/app-auth/session/route.ts index 8e889e04f..a6b8500ea 100644 --- a/app/api/v1/app-auth/session/route.ts +++ b/app/api/v1/app-auth/session/route.ts @@ -12,7 +12,8 @@ export const dynamic = "force-dynamic"; const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id, X-Request-ID", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id, X-Request-ID", "Access-Control-Max-Age": "86400", }; @@ -127,7 +128,10 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Session verification failed", + error: + error instanceof Error + ? error.message + : "Session verification failed", }, { status: 500, headers: CORS_HEADERS }, ); diff --git a/app/api/v1/app-builder/images/route.ts b/app/api/v1/app-builder/images/route.ts index 7d88fa3e0..1eb011666 100644 --- a/app/api/v1/app-builder/images/route.ts +++ b/app/api/v1/app-builder/images/route.ts @@ -93,7 +93,10 @@ export async function POST(request: NextRequest) { }); } catch (error) { if (error instanceof Error && error.message.includes("Invalid")) { - return NextResponse.json({ success: false, error: error.message }, { status: 400 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 400 }, + ); } if (getErrorStatusCode(error) >= 500) { logger.error("Error in image upload", { error }); diff --git a/app/api/v1/app-builder/route.ts b/app/api/v1/app-builder/route.ts index 5640a214d..6672b2740 100644 --- a/app/api/v1/app-builder/route.ts +++ b/app/api/v1/app-builder/route.ts @@ -102,7 +102,8 @@ export const GET = withRateLimit(async (request: NextRequest) => { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to list sessions", + error: + error instanceof Error ? error.message : "Failed to list sessions", }, { status: 500 }, ); diff --git a/app/api/v1/app-builder/sessions/[sessionId]/commit/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/commit/route.ts index 5bb90c97d..c54c739af 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/commit/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/commit/route.ts @@ -53,7 +53,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const { message, files } = validationResult.data; // Verify session ownership and get session data - const session = await aiAppBuilder.verifySessionOwnership(sessionId, user.id); + const session = await aiAppBuilder.verifySessionOwnership( + sessionId, + user.id, + ); if (!session.sandbox_id) { return NextResponse.json( @@ -90,7 +93,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); // Generate commit message if not provided - const commitMessage = message || `Manual save at ${new Date().toISOString()}`; + const commitMessage = + message || `Manual save at ${new Date().toISOString()}`; // Perform the commit const commitResult = await gitSyncService.commitAndPush( @@ -167,7 +171,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { sessionId } = await params; // Verify session ownership and get session data - const session = await aiAppBuilder.verifySessionOwnership(sessionId, user.id); + const session = await aiAppBuilder.verifySessionOwnership( + sessionId, + user.id, + ); if (!session.sandbox_id) { return NextResponse.json( @@ -183,14 +190,17 @@ export async function GET(request: NextRequest, { params }: RouteParams) { return NextResponse.json( { success: false, - error: "Unable to get git status - sandbox may not be a git repository", + error: + "Unable to get git status - sandbox may not be a git repository", }, { status: 400 }, ); } // Get current commit SHA - const currentSha = await gitSyncService.getCurrentCommitSha(session.sandbox_id); + const currentSha = await gitSyncService.getCurrentCommitSha( + session.sandbox_id, + ); return NextResponse.json({ success: true, diff --git a/app/api/v1/app-builder/sessions/[sessionId]/files/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/files/route.ts index a5a2d9b33..4615a0930 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/files/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/files/route.ts @@ -19,13 +19,22 @@ export async function GET(request: NextRequest, { params }: RouteParams) { user = authResult.user; } catch (error) { if (error instanceof AuthenticationError) { - return NextResponse.json({ success: false, error: error.message }, { status: 401 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 401 }, + ); } if (error instanceof ForbiddenError) { - return NextResponse.json({ success: false, error: error.message }, { status: 403 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 403 }, + ); } logger.error("[App Builder] Files GET auth error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } const { sessionId } = await params; @@ -33,7 +42,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { // Verify session ownership const session = await aiAppBuilder.getSession(sessionId, user.id); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } const searchParams = request.nextUrl.searchParams; @@ -77,13 +89,22 @@ export async function GET(request: NextRequest, { params }: RouteParams) { }); } catch (error) { if (error instanceof Error && error.message.includes("Session not found")) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } if (error instanceof Error && error.message.includes("Access denied")) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } logger.error("[App Builder] Files GET error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } } @@ -102,13 +123,22 @@ export async function POST(request: NextRequest, { params }: RouteParams) { user = authResult.user; } catch (error) { if (error instanceof AuthenticationError) { - return NextResponse.json({ success: false, error: error.message }, { status: 401 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 401 }, + ); } if (error instanceof ForbiddenError) { - return NextResponse.json({ success: false, error: error.message }, { status: 403 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 403 }, + ); } logger.error("[App Builder] Files POST auth error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } const { sessionId } = await params; @@ -116,14 +146,20 @@ export async function POST(request: NextRequest, { params }: RouteParams) { // Verify session ownership const session = await aiAppBuilder.getSession(sessionId, user.id); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } let body; try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON" }, + { status: 400 }, + ); } const validation = FileOperationSchema.safeParse(body); @@ -142,7 +178,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const { operation, path, content } = validation.data; if (operation === "read") { - const fileContent = await sandboxService.readFile(session.sandboxId, path); + const fileContent = await sandboxService.readFile( + session.sandboxId, + path, + ); return NextResponse.json({ success: true, content: fileContent, @@ -163,16 +202,28 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); } - return NextResponse.json({ success: false, error: "Invalid operation" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid operation" }, + { status: 400 }, + ); } catch (error) { if (error instanceof Error && error.message.includes("Session not found")) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } if (error instanceof Error && error.message.includes("Access denied")) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } logger.error("[App Builder] Files POST error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/app-builder/sessions/[sessionId]/history/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/history/route.ts index 4bcd5d141..bbc808e4c 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/history/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/history/route.ts @@ -15,9 +15,15 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { user } = await requireAuthOrApiKeyWithOrg(request); const { sessionId } = await params; - const session = await aiAppBuilder.verifySessionOwnership(sessionId, user.id); + const session = await aiAppBuilder.verifySessionOwnership( + sessionId, + user.id, + ); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } // Need app with GitHub repo to get history diff --git a/app/api/v1/app-builder/sessions/[sessionId]/prompts/stream/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/prompts/stream/route.ts index fb76ca463..1eb729a01 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/prompts/stream/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/prompts/stream/route.ts @@ -57,7 +57,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { await aiAppBuilder.verifySessionOwnership(sessionId, user.id); - const rateLimitResult = await checkRateLimitAsync(request, PROMPT_RATE_LIMIT); + const rateLimitResult = await checkRateLimitAsync( + request, + PROMPT_RATE_LIMIT, + ); if (!rateLimitResult.allowed) { const maxRequests = PROMPT_RATE_LIMIT.maxRequests; const windowSeconds = PROMPT_RATE_LIMIT.windowMs / 1000; @@ -153,13 +156,19 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const scheduleThinkingFlush = () => { if (!thinkingFlushTimer) { - thinkingFlushTimer = setTimeout(flushThinkingBuffer, THINKING_FLUSH_INTERVAL_MS); + thinkingFlushTimer = setTimeout( + flushThinkingBuffer, + THINKING_FLUSH_INTERVAL_MS, + ); } }; const scheduleReasoningFlush = () => { if (!reasoningFlushTimer) { - reasoningFlushTimer = setTimeout(flushReasoningBuffer, REASONING_FLUSH_INTERVAL_MS); + reasoningFlushTimer = setTimeout( + flushReasoningBuffer, + REASONING_FLUSH_INTERVAL_MS, + ); } }; @@ -226,7 +235,9 @@ export async function POST(request: NextRequest, { params }: RouteParams) { tool: event.toolName, input: event.args, result: - typeof event.result === "string" ? event.result : JSON.stringify(event.result), + typeof event.result === "string" + ? event.result + : JSON.stringify(event.result), }); } break; @@ -295,9 +306,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) { await flushReasoningBuffer(); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Failed to send prompt"; + const errorMessage = + error instanceof Error ? error.message : "Failed to send prompt"; - if (errorMessage.includes("aborted") || errorMessage.includes("cancelled")) { + if ( + errorMessage.includes("aborted") || + errorMessage.includes("cancelled") + ) { logger.info("Prompt operation cancelled", { sessionId }); if (streamWriter.isConnected()) { await streamWriter.sendEvent("cancelled", { diff --git a/app/api/v1/app-builder/sessions/[sessionId]/resume/stream/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/resume/stream/route.ts index 790e38b22..4e73d750c 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/resume/stream/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/resume/stream/route.ts @@ -82,7 +82,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Failed to resume session"; + const errorMessage = + error instanceof Error ? error.message : "Failed to resume session"; logger.error("Failed to resume session via stream", { sessionId, @@ -104,10 +105,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return new Response(stream.readable, { headers: SSE_HEADERS }); } catch (error) { if (error instanceof Error && error.message.includes("cannot be resumed")) { - return new Response(JSON.stringify({ success: false, error: error.message }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ success: false, error: error.message }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } const { status, body } = caughtErrorJson(error); if (status >= 500) { diff --git a/app/api/v1/app-builder/sessions/[sessionId]/rollback/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/rollback/route.ts index 7f76f4c96..a38c55150 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/rollback/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/rollback/route.ts @@ -27,9 +27,15 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const { sessionId } = await params; // Verify session ownership - const session = await aiAppBuilder.verifySessionOwnership(sessionId, user.id); + const session = await aiAppBuilder.verifySessionOwnership( + sessionId, + user.id, + ); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } // Need app with GitHub repo to rollback @@ -80,7 +86,11 @@ export async function POST(request: NextRequest, { params }: RouteParams) { } // Perform the rollback using git reset --hard - const rollbackResult = await performRollback(session.sandbox_id, commitSha, app.github_repo); + const rollbackResult = await performRollback( + session.sandbox_id, + commitSha, + app.github_repo, + ); if (!rollbackResult.success) { logger.error("Rollback failed", { @@ -88,7 +98,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) { commitSha, error: rollbackResult.error, }); - return NextResponse.json({ success: false, error: rollbackResult.error }, { status: 500 }); + return NextResponse.json( + { success: false, error: rollbackResult.error }, + { status: 500 }, + ); } logger.info("Rollback successful", { @@ -128,7 +141,9 @@ async function performRollback( ): Promise { try { // First ensure git is configured properly - const repoName = repoFullName.includes("/") ? repoFullName.split("/").pop()! : repoFullName; + const repoName = repoFullName.includes("/") + ? repoFullName.split("/").pop()! + : repoFullName; const configResult = await gitSyncService.configureGit({ sandboxId, @@ -222,6 +237,8 @@ async function performRollback( * Gets the active sandbox instance from global state. */ function getSandboxInstance(sandboxId: string) { - const sandboxes = (global as any).__sandboxInstances as Map | undefined; + const sandboxes = (global as any).__sandboxInstances as + | Map + | undefined; return sandboxes?.get(sandboxId) || null; } diff --git a/app/api/v1/app-builder/sessions/[sessionId]/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/route.ts index 7aedf752d..bce29e550 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/route.ts @@ -15,7 +15,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const session = await aiAppBuilder.getSession(sessionId, user.id); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } return NextResponse.json({ success: true, session }); diff --git a/app/api/v1/app-builder/sessions/[sessionId]/snapshots/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/snapshots/route.ts index 4ad6d8957..0fcdf7632 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/snapshots/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/snapshots/route.ts @@ -15,9 +15,15 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { user } = await requireAuthOrApiKeyWithOrg(request); const { sessionId } = await params; - const session = await aiAppBuilder.verifySessionOwnership(sessionId, user.id); + const session = await aiAppBuilder.verifySessionOwnership( + sessionId, + user.id, + ); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } // Check if app has GitHub storage diff --git a/app/api/v1/app-builder/sessions/[sessionId]/terminal/route.ts b/app/api/v1/app-builder/sessions/[sessionId]/terminal/route.ts index 502b3d76b..e733bf0a6 100644 --- a/app/api/v1/app-builder/sessions/[sessionId]/terminal/route.ts +++ b/app/api/v1/app-builder/sessions/[sessionId]/terminal/route.ts @@ -79,13 +79,22 @@ export async function POST( user = authResult.user; } catch (error) { if (error instanceof AuthenticationError) { - return NextResponse.json({ success: false, error: error.message }, { status: 401 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 401 }, + ); } if (error instanceof ForbiddenError) { - return NextResponse.json({ success: false, error: error.message }, { status: 403 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 403 }, + ); } logger.error("[App Builder] Terminal POST auth error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } const { sessionId } = await params; @@ -97,17 +106,26 @@ export async function POST( try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON" }, + { status: 400 }, + ); } const { command, cwd } = body as { command: string; cwd?: string }; if (!command || typeof command !== "string") { - return NextResponse.json({ success: false, error: "Command is required" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Command is required" }, + { status: 400 }, + ); } if (cwd !== undefined && typeof cwd !== "string") { - return NextResponse.json({ success: false, error: "cwd must be a string" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "cwd must be a string" }, + { status: 400 }, + ); } // Validate command @@ -134,12 +152,18 @@ export async function POST( // Get sandbox instance const session = await aiAppBuilder.getSession(sessionId, user.id); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } const sandbox = sandboxService.getSandboxInstance(session.sandboxId); if (!sandbox) { - return NextResponse.json({ success: false, error: "Sandbox not available" }, { status: 503 }); + return NextResponse.json( + { success: false, error: "Sandbox not available" }, + { status: 503 }, + ); } logger.info("Terminal command execution", { @@ -185,13 +209,22 @@ export async function POST( }); } catch (error) { if (error instanceof Error && error.message.includes("Session not found")) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } if (error instanceof Error && error.message.includes("Access denied")) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } logger.error("[App Builder] Terminal POST error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } } @@ -209,13 +242,22 @@ export async function GET( user = authResult.user; } catch (error) { if (error instanceof AuthenticationError) { - return NextResponse.json({ success: false, error: error.message }, { status: 401 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 401 }, + ); } if (error instanceof ForbiddenError) { - return NextResponse.json({ success: false, error: error.message }, { status: 403 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 403 }, + ); } logger.error("[App Builder] Terminal GET auth error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } const { sessionId } = await params; @@ -227,7 +269,10 @@ export async function GET( const cwd = url.searchParams.get("cwd") || "/app"; if (!command) { - return NextResponse.json({ success: false, error: "Command is required" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Command is required" }, + { status: 400 }, + ); } // Validate command @@ -255,17 +300,26 @@ export async function GET( })(); if (!normalizedCwd) { - return NextResponse.json({ success: false, error: "Invalid cwd" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid cwd" }, + { status: 400 }, + ); } const session = await aiAppBuilder.getSession(sessionId, user.id); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } const sandbox = sandboxService.getSandboxInstance(session.sandboxId); if (!sandbox) { - return NextResponse.json({ success: false, error: "Sandbox not available" }, { status: 503 }); + return NextResponse.json( + { success: false, error: "Sandbox not available" }, + { status: 503 }, + ); } // Create streaming response @@ -300,12 +354,16 @@ export async function GET( if (stdout) { controller.enqueue( - encoder.encode(`data: ${JSON.stringify({ stream: "stdout", data: stdout })}\n\n`), + encoder.encode( + `data: ${JSON.stringify({ stream: "stdout", data: stdout })}\n\n`, + ), ); } if (stderr) { controller.enqueue( - encoder.encode(`data: ${JSON.stringify({ stream: "stderr", data: stderr })}\n\n`), + encoder.encode( + `data: ${JSON.stringify({ stream: "stderr", data: stderr })}\n\n`, + ), ); } } @@ -318,9 +376,12 @@ export async function GET( ); controller.close(); } catch (error) { - const message = error instanceof Error ? error.message : "Stream error"; + const message = + error instanceof Error ? error.message : "Stream error"; controller.enqueue( - encoder.encode(`data: ${JSON.stringify({ type: "error", error: message })}\n\n`), + encoder.encode( + `data: ${JSON.stringify({ type: "error", error: message })}\n\n`, + ), ); controller.close(); } @@ -336,12 +397,21 @@ export async function GET( }); } catch (error) { if (error instanceof Error && error.message.includes("Session not found")) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } if (error instanceof Error && error.message.includes("Access denied")) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } logger.error("[App Builder] Terminal GET error", { error }); - return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/app-builder/stream/route.ts b/app/api/v1/app-builder/stream/route.ts index 134a4b9d1..dbae764d0 100644 --- a/app/api/v1/app-builder/stream/route.ts +++ b/app/api/v1/app-builder/stream/route.ts @@ -36,7 +36,10 @@ const _CreateSessionSchema = z.object({ export async function POST(request: NextRequest) { return new Response( - JSON.stringify({ success: false, error: "App creation is temporarily disabled" }), + JSON.stringify({ + success: false, + error: "App creation is temporarily disabled", + }), { status: 403, headers: { "Content-Type": "application/json" } }, ); diff --git a/app/api/v1/app-credits/checkout/route.ts b/app/api/v1/app-credits/checkout/route.ts index ea87d2310..156d7c985 100644 --- a/app/api/v1/app-credits/checkout/route.ts +++ b/app/api/v1/app-credits/checkout/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { assertAllowedAbsoluteRedirectUrl, @@ -152,9 +155,11 @@ export async function POST(request: NextRequest) { { headers: corsHeaders }, ); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Failed to create checkout"; + const errorMessage = + error instanceof Error ? error.message : "Failed to create checkout"; const isValidationError = - errorMessage.includes("Invalid success_url") || errorMessage.includes("Invalid cancel_url"); + errorMessage.includes("Invalid success_url") || + errorMessage.includes("Invalid cancel_url"); if (isValidationError) { return NextResponse.json( diff --git a/app/api/v1/app-credits/verify/route.ts b/app/api/v1/app-credits/verify/route.ts index b8cded619..b52f25138 100644 --- a/app/api/v1/app-credits/verify/route.ts +++ b/app/api/v1/app-credits/verify/route.ts @@ -1,6 +1,9 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { appCreditsService } from "@/lib/services/app-credits"; import { requireStripe } from "@/lib/stripe"; @@ -118,7 +121,9 @@ export async function GET(request: NextRequest) { } const paymentIntentId = - typeof session.payment_intent === "string" ? session.payment_intent : null; + typeof session.payment_intent === "string" + ? session.payment_intent + : null; if (!paymentIntentId) { return NextResponse.json( diff --git a/app/api/v1/app/agents/route.ts b/app/api/v1/app/agents/route.ts index fe50ca1ec..31676323e 100644 --- a/app/api/v1/app/agents/route.ts +++ b/app/api/v1/app/agents/route.ts @@ -44,7 +44,10 @@ const AGENT_LIMITS = { * Gets the maximum number of agents allowed for an organization. * Similar to container quotas, based on credit balance. */ -function getMaxAgentsForOrg(creditBalance: number, orgSettings?: Record): number { +function getMaxAgentsForOrg( + creditBalance: number, + orgSettings?: Record, +): number { // Check if org has custom limit in settings const customLimit = orgSettings?.max_agents as number | undefined; if (customLimit && customLimit > 0) { @@ -109,7 +112,8 @@ async function handlePOST(request: NextRequest) { ); } - const { name, bio, tokenChain, tokenName, tokenTicker } = validationResult.data; + const { name, bio, tokenChain, tokenName, tokenTicker } = + validationResult.data; // Normalise so EVM checksum variants are treated as the same address. const tokenAddress = validationResult.data.tokenAddress ? normalizeTokenAddress(validationResult.data.tokenAddress, tokenChain) @@ -161,7 +165,8 @@ async function handlePOST(request: NextRequest) { details: { current: count, max: maxAgents, - upgrade_hint: "Add credits to your account to increase your agent limit.", + upgrade_hint: + "Add credits to your account to increase your agent limit.", }, }, { status: 403 }, // 403 Forbidden for quota exceeded @@ -170,7 +175,10 @@ async function handlePOST(request: NextRequest) { // If a token is specified, check for existing linkage (unique constraint) if (tokenAddress) { - const existing = await userCharactersRepository.findByTokenAddress(tokenAddress, tokenChain); + const existing = await userCharactersRepository.findByTokenAddress( + tokenAddress, + tokenChain, + ); if (existing) { return NextResponse.json( { @@ -261,7 +269,8 @@ async function handlePOST(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to create agent", + error: + error instanceof Error ? error.message : "Failed to create agent", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/analytics/requests/route.ts b/app/api/v1/apps/[id]/analytics/requests/route.ts index f45da42fd..23fcbdd9e 100644 --- a/app/api/v1/apps/[id]/analytics/requests/route.ts +++ b/app/api/v1/apps/[id]/analytics/requests/route.ts @@ -33,11 +33,17 @@ async function handleGET( const existingApp = await appsService.getById(id); if (!existingApp) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (existingApp.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } const view = searchParams.get("view") || "stats"; @@ -54,7 +60,10 @@ async function handleGET( const MAX_LIMIT = 100; const rawLimit = Number.parseInt(searchParams.get("limit") || "50", 10); const rawOffset = Number.parseInt(searchParams.get("offset") || "0", 10); - const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 50 : rawLimit, 1), MAX_LIMIT); + const limit = Math.min( + Math.max(Number.isNaN(rawLimit) ? 50 : rawLimit, 1), + MAX_LIMIT, + ); const offset = Math.max(Number.isNaN(rawOffset) ? 0 : rawOffset, 0); switch (view) { @@ -76,7 +85,12 @@ async function handleGET( } case "visitors": { - const visitors = await appsService.getTopVisitors(id, limit, startDate, endDate); + const visitors = await appsService.getTopVisitors( + id, + limit, + startDate, + endDate, + ); return NextResponse.json({ success: true, visitors, @@ -88,7 +102,8 @@ async function handleGET( | "hourly" | "daily" | "monthly"; - const timelineStart = startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const timelineStart = + startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const timelineEnd = endDate || new Date(); const timeline = await appsService.getRequestsOverTime( @@ -122,7 +137,10 @@ async function handleGET( return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get request analytics", + error: + error instanceof Error + ? error.message + : "Failed to get request analytics", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/analytics/route.ts b/app/api/v1/apps/[id]/analytics/route.ts index bc40d935b..0150fe91c 100644 --- a/app/api/v1/apps/[id]/analytics/route.ts +++ b/app/api/v1/apps/[id]/analytics/route.ts @@ -18,14 +18,20 @@ import { logger } from "@/lib/utils/logger"; * @param params - Route parameters containing the app ID. * @returns Analytics data and total statistics for the specified period. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; const { searchParams } = new URL(request.url); // Parse query parameters - const periodType = (searchParams.get("period") || "daily") as "hourly" | "daily" | "monthly"; + const periodType = (searchParams.get("period") || "daily") as + | "hourly" + | "daily" + | "monthly"; const startDate = searchParams.get("start_date") ? new Date(searchParams.get("start_date")!) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // Default: 30 days ago @@ -57,7 +63,12 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } // Get analytics - const analytics = await appsService.getAnalytics(id, periodType, startDate, endDate); + const analytics = await appsService.getAnalytics( + id, + periodType, + startDate, + endDate, + ); // Get total stats const totalStats = await appsService.getTotalStats(id); @@ -77,7 +88,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get app analytics", + error: + error instanceof Error + ? error.message + : "Failed to get app analytics", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/characters/route.ts b/app/api/v1/apps/[id]/characters/route.ts index a375e6bb2..b0a959787 100644 --- a/app/api/v1/apps/[id]/characters/route.ts +++ b/app/api/v1/apps/[id]/characters/route.ts @@ -13,7 +13,10 @@ import { logger } from "@/lib/utils/logger"; * @param params - Route parameters containing the app ID. * @returns List of linked characters. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { // Allow both authenticated users and API key access let organizationId: string; @@ -47,12 +50,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const app = await appsService.getById(id); if (!app) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } // Verify ownership (unless accessed via the app's own API key) if (app.organization_id !== organizationId) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } // Get the linked character IDs @@ -99,7 +108,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get characters", + error: + error instanceof Error ? error.message : "Failed to get characters", }, { status: 500 }, ); @@ -115,7 +125,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ * @param params - Route parameters containing the app ID. * @returns Updated list of linked characters. */ -export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -123,11 +136,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ const existingApp = await appsService.getById(id); if (!existingApp) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (existingApp.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } const body = await request.json(); @@ -188,7 +207,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to update characters", + error: + error instanceof Error + ? error.message + : "Failed to update characters", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/chat/route.ts b/app/api/v1/apps/[id]/chat/route.ts index 33d0defc3..fada9558c 100644 --- a/app/api/v1/apps/[id]/chat/route.ts +++ b/app/api/v1/apps/[id]/chat/route.ts @@ -6,7 +6,10 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { addCorsHeaders, createPreflightResponse } from "@/lib/middleware/cors-apps"; +import { + addCorsHeaders, + createPreflightResponse, +} from "@/lib/middleware/cors-apps"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { calculateCost, @@ -14,8 +17,14 @@ import { getProviderFromModel, normalizeModelName, } from "@/lib/pricing"; -import { getProviderForModelWithFallback, withProviderFallback } from "@/lib/providers"; -import type { OpenAIChatMessage, OpenAIChatRequest } from "@/lib/providers/types"; +import { + getProviderForModelWithFallback, + withProviderFallback, +} from "@/lib/providers"; +import type { + OpenAIChatMessage, + OpenAIChatRequest, +} from "@/lib/providers/types"; import { appCreditsService } from "@/lib/services/app-credits"; import { appsService } from "@/lib/services/apps"; import { logger } from "@/lib/utils/logger"; @@ -57,7 +66,10 @@ export async function OPTIONS(request: NextRequest) { * * @returns Streaming or non-streaming chat completion response. */ -async function handlePOST(request: NextRequest, context: RouteContext): Promise { +async function handlePOST( + request: NextRequest, + context: RouteContext, +): Promise { const startTime = Date.now(); const origin = request.headers.get("origin"); const routeTimeoutMs = getRouteTimeoutMs(maxDuration); @@ -118,7 +130,10 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< // Access control: non-monetized apps are internal (same org only) // Monetized apps are public (anyone with credits can use) - if (!app.monetization_enabled && app.organization_id !== user.organization_id) { + if ( + !app.monetization_enabled && + app.organization_id !== user.organization_id + ) { return withCors( NextResponse.json( { @@ -149,7 +164,10 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< ); } - if (!Array.isArray(chatRequest.messages) || chatRequest.messages.length === 0) { + if ( + !Array.isArray(chatRequest.messages) || + chatRequest.messages.length === 0 + ) { return withCors( NextResponse.json( { @@ -269,7 +287,10 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< appId, userId: user.id, reservedBaseCost, - error: providerError instanceof Error ? providerError.message : "Unknown error", + error: + providerError instanceof Error + ? providerError.message + : "Unknown error", }); await appCreditsService.reconcileCredits({ @@ -314,11 +335,14 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< const reader = providerResponse.body?.getReader(); if (!reader) { // No response body - refund credits and close stream - logger.error("[App Chat] No response body from provider, refunding credits", { - appId, - userId: user.id, - reservedBaseCost, - }); + logger.error( + "[App Chat] No response body from provider, refunding credits", + { + appId, + userId: user.id, + reservedBaseCost, + }, + ); await appCreditsService.reconcileCredits({ appId, @@ -422,7 +446,9 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< if (inputTokens === 0 && outputTokens === 0) { const inputText = chatRequest.messages .map((m: OpenAIChatMessage) => - typeof m.content === "string" ? m.content : JSON.stringify(m.content), + typeof m.content === "string" + ? m.content + : JSON.stringify(m.content), ) .join(" "); inputTokens = estimateTokens(inputText); @@ -478,13 +504,17 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< }); } catch (error) { // Stream failed - refund the reserved charge since we don't know actual usage - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - logger.error("[App Chat] Stream processing failed, refunding reserved", { - appId, - userId: user.id, - reservedBaseCost, - error: errorMessage, - }); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + logger.error( + "[App Chat] Stream processing failed, refunding reserved", + { + appId, + userId: user.id, + reservedBaseCost, + error: errorMessage, + }, + ); await appCreditsService.reconcileCredits({ appId, @@ -592,7 +622,8 @@ async function handlePOST(request: NextRequest, context: RouteContext): Promise< NextResponse.json( { error: { - message: error instanceof Error ? error.message : "Internal server error", + message: + error instanceof Error ? error.message : "Internal server error", type: "api_error", code: "internal_server_error", }, diff --git a/app/api/v1/apps/[id]/deploy/route.ts b/app/api/v1/apps/[id]/deploy/route.ts index 483571a43..76b1b2741 100644 --- a/app/api/v1/apps/[id]/deploy/route.ts +++ b/app/api/v1/apps/[id]/deploy/route.ts @@ -57,7 +57,10 @@ async function handleDeployPOST( // Verify app ownership const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } // Check if deployment is configured @@ -133,7 +136,10 @@ async function handleDeployPOST( }) .where(eq(apps.id, appId)); - return NextResponse.json({ success: false, error: result.error }, { status: 500 }); + return NextResponse.json( + { success: false, error: result.error }, + { status: 500 }, + ); } // If deployment was triggered successfully, update status to "deployed" @@ -168,14 +174,20 @@ export const POST = withRateLimit(handleDeployPOST, RateLimitPresets.STANDARD); * * @returns Deployment information including production URL and recent deployments */ -export async function GET(request: NextRequest, { params }: RouteParams): Promise { +export async function GET( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; // Verify app ownership const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } // Get production URL @@ -185,7 +197,8 @@ export async function GET(request: NextRequest, { params }: RouteParams): Promis const deployments = await vercelDeploymentsService.listDeployments(appId, 10); // Check if deployment is configured - const deploymentConfigured = vercelDeploymentsService.isDeploymentConfigured(); + const deploymentConfigured = + vercelDeploymentsService.isDeploymentConfigured(); return NextResponse.json({ success: true, diff --git a/app/api/v1/apps/[id]/discord-automation/post/route.ts b/app/api/v1/apps/[id]/discord-automation/post/route.ts index ea63811c9..65aefddc7 100644 --- a/app/api/v1/apps/[id]/discord-automation/post/route.ts +++ b/app/api/v1/apps/[id]/discord-automation/post/route.ts @@ -20,7 +20,10 @@ const postSchema = z.object({ text: z.string().max(2000).optional(), }); -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; @@ -35,7 +38,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } try { @@ -68,6 +74,9 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to post announcement" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to post announcement" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/apps/[id]/discord-automation/route.ts b/app/api/v1/apps/[id]/discord-automation/route.ts index de938fc05..c4128b6c3 100644 --- a/app/api/v1/apps/[id]/discord-automation/route.ts +++ b/app/api/v1/apps/[id]/discord-automation/route.ts @@ -29,7 +29,10 @@ const automationConfigSchema = z.object({ agentCharacterId: z.string().uuid().optional(), // Character voice for posts }); -export async function GET(request: NextRequest, { params }: RouteParams): Promise { +export async function GET( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; @@ -47,11 +50,17 @@ export async function GET(request: NextRequest, { params }: RouteParams): Promis appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to get automation status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get automation status" }, + { status: 500 }, + ); } } -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; @@ -66,7 +75,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } // Validate interval range - defaults are min=120, max=240 @@ -132,16 +144,25 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to enable automation" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to enable automation" }, + { status: 500 }, + ); } } -export async function DELETE(request: NextRequest, { params }: RouteParams): Promise { +export async function DELETE( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; try { - await discordAppAutomationService.disableAutomation(user.organization_id, appId); + await discordAppAutomationService.disableAutomation( + user.organization_id, + appId, + ); logger.info("[Discord Automation] Automation disabled", { appId, @@ -157,6 +178,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams): Pro appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to disable automation" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to disable automation" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/apps/[id]/domains/route.ts b/app/api/v1/apps/[id]/domains/route.ts index abe254677..c92e0b18b 100644 --- a/app/api/v1/apps/[id]/domains/route.ts +++ b/app/api/v1/apps/[id]/domains/route.ts @@ -80,13 +80,19 @@ interface RouteParams { * GET /api/v1/apps/:id/domains * List all domains for an app */ -export async function GET(request: NextRequest, { params }: RouteParams): Promise { +export async function GET( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } const domains = await vercelDomainsService.getDomainsForApp(appId); @@ -114,13 +120,19 @@ export async function GET(request: NextRequest, { params }: RouteParams): Promis * POST /api/v1/apps/:id/domains * Add a custom domain to an app */ -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } const body = await request.json(); @@ -163,7 +175,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi } const isApex = vercelDomainsService.isApexDomain(domain); - const dnsInstructions = vercelDomainsService.getDnsInstructions(domain, isApex); + const dnsInstructions = vercelDomainsService.getDnsInstructions( + domain, + isApex, + ); return NextResponse.json({ success: true, @@ -179,20 +194,29 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi * DELETE /api/v1/apps/:id/domains * Remove a custom domain from an app */ -export async function DELETE(request: NextRequest, { params }: RouteParams): Promise { +export async function DELETE( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } const body = await request.json(); const validation = RemoveDomainSchema.safeParse(body); if (!validation.success) { - return NextResponse.json({ success: false, error: "Invalid domain format" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid domain format" }, + { status: 400 }, + ); } const { domain } = validation.data; diff --git a/app/api/v1/apps/[id]/domains/status/route.ts b/app/api/v1/apps/[id]/domains/status/route.ts index 0bcb6c2ef..5ccbb5756 100644 --- a/app/api/v1/apps/[id]/domains/status/route.ts +++ b/app/api/v1/apps/[id]/domains/status/route.ts @@ -27,20 +27,29 @@ interface RouteParams { * POST /api/v1/apps/:id/domains/status * Check the DNS status of a domain */ -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } const body = await request.json(); const validation = StatusSchema.safeParse(body); if (!validation.success) { - return NextResponse.json({ success: false, error: "Invalid domain format" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid domain format" }, + { status: 400 }, + ); } const { domain } = validation.data; @@ -60,7 +69,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi // Get DNS instructions based on domain type const isApex = vercelDomainsService.isApexDomain(domain); - const dnsInstructions = vercelDomainsService.getDnsInstructions(domain, isApex); + const dnsInstructions = vercelDomainsService.getDnsInstructions( + domain, + isApex, + ); // If verified, sync status to database if (status.verified) { diff --git a/app/api/v1/apps/[id]/domains/sync/route.ts b/app/api/v1/apps/[id]/domains/sync/route.ts index 5e8a5e3c8..5a9c17b23 100644 --- a/app/api/v1/apps/[id]/domains/sync/route.ts +++ b/app/api/v1/apps/[id]/domains/sync/route.ts @@ -19,13 +19,19 @@ interface RouteParams { * POST /api/v1/apps/:id/domains/sync * Sync all domain statuses from Vercel */ -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } logger.info("[Domains API] Syncing domain status", { diff --git a/app/api/v1/apps/[id]/domains/verify/route.ts b/app/api/v1/apps/[id]/domains/verify/route.ts index ad131406a..faad5234e 100644 --- a/app/api/v1/apps/[id]/domains/verify/route.ts +++ b/app/api/v1/apps/[id]/domains/verify/route.ts @@ -28,20 +28,29 @@ interface RouteParams { * POST /api/v1/apps/:id/domains/verify * Verify domain ownership */ -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; const app = await appsService.getById(appId); if (!app || app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } const body = await request.json(); const validation = VerifySchema.safeParse(body); if (!validation.success) { - return NextResponse.json({ success: false, error: "Invalid domain format" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid domain format" }, + { status: 400 }, + ); } const { domain } = validation.data; @@ -73,7 +82,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi // Get updated status regardless const status = await vercelDomainsService.getDomainStatus(appId, domain); const isApex = vercelDomainsService.isApexDomain(domain); - const dnsInstructions = vercelDomainsService.getDnsInstructions(domain, isApex); + const dnsInstructions = vercelDomainsService.getDnsInstructions( + domain, + isApex, + ); return NextResponse.json({ success: true, diff --git a/app/api/v1/apps/[id]/earnings/history/route.ts b/app/api/v1/apps/[id]/earnings/history/route.ts index 928141475..73faeeaee 100644 --- a/app/api/v1/apps/[id]/earnings/history/route.ts +++ b/app/api/v1/apps/[id]/earnings/history/route.ts @@ -8,7 +8,9 @@ import { logger } from "@/lib/utils/logger"; const QuerySchema = z.object({ limit: z.coerce.number().int().positive().max(100).optional().default(50), offset: z.coerce.number().int().min(0).optional().default(0), - type: z.enum(["inference_markup", "purchase_share", "withdrawal", "adjustment"]).optional(), + type: z + .enum(["inference_markup", "purchase_share", "withdrawal", "adjustment"]) + .optional(), }); /** @@ -26,7 +28,10 @@ const QuerySchema = z.object({ * @param params - Route parameters containing the app ID. * @returns Transaction history with pagination information. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -100,7 +105,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get earnings history", + error: + error instanceof Error + ? error.message + : "Failed to get earnings history", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/earnings/route.ts b/app/api/v1/apps/[id]/earnings/route.ts index 20b8e4674..f64e4464f 100644 --- a/app/api/v1/apps/[id]/earnings/route.ts +++ b/app/api/v1/apps/[id]/earnings/route.ts @@ -121,7 +121,11 @@ function generateTestData(days: number) { // Weight towards inference_markup (most common) const typeRoll = Math.random(); const type = - typeRoll < 0.6 ? "inference_markup" : typeRoll < 0.9 ? "purchase_share" : "withdrawal"; + typeRoll < 0.6 + ? "inference_markup" + : typeRoll < 0.9 + ? "purchase_share" + : "withdrawal"; let amount: number; let description: string; @@ -169,29 +173,41 @@ function generateTestData(days: number) { * @param params - Route parameters containing the app ID. * @returns Earnings summary, breakdown by period, chart data, recent transactions, and monetization settings. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; const daysParam = request.nextUrl.searchParams.get("days"); - const days = daysParam ? Math.min(Math.max(parseInt(daysParam, 10), 1), 90) : 30; + const days = daysParam + ? Math.min(Math.max(parseInt(daysParam, 10), 1), 90) + : 30; // Check for testData flag - ONLY allowed in development mode // Double-check with VERCEL_ENV to prevent misconfigured deployments const testDataParam = request.nextUrl.searchParams.get("testData"); const isDevelopment = - process.env.NODE_ENV === "development" && process.env.VERCEL_ENV !== "production"; + process.env.NODE_ENV === "development" && + process.env.VERCEL_ENV !== "production"; const useTestData = isDevelopment && testDataParam === "true"; const app = await appsService.getById(id); if (!app) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } // Return test data if requested (for UI verification) @@ -213,12 +229,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }); } - const [summary, breakdown, recentTransactions, chartData] = await Promise.all([ - appEarningsService.getEarningsSummary(id), - appEarningsService.getEarningsBreakdown(id), - appEarningsService.getTransactionHistory(id, { limit: 10 }), - appEarningsService.getDailyEarningsChart(id, days), - ]); + const [summary, breakdown, recentTransactions, chartData] = + await Promise.all([ + appEarningsService.getEarningsSummary(id), + appEarningsService.getEarningsBreakdown(id), + appEarningsService.getTransactionHistory(id, { limit: 10 }), + appEarningsService.getDailyEarningsChart(id, days), + ]); return NextResponse.json({ success: true, @@ -237,7 +254,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get app earnings", + error: + error instanceof Error ? error.message : "Failed to get app earnings", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/earnings/withdraw/route.ts b/app/api/v1/apps/[id]/earnings/withdraw/route.ts index 9db1de03f..68f8d33d7 100644 --- a/app/api/v1/apps/[id]/earnings/withdraw/route.ts +++ b/app/api/v1/apps/[id]/earnings/withdraw/route.ts @@ -18,7 +18,10 @@ const WithdrawRequestSchema = z.object({ amount: z .number() .positive("Amount must be positive") - .max(MAXIMUM_WITHDRAWAL, `Maximum withdrawal is $${MAXIMUM_WITHDRAWAL.toLocaleString()}`), + .max( + MAXIMUM_WITHDRAWAL, + `Maximum withdrawal is $${MAXIMUM_WITHDRAWAL.toLocaleString()}`, + ), idempotency_key: z .string() .min(16, "Idempotency key must be at least 16 characters") @@ -61,11 +64,17 @@ async function handlePOST(request: NextRequest, context: RouteContext) { const app = await appsService.getById(id); if (!app) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } // CRITICAL: Only the app creator can withdraw earnings @@ -114,7 +123,8 @@ async function handlePOST(request: NextRequest, context: RouteContext) { // Get earnings summary to read the actual payout threshold from database const earningsSummary = await appEarningsService.getEarningsSummary(id); - const minimumPayout = earningsSummary?.payoutThreshold ?? DEFAULT_MINIMUM_PAYOUT; + const minimumPayout = + earningsSummary?.payoutThreshold ?? DEFAULT_MINIMUM_PAYOUT; // Early validation: fail fast if amount below minimum (using database value) if (amount < minimumPayout) { @@ -127,7 +137,11 @@ async function handlePOST(request: NextRequest, context: RouteContext) { ); } - const result = await appEarningsService.requestWithdrawal(id, amount, idempotency_key); + const result = await appEarningsService.requestWithdrawal( + id, + amount, + idempotency_key, + ); if (!result.success) { logger.warn("[Withdrawal] Request failed", { @@ -137,7 +151,10 @@ async function handlePOST(request: NextRequest, context: RouteContext) { error: result.message, }); - return NextResponse.json({ success: false, error: result.message }, { status: 400 }); + return NextResponse.json( + { success: false, error: result.message }, + { status: 400 }, + ); } logger.info("[Withdrawal] Request successful", { diff --git a/app/api/v1/apps/[id]/monetization/route.ts b/app/api/v1/apps/[id]/monetization/route.ts index c52756368..5cb2797a0 100644 --- a/app/api/v1/apps/[id]/monetization/route.ts +++ b/app/api/v1/apps/[id]/monetization/route.ts @@ -20,7 +20,10 @@ const UpdateMonetizationSchema = z.object({ * @param params - Route parameters containing the app ID. * @returns Monetization settings including markup percentages and enabled status. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -28,11 +31,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const app = await appsService.getById(id); if (!app) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } const settings = await appCreditsService.getMonetizationSettings(id); @@ -43,7 +52,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get monetization settings", + error: + error instanceof Error + ? error.message + : "Failed to get monetization settings", }, { status: 500 }, ); @@ -64,7 +76,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ * @param params - Route parameters containing the app ID. * @returns Updated monetization settings. */ -export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -72,11 +87,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ const app = await appsService.getById(id); if (!app) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } const body = await request.json(); @@ -93,7 +114,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ ); } - await appCreditsService.updateMonetizationSettings(id, validationResult.data); + await appCreditsService.updateMonetizationSettings( + id, + validationResult.data, + ); const updatedSettings = await appCreditsService.getMonetizationSettings(id); logger.info(`Updated monetization settings for app: ${id}`, { @@ -108,7 +132,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to update monetization settings", + error: + error instanceof Error + ? error.message + : "Failed to update monetization settings", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/promote/analytics/route.ts b/app/api/v1/apps/[id]/promote/analytics/route.ts index fa7f8490d..8c2acbbd3 100644 --- a/app/api/v1/apps/[id]/promote/analytics/route.ts +++ b/app/api/v1/apps/[id]/promote/analytics/route.ts @@ -25,7 +25,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const days = parseInt(url.searchParams.get("days") || "30"); const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000); - const campaigns = await advertisingService.listCampaigns(user.organization_id!, { appId: id }); + const campaigns = await advertisingService.listCampaigns( + user.organization_id!, + { appId: id }, + ); const totals = campaigns.reduce( (acc, c) => ({ @@ -38,7 +41,8 @@ export async function GET(request: NextRequest, { params }: RouteParams) { ); const round2 = (n: number) => Math.round(n * 100) / 100; - const safeDiv = (a: number, b: number, mult = 1) => (b > 0 ? (a / b) * mult : 0); + const safeDiv = (a: number, b: number, mult = 1) => + b > 0 ? (a / b) * mult : 0; const attribution = await conversionTrackingService.getCampaignAttribution( user.organization_id!, diff --git a/app/api/v1/apps/[id]/promote/preview/route.ts b/app/api/v1/apps/[id]/promote/preview/route.ts index d3543c4e2..d350e962f 100644 --- a/app/api/v1/apps/[id]/promote/preview/route.ts +++ b/app/api/v1/apps/[id]/promote/preview/route.ts @@ -40,7 +40,10 @@ interface PostPreview { timestamp: string; } -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -97,12 +100,18 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi if (platforms.includes("discord")) { generatePromises.push( (async () => { - const postTypes = ["promotional", "engagement", "educational", "announcement"] as const; + const postTypes = [ + "promotional", + "engagement", + "educational", + "announcement", + ] as const; for (let i = 0; i < Math.min(count, postTypes.length); i++) { - const content = await discordAppAutomationService.generateAnnouncement( - user.organization_id, - previewApp, - ); + const content = + await discordAppAutomationService.generateAnnouncement( + user.organization_id, + previewApp, + ); previews.push({ platform: "discord", content, @@ -111,7 +120,8 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi }); } })().catch((error) => { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("[Promote Preview API] Discord generation failed", { appId: id, error: errorMessage, @@ -124,12 +134,18 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi if (platforms.includes("telegram")) { generatePromises.push( (async () => { - const postTypes = ["announcement", "update", "feature", "community"] as const; + const postTypes = [ + "announcement", + "update", + "feature", + "community", + ] as const; for (let i = 0; i < Math.min(count, postTypes.length); i++) { - const content = await telegramAppAutomationService.generateAnnouncement( - user.organization_id, - previewApp, - ); + const content = + await telegramAppAutomationService.generateAnnouncement( + user.organization_id, + previewApp, + ); previews.push({ platform: "telegram", content, @@ -138,7 +154,8 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi }); } })().catch((error) => { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("[Promote Preview API] Telegram generation failed", { appId: id, error: errorMessage, @@ -151,7 +168,12 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi if (platforms.includes("twitter")) { generatePromises.push( (async () => { - const tweetTypes = ["promotional", "engagement", "educational", "announcement"] as const; + const tweetTypes = [ + "promotional", + "engagement", + "educational", + "announcement", + ] as const; for (let i = 0; i < Math.min(count, tweetTypes.length); i++) { const tweet = await twitterAppAutomationService.generateAppTweet( user.organization_id, @@ -166,7 +188,8 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi }); } })().catch((error) => { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("[Promote Preview API] Twitter generation failed", { appId: id, error: errorMessage, diff --git a/app/api/v1/apps/[id]/promote/route.ts b/app/api/v1/apps/[id]/promote/route.ts index 2a663871c..b0f25ec3f 100644 --- a/app/api/v1/apps/[id]/promote/route.ts +++ b/app/api/v1/apps/[id]/promote/route.ts @@ -1,7 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { appPromotionService, type PromotionConfig } from "@/lib/services/app-promotion"; +import { + appPromotionService, + type PromotionConfig, +} from "@/lib/services/app-promotion"; import { logger } from "@/lib/utils/logger"; export const dynamic = "force-dynamic"; @@ -58,7 +61,12 @@ const PromotionConfigSchema = z.object({ adAccountId: z.string().uuid(), budget: z.number().positive().max(10000), budgetType: z.enum(["daily", "lifetime"]), - objective: z.enum(["awareness", "traffic", "engagement", "app_promotion"]), + objective: z.enum([ + "awareness", + "traffic", + "engagement", + "app_promotion", + ]), duration: z.number().int().positive().max(365).optional(), targetLocations: z.array(z.string().length(2)).max(50).optional(), }) @@ -114,11 +122,17 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const isHistory = url.searchParams.get("history") === "true"; if (isHistory) { - const history = await appPromotionService.getPromotionHistory(user.organization_id, id); + const history = await appPromotionService.getPromotionHistory( + user.organization_id, + id, + ); return NextResponse.json(history); } - const suggestions = await appPromotionService.getPromotionSuggestions(user.organization_id, id); + const suggestions = await appPromotionService.getPromotionSuggestions( + user.organization_id, + id, + ); return NextResponse.json(suggestions); } @@ -155,34 +169,47 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (config.channels.includes("advertising") && !config.advertising) { return NextResponse.json( { - error: "Advertising config required when advertising channel is selected", + error: + "Advertising config required when advertising channel is selected", }, { status: 400 }, ); } - if (config.channels.includes("twitter_automation") && !config.twitterAutomation) { + if ( + config.channels.includes("twitter_automation") && + !config.twitterAutomation + ) { return NextResponse.json( { - error: "Twitter automation config required when twitter_automation channel is selected", + error: + "Twitter automation config required when twitter_automation channel is selected", }, { status: 400 }, ); } - if (config.channels.includes("telegram_automation") && !config.telegramAutomation) { + if ( + config.channels.includes("telegram_automation") && + !config.telegramAutomation + ) { return NextResponse.json( { - error: "Telegram automation config required when telegram_automation channel is selected", + error: + "Telegram automation config required when telegram_automation channel is selected", }, { status: 400 }, ); } - if (config.channels.includes("discord_automation") && !config.discordAutomation) { + if ( + config.channels.includes("discord_automation") && + !config.discordAutomation + ) { return NextResponse.json( { - error: "Discord automation config required when discord_automation channel is selected", + error: + "Discord automation config required when discord_automation channel is selected", }, { status: 400 }, ); @@ -194,7 +221,12 @@ export async function POST(request: NextRequest, { params }: RouteParams) { userId: user.id, }); - const result = await appPromotionService.promoteApp(user.organization_id, user.id, id, config); + const result = await appPromotionService.promoteApp( + user.organization_id, + user.id, + id, + config, + ); logger.info("[Promote API] Promotion complete", { appId: id, diff --git a/app/api/v1/apps/[id]/public/route.ts b/app/api/v1/apps/[id]/public/route.ts index fb00021d6..ebd437271 100644 --- a/app/api/v1/apps/[id]/public/route.ts +++ b/app/api/v1/apps/[id]/public/route.ts @@ -11,7 +11,8 @@ export const dynamic = "force-dynamic"; const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id, X-Request-ID", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id, X-Request-ID", "Access-Control-Max-Age": "86400", }; @@ -34,7 +35,10 @@ export async function OPTIONS() { * * Only returns non-sensitive information like name, description, logo. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { id } = await params; @@ -51,7 +55,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ is_approved: apps.is_approved, }) .from(apps) - .where(and(eq(apps.id, id), eq(apps.is_active, true), eq(apps.is_approved, true))) + .where( + and( + eq(apps.id, id), + eq(apps.is_active, true), + eq(apps.is_approved, true), + ), + ) .limit(1); if (!app) { diff --git a/app/api/v1/apps/[id]/regenerate-api-key/route.ts b/app/api/v1/apps/[id]/regenerate-api-key/route.ts index 867407235..8f5097f0e 100644 --- a/app/api/v1/apps/[id]/regenerate-api-key/route.ts +++ b/app/api/v1/apps/[id]/regenerate-api-key/route.ts @@ -13,7 +13,10 @@ import { logger } from "@/lib/utils/logger"; * @param params - Route parameters containing the app ID. * @returns New API key (only shown once). */ -async function handlePOST(request: NextRequest, context: { params: Promise<{ id: string }> }) { +async function handlePOST( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { if (!context) { return NextResponse.json( { success: false, error: "Missing route parameters" }, @@ -59,14 +62,18 @@ async function handlePOST(request: NextRequest, context: { params: Promise<{ id: return NextResponse.json({ success: true, apiKey: newApiKey, // Only returned once - message: "API key regenerated successfully. Make sure to save it securely.", + message: + "API key regenerated successfully. Make sure to save it securely.", }); } catch (error) { logger.error("Failed to regenerate API key:", error); return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to regenerate API key", + error: + error instanceof Error + ? error.message + : "Failed to regenerate API key", }, { status: 500 }, ); diff --git a/app/api/v1/apps/[id]/route.ts b/app/api/v1/apps/[id]/route.ts index 4a0c17252..d8a444bc5 100644 --- a/app/api/v1/apps/[id]/route.ts +++ b/app/api/v1/apps/[id]/route.ts @@ -9,7 +9,10 @@ import { logger } from "@/lib/utils/logger"; // - Omitted field → undefined (Drizzle skips, keeps old value) // - Empty string "" → null (Drizzle sets NULL, clears the value) // - Valid URL → the URL string -const optionalUrl = z.preprocess((val) => (val === "" ? null : val), z.string().url().nullish()); +const optionalUrl = z.preprocess( + (val) => (val === "" ? null : val), + z.string().url().nullish(), +); const optionalEmail = z.preprocess( (val) => (val === "" ? null : val), @@ -37,7 +40,10 @@ const UpdateAppSchema = z.object({ * @param params - Route parameters containing the app ID. * @returns App details. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -45,11 +51,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const app = await appsService.getById(id); if (!app) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (app.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } return NextResponse.json({ success: true, app }); @@ -74,7 +86,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ * @param params - Route parameters containing the app ID. * @returns Updated app details. */ -export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -82,11 +97,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ const existingApp = await appsService.getById(id); if (!existingApp) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (existingApp.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } const body = await request.json(); @@ -138,7 +159,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ * @param params - Route parameters containing the app ID. * @returns Updated app details. */ -export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -146,11 +170,17 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const existingApp = await appsService.getById(id); if (!existingApp) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (existingApp.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } const body = await request.json(); @@ -219,17 +249,24 @@ export async function DELETE( const existingApp = await appsService.getById(id); if (!existingApp) { - return NextResponse.json({ success: false, error: "App not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "App not found" }, + { status: 404 }, + ); } if (existingApp.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Access denied" }, { status: 403 }); + return NextResponse.json( + { success: false, error: "Access denied" }, + { status: 403 }, + ); } // Parse optional query params for cleanup options const searchParams = request.nextUrl.searchParams; const deleteGitHubRepo = searchParams.get("deleteGitHubRepo") !== "false"; - const deleteVercelProject = searchParams.get("deleteVercelProject") !== "false"; + const deleteVercelProject = + searchParams.get("deleteVercelProject") !== "false"; // Perform comprehensive cleanup and delete const cleanupResult = await appCleanupService.deleteAppWithCleanup(id, { @@ -252,7 +289,8 @@ export async function DELETE( ? "App deleted successfully with all resources cleaned up" : "App deleted with some cleanup errors", cleaned: cleanupResult.cleaned, - errors: cleanupResult.errors.length > 0 ? cleanupResult.errors : undefined, + errors: + cleanupResult.errors.length > 0 ? cleanupResult.errors : undefined, }); } catch (error) { logger.error("Failed to delete app:", error); diff --git a/app/api/v1/apps/[id]/telegram-automation/post/route.ts b/app/api/v1/apps/[id]/telegram-automation/post/route.ts index 537618075..e3671945a 100644 --- a/app/api/v1/apps/[id]/telegram-automation/post/route.ts +++ b/app/api/v1/apps/[id]/telegram-automation/post/route.ts @@ -22,7 +22,10 @@ const postSchema = z.object({ groupId: z.string().optional(), }); -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; @@ -37,7 +40,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } try { @@ -50,7 +56,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi ); if (!result.success) { - return NextResponse.json({ error: result.error || "Failed to post" }, { status: 400 }); + return NextResponse.json( + { error: result.error || "Failed to post" }, + { status: 400 }, + ); } logger.info("[Telegram Post] Announcement posted", { diff --git a/app/api/v1/apps/[id]/telegram-automation/route.ts b/app/api/v1/apps/[id]/telegram-automation/route.ts index 3b170c41a..f5da51b9d 100644 --- a/app/api/v1/apps/[id]/telegram-automation/route.ts +++ b/app/api/v1/apps/[id]/telegram-automation/route.ts @@ -31,7 +31,10 @@ const automationConfigSchema = z.object({ agentCharacterId: z.string().uuid().optional(), // Character voice for posts }); -export async function GET(request: NextRequest, { params }: RouteParams): Promise { +export async function GET( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; @@ -49,11 +52,17 @@ export async function GET(request: NextRequest, { params }: RouteParams): Promis appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to get automation status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get automation status" }, + { status: 500 }, + ); } } -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; @@ -68,7 +77,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } // Validate interval range - defaults are min=120, max=240 @@ -122,23 +134,35 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi if (error instanceof Error && error.message === "App not found") { return NextResponse.json({ error: "App not found" }, { status: 404 }); } - if (error instanceof Error && error.message.includes("Telegram bot not connected")) { + if ( + error instanceof Error && + error.message.includes("Telegram bot not connected") + ) { return NextResponse.json({ error: error.message }, { status: 400 }); } logger.error("[Telegram Automation] Failed to enable", { appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to enable automation" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to enable automation" }, + { status: 500 }, + ); } } -export async function DELETE(request: NextRequest, { params }: RouteParams): Promise { +export async function DELETE( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: appId } = await params; try { - await telegramAppAutomationService.disableAutomation(user.organization_id, appId); + await telegramAppAutomationService.disableAutomation( + user.organization_id, + appId, + ); logger.info("[Telegram Automation] Automation disabled", { appId, @@ -154,6 +178,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams): Pro appId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to disable automation" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to disable automation" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/apps/[id]/twitter-automation/post/route.ts b/app/api/v1/apps/[id]/twitter-automation/post/route.ts index 2b66b574b..ada48da09 100644 --- a/app/api/v1/apps/[id]/twitter-automation/post/route.ts +++ b/app/api/v1/apps/[id]/twitter-automation/post/route.ts @@ -13,10 +13,15 @@ interface RouteParams { const PostTweetSchema = z.object({ text: z.string().max(280).optional(), - type: z.enum(["promotional", "engagement", "educational", "announcement"]).optional(), + type: z + .enum(["promotional", "engagement", "educational", "announcement"]) + .optional(), }); -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -45,7 +50,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi if (!result.success) { const status = result.error === "App not found" ? 404 : 400; - return NextResponse.json({ error: result.error || "Failed to post tweet" }, { status }); + return NextResponse.json( + { error: result.error || "Failed to post tweet" }, + { status }, + ); } return NextResponse.json({ @@ -57,13 +65,19 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi if (error instanceof Error && error.message === "App not found") { return NextResponse.json({ error: "App not found" }, { status: 404 }); } - if (error instanceof Error && error.message.includes("Insufficient credits")) { + if ( + error instanceof Error && + error.message.includes("Insufficient credits") + ) { return NextResponse.json({ error: error.message }, { status: 402 }); } logger.error("[Twitter Automation API] Failed to post tweet", { appId: id, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to post tweet. Please try again." }, { status: 500 }); + return NextResponse.json( + { error: "Failed to post tweet. Please try again." }, + { status: 500 }, + ); } } diff --git a/app/api/v1/apps/[id]/twitter-automation/route.ts b/app/api/v1/apps/[id]/twitter-automation/route.ts index e46f7f552..71809bc1b 100644 --- a/app/api/v1/apps/[id]/twitter-automation/route.ts +++ b/app/api/v1/apps/[id]/twitter-automation/route.ts @@ -23,16 +23,25 @@ const TwitterAutomationConfigSchema = z.object({ topics: z.array(z.string().max(50)).max(10).optional(), }); -export async function GET(request: NextRequest, { params }: RouteParams): Promise { +export async function GET( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; - const status = await twitterAppAutomationService.getAutomationStatus(user.organization_id, id); + const status = await twitterAppAutomationService.getAutomationStatus( + user.organization_id, + id, + ); return NextResponse.json(status); } -export async function POST(request: NextRequest, { params }: RouteParams): Promise { +export async function POST( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -99,7 +108,10 @@ export async function POST(request: NextRequest, { params }: RouteParams): Promi }); } -export async function DELETE(request: NextRequest, { params }: RouteParams): Promise { +export async function DELETE( + request: NextRequest, + { params }: RouteParams, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; @@ -108,7 +120,10 @@ export async function DELETE(request: NextRequest, { params }: RouteParams): Pro userId: user.id, }); - const app = await twitterAppAutomationService.disableAutomation(user.organization_id, id); + const app = await twitterAppAutomationService.disableAutomation( + user.organization_id, + id, + ); return NextResponse.json({ success: true, diff --git a/app/api/v1/apps/[id]/users/route.ts b/app/api/v1/apps/[id]/users/route.ts index d0a161a7e..a142fa7f2 100644 --- a/app/api/v1/apps/[id]/users/route.ts +++ b/app/api/v1/apps/[id]/users/route.ts @@ -15,12 +15,17 @@ import { logger } from "@/lib/utils/logger"; * @param params - Route parameters containing the app ID. * @returns List of app users with pagination information. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; const { searchParams } = new URL(request.url); - const limit = searchParams.get("limit") ? parseInt(searchParams.get("limit")!) : undefined; + const limit = searchParams.get("limit") + ? parseInt(searchParams.get("limit")!) + : undefined; // Verify the app exists and belongs to the user's organization const existingApp = await appsService.getById(id); @@ -61,7 +66,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get app users", + error: + error instanceof Error ? error.message : "Failed to get app users", }, { status: 500 }, ); diff --git a/app/api/v1/apps/check-name/route.ts b/app/api/v1/apps/check-name/route.ts index c69bf7ed2..5abc5b748 100644 --- a/app/api/v1/apps/check-name/route.ts +++ b/app/api/v1/apps/check-name/route.ts @@ -52,7 +52,8 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to check app name", + error: + error instanceof Error ? error.message : "Failed to check app name", }, { status: 500 }, ); diff --git a/app/api/v1/billing/settings/route.ts b/app/api/v1/billing/settings/route.ts index 4adb0e7af..5fea71480 100644 --- a/app/api/v1/billing/settings/route.ts +++ b/app/api/v1/billing/settings/route.ts @@ -29,7 +29,10 @@ import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { AUTO_TOP_UP_LIMITS, autoTopUpService } from "@/lib/services/auto-top-up"; +import { + AUTO_TOP_UP_LIMITS, + autoTopUpService, +} from "@/lib/services/auto-top-up"; import { logger } from "@/lib/utils/logger"; const UpdateSettingsSchema = z.object({ @@ -79,7 +82,9 @@ async function handleGET(req: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(req); - const autoTopUpSettings = await autoTopUpService.getSettings(user.organization_id); + const autoTopUpSettings = await autoTopUpService.getSettings( + user.organization_id, + ); return NextResponse.json({ success: true, @@ -102,7 +107,10 @@ async function handleGET(req: NextRequest) { logger.error("[Billing Settings API] Error getting settings:", error); if (isAuthenticationError(error)) { - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); } return NextResponse.json( @@ -160,7 +168,9 @@ async function handlePUT(req: NextRequest) { } // Return updated settings - const updatedSettings = await autoTopUpService.getSettings(user.organization_id); + const updatedSettings = await autoTopUpService.getSettings( + user.organization_id, + ); return NextResponse.json({ success: true, @@ -178,7 +188,10 @@ async function handlePUT(req: NextRequest) { logger.error("[Billing Settings API] Error updating settings:", error); if (isAuthenticationError(error)) { - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); } // Allow domain-specific validation messages through (e.g. "Cannot enable @@ -190,7 +203,12 @@ async function handlePUT(req: NextRequest) { message.includes("cannot exceed"); return NextResponse.json( - { success: false, error: isValidationError ? message : "Failed to update billing settings" }, + { + success: false, + error: isValidationError + ? message + : "Failed to update billing settings", + }, { status: isValidationError ? 400 : 500 }, ); } diff --git a/app/api/v1/blooio/connect/route.ts b/app/api/v1/blooio/connect/route.ts index 4567f42bf..69e6769ae 100644 --- a/app/api/v1/blooio/connect/route.ts +++ b/app/api/v1/blooio/connect/route.ts @@ -34,13 +34,18 @@ export async function POST(request: NextRequest): Promise { } if (!body || typeof body !== "object" || Array.isArray(body)) { - return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); + return NextResponse.json( + { error: "Request body must be a JSON object" }, + { status: 400 }, + ); } const parsedBody = blooioConnectSchema.safeParse(body); if (!parsedBody.success) { return NextResponse.json( - { error: parsedBody.error.issues[0]?.message || "Invalid request body" }, + { + error: parsedBody.error.issues[0]?.message || "Invalid request body", + }, { status: 400 }, ); } @@ -53,18 +58,27 @@ export async function POST(request: NextRequest): Promise { const validation = await blooioAutomationService.validateApiKey(apiKey); if (!validation.valid) { - return NextResponse.json({ error: validation.error || "Invalid API key" }, { status: 400 }); + return NextResponse.json( + { error: validation.error || "Invalid API key" }, + { status: 400 }, + ); } // Store credentials - await blooioAutomationService.storeCredentials(user.organization_id, user.id, { - apiKey, - webhookSecret, - fromNumber, - }); + await blooioAutomationService.storeCredentials( + user.organization_id, + user.id, + { + apiKey, + webhookSecret, + fromNumber, + }, + ); // Get the webhook URL to display to user - const webhookUrl = blooioAutomationService.getWebhookUrl(user.organization_id); + const webhookUrl = blooioAutomationService.getWebhookUrl( + user.organization_id, + ); await invalidateOAuthState(user.organization_id, "blooio", user.id); @@ -86,6 +100,9 @@ export async function POST(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to connect Blooio" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to connect Blooio" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/blooio/disconnect/route.ts b/app/api/v1/blooio/disconnect/route.ts index 020b06a9a..367bfd415 100644 --- a/app/api/v1/blooio/disconnect/route.ts +++ b/app/api/v1/blooio/disconnect/route.ts @@ -18,7 +18,10 @@ async function handleDisconnect(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); try { - await blooioAutomationService.removeCredentials(user.organization_id, user.id); + await blooioAutomationService.removeCredentials( + user.organization_id, + user.id, + ); await invalidateOAuthState(user.organization_id, "blooio", user.id); @@ -36,7 +39,10 @@ async function handleDisconnect(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to disconnect Blooio" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to disconnect Blooio" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/blooio/status/route.ts b/app/api/v1/blooio/status/route.ts index b90989396..1b585d4fb 100644 --- a/app/api/v1/blooio/status/route.ts +++ b/app/api/v1/blooio/status/route.ts @@ -37,6 +37,9 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), orgId, }); - return NextResponse.json({ error: "Failed to get Blooio status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get Blooio status" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/blooio/webhook-secret/route.ts b/app/api/v1/blooio/webhook-secret/route.ts index 5649a107a..3993184e0 100644 --- a/app/api/v1/blooio/webhook-secret/route.ts +++ b/app/api/v1/blooio/webhook-secret/route.ts @@ -22,7 +22,10 @@ export async function POST(request: NextRequest): Promise { const { webhookSecret } = await request.json(); if (!webhookSecret || typeof webhookSecret !== "string") { - return NextResponse.json({ error: "Webhook secret is required" }, { status: 400 }); + return NextResponse.json( + { error: "Webhook secret is required" }, + { status: 400 }, + ); } if (!webhookSecret.startsWith("whsec_")) { @@ -39,7 +42,10 @@ export async function POST(request: NextRequest): Promise { ]); if (!apiKey) { - return NextResponse.json({ error: "Please connect Blooio first" }, { status: 400 }); + return NextResponse.json( + { error: "Please connect Blooio first" }, + { status: 400 }, + ); } await blooioAutomationService.storeCredentials(orgId, user.id, { @@ -58,6 +64,9 @@ export async function POST(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), orgId, }); - return NextResponse.json({ error: "Failed to save webhook secret" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to save webhook secret" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/chain/nfts/[chain]/[address]/route.ts b/app/api/v1/chain/nfts/[chain]/[address]/route.ts index 183ea80d3..eac342a52 100644 --- a/app/api/v1/chain/nfts/[chain]/[address]/route.ts +++ b/app/api/v1/chain/nfts/[chain]/[address]/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; import { isValidAddress } from "@/lib/services/proxy/services/address-validation"; -import { chainDataConfig, chainDataHandler } from "@/lib/services/proxy/services/chain-data"; +import { + chainDataConfig, + chainDataHandler, +} from "@/lib/services/proxy/services/chain-data"; import { ALCHEMY_SLUGS } from "@/lib/services/proxy/services/rpc"; export const maxDuration = 30; @@ -37,7 +40,10 @@ export async function GET( // Validate address format if (!isValidAddress(normalized, address)) { return applyCorsHeaders( - NextResponse.json({ error: "Invalid address format for this chain" }, { status: 400 }), + NextResponse.json( + { error: "Invalid address format for this chain" }, + { status: 400 }, + ), CORS_METHODS, ); } diff --git a/app/api/v1/chain/tokens/[chain]/[address]/route.ts b/app/api/v1/chain/tokens/[chain]/[address]/route.ts index eabeba3f7..5fed0e1d6 100644 --- a/app/api/v1/chain/tokens/[chain]/[address]/route.ts +++ b/app/api/v1/chain/tokens/[chain]/[address]/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; import { isValidAddress } from "@/lib/services/proxy/services/address-validation"; -import { chainDataConfig, chainDataHandler } from "@/lib/services/proxy/services/chain-data"; +import { + chainDataConfig, + chainDataHandler, +} from "@/lib/services/proxy/services/chain-data"; import { ALCHEMY_SLUGS } from "@/lib/services/proxy/services/rpc"; export const maxDuration = 30; @@ -37,7 +40,10 @@ export async function GET( // Validate address format if (!isValidAddress(normalized, address)) { return applyCorsHeaders( - NextResponse.json({ error: "Invalid address format for this chain" }, { status: 400 }), + NextResponse.json( + { error: "Invalid address format for this chain" }, + { status: 400 }, + ), CORS_METHODS, ); } diff --git a/app/api/v1/chain/transfers/[chain]/[address]/route.ts b/app/api/v1/chain/transfers/[chain]/[address]/route.ts index 475d6b386..40b74120f 100644 --- a/app/api/v1/chain/transfers/[chain]/[address]/route.ts +++ b/app/api/v1/chain/transfers/[chain]/[address]/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; import { isValidAddress } from "@/lib/services/proxy/services/address-validation"; -import { chainDataConfig, chainDataHandler } from "@/lib/services/proxy/services/chain-data"; +import { + chainDataConfig, + chainDataHandler, +} from "@/lib/services/proxy/services/chain-data"; import { ALCHEMY_SLUGS } from "@/lib/services/proxy/services/rpc"; export const maxDuration = 30; @@ -37,7 +40,10 @@ export async function GET( // Validate address format if (!isValidAddress(normalized, address)) { return applyCorsHeaders( - NextResponse.json({ error: "Invalid address format for this chain" }, { status: 400 }), + NextResponse.json( + { error: "Invalid address format for this chain" }, + { status: 400 }, + ), CORS_METHODS, ); } diff --git a/app/api/v1/character-assistant/route.ts b/app/api/v1/character-assistant/route.ts index aff18c016..3ccc8b102 100644 --- a/app/api/v1/character-assistant/route.ts +++ b/app/api/v1/character-assistant/route.ts @@ -117,10 +117,16 @@ export async function POST(request: Request) { } = body; if (!Array.isArray(messages) || messages.length === 0) { - return NextResponse.json({ error: "Messages array cannot be empty" }, { status: 400 }); + return NextResponse.json( + { error: "Messages array cannot be empty" }, + { status: 400 }, + ); } - const systemPrompt = isEditMode && character ? editSystemPrompt(character) : createSystemPrompt; + const systemPrompt = + isEditMode && character + ? editSystemPrompt(character) + : createSystemPrompt; const result = streamText({ model: "gpt-4o-mini", @@ -135,7 +141,9 @@ export async function POST(request: Request) { logger.error("Character assistant error:", error); const status = getErrorStatusCode(error); const errorMessage = - status === 500 ? "Failed to process character assistant request" : getSafeErrorMessage(error); + status === 500 + ? "Failed to process character assistant request" + : getSafeErrorMessage(error); return new Response( JSON.stringify({ error: errorMessage, diff --git a/app/api/v1/chat/completions/route.ts b/app/api/v1/chat/completions/route.ts index 2dd8fe414..51d52415b 100644 --- a/app/api/v1/chat/completions/route.ts +++ b/app/api/v1/chat/completions/route.ts @@ -9,12 +9,21 @@ * IMPORTANT: Do NOT call provider APIs directly. Always use AI SDK. */ -import { convertToModelMessages, generateText, streamText, type UIMessage } from "ai"; +import { + convertToModelMessages, + generateText, + streamText, + type UIMessage, +} from "ai"; import type { NextRequest } from "next/server"; import { getErrorStatusCode } from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { createPreflightResponse } from "@/lib/middleware/cors-apps"; -import { enforceOrgRateLimit, RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; +import { + enforceOrgRateLimit, + RateLimitPresets, + withRateLimit, +} from "@/lib/middleware/rate-limit"; import { calculateCost, getProviderFromModel, @@ -69,7 +78,10 @@ function computeEffectiveMaxTokens( if (cotBudget !== null) { // When CoT is active, ensure max_tokens covers both thinking budget AND response capacity // Without this, thinking consumes all tokens leaving nothing for the actual response - return Math.max(requestMaxTokens ?? MIN_RESPONSE_TOKENS, cotBudget + MIN_RESPONSE_TOKENS); + return Math.max( + requestMaxTokens ?? MIN_RESPONSE_TOKENS, + cotBudget + MIN_RESPONSE_TOKENS, + ); } return requestMaxTokens; } @@ -115,7 +127,10 @@ interface ChatRequest { parameters?: Record; }; }>; - tool_choice?: "auto" | "none" | { type: "function"; function: { name: string } }; + tool_choice?: + | "auto" + | "none" + | { type: "function"; function: { name: string } }; /** Enable provider-native web search. Defaults to false. */ webSearchEnabled?: boolean; /** Optional max search budget for provider-native web search. */ @@ -156,10 +171,14 @@ function addCorsHeaders(response: Response): Response { */ function inferImageMediaType(url: string): string { const lowerUrl = url.toLowerCase(); - if (lowerUrl.includes(".png") || lowerUrl.includes("image/png")) return "image/png"; - if (lowerUrl.includes(".gif") || lowerUrl.includes("image/gif")) return "image/gif"; - if (lowerUrl.includes(".webp") || lowerUrl.includes("image/webp")) return "image/webp"; - if (lowerUrl.includes(".svg") || lowerUrl.includes("image/svg")) return "image/svg+xml"; + if (lowerUrl.includes(".png") || lowerUrl.includes("image/png")) + return "image/png"; + if (lowerUrl.includes(".gif") || lowerUrl.includes("image/gif")) + return "image/gif"; + if (lowerUrl.includes(".webp") || lowerUrl.includes("image/webp")) + return "image/webp"; + if (lowerUrl.includes(".svg") || lowerUrl.includes("image/svg")) + return "image/svg+xml"; // Default to JPEG for .jpg, .jpeg, or unknown return "image/jpeg"; } @@ -171,7 +190,10 @@ function getImageUrl(imageUrl: { url: string } | string): string | null { return imageUrl.url || null; } -function inferFileMediaType(fileData: string | undefined, filename: string | undefined): string { +function inferFileMediaType( + fileData: string | undefined, + filename: string | undefined, +): string { const dataUrlMatch = fileData?.match(/^data:([^;,]+)[;,]/i); if (dataUrlMatch?.[1]) { return dataUrlMatch[1]; @@ -220,11 +242,14 @@ function convertToUIMessages(messages: ChatMessage[]): UIMessage[] { if (part.file) { const fileUrl = part.file.file_data; if (!fileUrl) { - logger.warn("[chat/completions] Ignoring file part without file_data", { - role: msg.role, - filename: part.file.filename, - hasFileId: typeof part.file.file_id === "string", - }); + logger.warn( + "[chat/completions] Ignoring file part without file_data", + { + role: msg.role, + filename: part.file.filename, + hasFileId: typeof part.file.file_id === "string", + }, + ); return null; } return { @@ -269,14 +294,18 @@ async function handlePOST(req: NextRequest) { // 1b. Per-org tier rate limit if (user.organization_id) { - const orgRateLimited = await enforceOrgRateLimit(user.organization_id, "completions"); + const orgRateLimited = await enforceOrgRateLimit( + user.organization_id, + "completions", + ); if (orgRateLimited) return orgRateLimited; } // 2. Check for app monetization const appId = req.headers.get("X-App-Id"); let useAppCredits = false; - let monetizedApp: Awaited> | null = null; + let monetizedApp: Awaited> | null = + null; if (appId) { monetizedApp = await appsService.getById(appId); @@ -326,10 +355,19 @@ async function handlePOST(req: NextRequest) { const billingSource = resolveAiProviderSource(model) ?? "gateway"; const cotBudget = resolveAnthropicThinkingBudgetTokens(model, process.env); const cotOptions = - cotBudget != null ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) : {}; - const effectiveMaxTokens = computeEffectiveMaxTokens(request.max_tokens, cotBudget); + cotBudget != null + ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) + : {}; + const effectiveMaxTokens = computeEffectiveMaxTokens( + request.max_tokens, + cotBudget, + ); const webSearchEnabled = request.webSearchEnabled === true; - const webSearchActive = isAnthropicWebSearchEnabled(provider, model, webSearchEnabled); + const webSearchActive = isAnthropicWebSearchEnabled( + provider, + model, + webSearchEnabled, + ); const webSearchOptions = buildProviderNativeWebSearchTools({ provider, model, @@ -343,7 +381,8 @@ async function handlePOST(req: NextRequest) { Response.json( { error: { - message: "Your account has been suspended due to policy violations.", + message: + "Your account has been suspended due to policy violations.", type: "account_suspended", code: "moderation_violation", }, @@ -354,24 +393,36 @@ async function handlePOST(req: NextRequest) { } // Start async moderation in background - const lastUserMessage = request.messages.filter((m) => m.role === "user").pop(); + const lastUserMessage = request.messages + .filter((m) => m.role === "user") + .pop(); if (lastUserMessage) { const content = getMessageContent(lastUserMessage); if (content) { - contentModerationService.moderateInBackground(content, user.id, undefined, (result) => { - logger.warn("[Chat Completions] Async moderation detected violation", { - userId: user.id, - categories: result.flaggedCategories, - }); - }); + contentModerationService.moderateInBackground( + content, + user.id, + undefined, + (result) => { + logger.warn( + "[Chat Completions] Async moderation detected violation", + { + userId: user.id, + categories: result.flaggedCategories, + }, + ); + }, + ); } } // 6. Estimate tokens and reserve credits const estimatedInputTokens = - estimateInputTokens(request.messages.map((m) => ({ content: getMessageContent(m) }))) + - (webSearchActive ? ANTHROPIC_WEB_SEARCH_INPUT_TOKEN_BUFFER : 0); - const estimatedOutputTokens = effectiveMaxTokens ?? request.max_tokens ?? 500; + estimateInputTokens( + request.messages.map((m) => ({ content: getMessageContent(m) })), + ) + (webSearchActive ? ANTHROPIC_WEB_SEARCH_INPUT_TOKEN_BUFFER : 0); + const estimatedOutputTokens = + effectiveMaxTokens ?? request.max_tokens ?? 500; const affiliateCode = req.headers.get("X-Affiliate-Code"); let reservation: CreditReservation; @@ -388,7 +439,10 @@ async function handlePOST(req: NextRequest) { estimatedOutputTokens, billingSource, ); - const costWithMarkup = await appCreditsService.calculateCostWithMarkup(appId, totalCost); + const costWithMarkup = await appCreditsService.calculateCostWithMarkup( + appId, + totalCost, + ); const balanceCheck = await appCreditsService.checkBalance( appId, @@ -454,8 +508,12 @@ async function handlePOST(req: NextRequest) { // 7. Convert messages for AI SDK const systemMessage = request.messages.find((m) => m.role === "system"); - const systemPrompt = systemMessage ? getMessageContent(systemMessage) : undefined; - const nonSystemMessages = request.messages.filter((m) => m.role !== "system"); + const systemPrompt = systemMessage + ? getMessageContent(systemMessage) + : undefined; + const nonSystemMessages = request.messages.filter( + (m) => m.role !== "system", + ); const uiMessages = convertToUIMessages(nonSystemMessages); logger.info("[Chat Completions] Request", { @@ -550,7 +608,9 @@ async function handleStreamingRequest( request: ChatRequest, user: { id: string; organization_id: string }, apiKey: { id: string } | null, - appCreditsInfo: { appId: string; estimatedBaseCost: number; app: unknown } | undefined, + appCreditsInfo: + | { appId: string; estimatedBaseCost: number; app: unknown } + | undefined, affiliateCode: string | null, startTime: number, abortSignal: AbortSignal | undefined, @@ -627,7 +687,9 @@ async function handleStreamingRequest( type: "chat", content: text, systemPrompt, - prompt: request.messages.map((m) => `[${m.role}] ${getMessageContent(m)}`).join("\n"), + prompt: request.messages + .map((m) => `[${m.role}] ${getMessageContent(m)}`) + .join("\n"), latencyMs: Date.now() - startTime, }, ); @@ -682,7 +744,9 @@ async function handleStreamingRequest( ], }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`), + ); } // Send final chunk with finish_reason @@ -699,7 +763,9 @@ async function handleStreamingRequest( }, ], }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`), + ); controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); } catch (error) { @@ -730,7 +796,9 @@ async function handleNonStreamingRequest( request: ChatRequest, user: { id: string; organization_id: string }, apiKey: { id: string } | null, - appCreditsInfo: { appId: string; estimatedBaseCost: number; app: unknown } | undefined, + appCreditsInfo: + | { appId: string; estimatedBaseCost: number; app: unknown } + | undefined, affiliateCode: string | null, startTime: number, abortSignal: AbortSignal | undefined, @@ -811,7 +879,9 @@ async function handleNonStreamingRequest( type: "chat", content: result.text, systemPrompt, - prompt: request.messages.map((m) => `[${m.role}] ${getMessageContent(m)}`).join("\n"), + prompt: request.messages + .map((m) => `[${m.role}] ${getMessageContent(m)}`) + .join("\n"), latencyMs: Date.now() - startTime, }, ); diff --git a/app/api/v1/chat/route.ts b/app/api/v1/chat/route.ts index 8a7350455..f36f47003 100644 --- a/app/api/v1/chat/route.ts +++ b/app/api/v1/chat/route.ts @@ -85,7 +85,9 @@ function normalizeMessages( /** * Extract text content from message parts. */ -function extractTextFromParts(parts: Array<{ type: string; text?: string }>): string { +function extractTextFromParts( + parts: Array<{ type: string; text?: string }>, +): string { return parts.map((p) => (p.type === "text" ? p.text : "")).join(""); } @@ -160,7 +162,10 @@ async function handlePOST(req: NextRequest) { // Validate messages array is not empty if (!rawMessages || rawMessages.length === 0) { - return NextResponse.json({ error: "Messages array cannot be empty" }, { status: 400 }); + return NextResponse.json( + { error: "Messages array cannot be empty" }, + { status: 400 }, + ); } // Normalize messages to UIMessage format (handles both OpenAI and UIMessage formats) @@ -168,7 +173,10 @@ async function handlePOST(req: NextRequest) { try { messages = normalizeMessages(rawMessages); } catch (error) { - if (error instanceof Error && error.message.includes("Invalid message role")) { + if ( + error instanceof Error && + error.message.includes("Invalid message role") + ) { return NextResponse.json({ error: error.message }, { status: 400 }); } throw error; @@ -177,7 +185,8 @@ async function handlePOST(req: NextRequest) { // Don't pass agent/conversation UUIDs to resolveModel — they're not tier // names or model IDs and would be treated as a custom model, causing // "model not found" errors (see elizaOS/eliza#6331). - const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const tierOrModel = tier || (id && !UUID_RE.test(id) ? id : undefined); const modelConfig = resolveModel(tierOrModel); const selectedModel = modelConfig.modelId; @@ -194,7 +203,10 @@ async function handlePOST(req: NextRequest) { const conversationId = metadata?.conversationId; if (!hasLanguageModelProviderConfigured(selectedModel)) { - return NextResponse.json({ error: getAiProviderConfigurationError() }, { status: 503 }); + return NextResponse.json( + { error: getAiProviderConfigurationError() }, + { status: 503 }, + ); } // Check if user is blocked due to moderation violations @@ -232,7 +244,9 @@ async function handlePOST(req: NextRequest) { // Handle anonymous user rate limiting if (isAnonymous && anonymousSession) { // Check message limit for anonymous users - const limitCheck = await checkAnonymousLimit(anonymousSession.session_token); + const limitCheck = await checkAnonymousLimit( + anonymousSession.session_token, + ); if (!limitCheck.allowed) { const errorMessage = @@ -267,11 +281,14 @@ async function handlePOST(req: NextRequest) { } // Reserve credits BEFORE making API call (prevents TOCTOU race condition) - let reservation: CreditReservation = creditsService.createAnonymousReservation(); + let reservation: CreditReservation = + creditsService.createAnonymousReservation(); const affiliateCode = req.headers.get("X-Affiliate-Code"); if (!isAnonymous && user.organization_id) { - const messageText = messages.map((m) => extractTextFromParts(m.parts)).join(" "); + const messageText = messages + .map((m) => extractTextFromParts(m.parts)) + .join(" "); const estimatedInputTokens = estimateTokens(messageText); try { @@ -305,10 +322,16 @@ async function handlePOST(req: NextRequest) { // for actual response generation on top of the thinking budget. // Default minimum output tokens to allow for actual response generation (consistent with MCP endpoint) const DEFAULT_MIN_OUTPUT_TOKENS = 4096; - const cotBudget = resolveAnthropicThinkingBudgetTokens(selectedModel, process.env); + const cotBudget = resolveAnthropicThinkingBudgetTokens( + selectedModel, + process.env, + ); const effectiveMaxOutputTokens = cotBudget != null - ? Math.max(DEFAULT_MIN_OUTPUT_TOKENS, cotBudget + DEFAULT_MIN_OUTPUT_TOKENS) + ? Math.max( + DEFAULT_MIN_OUTPUT_TOKENS, + cotBudget + DEFAULT_MIN_OUTPUT_TOKENS, + ) : undefined; const result = streamText({ @@ -318,8 +341,14 @@ async function handlePOST(req: NextRequest) { messages: await convertToModelMessages(messages), abortSignal: req.signal, timeout: routeTimeoutMs, - ...(effectiveMaxOutputTokens != null ? { maxOutputTokens: effectiveMaxOutputTokens } : {}), - ...mergeAnthropicCotProviderOptions(selectedModel, process.env, cotBudget ?? undefined), + ...(effectiveMaxOutputTokens != null + ? { maxOutputTokens: effectiveMaxOutputTokens } + : {}), + ...mergeAnthropicCotProviderOptions( + selectedModel, + process.env, + cotBudget ?? undefined, + ), onFinish: async ({ text, usage }) => { try { if (!usage) { @@ -329,12 +358,18 @@ async function handlePOST(req: NextRequest) { // Increment message count AFTER successful completion (for anonymous users) if (isAnonymous && anonymousSession) { - await anonymousSessionsService.incrementMessageCount(anonymousSession.id); + await anonymousSessionsService.incrementMessageCount( + anonymousSession.id, + ); - logger.info("chat-api", "Incremented anonymous message count after success", { - sessionId: anonymousSession.id, - newCount: anonymousSession.message_count + 1, - }); + logger.info( + "chat-api", + "Incremented anonymous message count after success", + { + sessionId: anonymousSession.id, + newCount: anonymousSession.message_count + 1, + }, + ); } const userMessage = messages[messages.length - 1]; @@ -411,7 +446,9 @@ async function handlePOST(req: NextRequest) { if (apiKey) { const lastMessageParts = messages[messages.length - 1]?.parts; - const userPrompt = lastMessageParts ? extractTextFromParts(lastMessageParts) : ""; + const userPrompt = lastMessageParts + ? extractTextFromParts(lastMessageParts) + : ""; await generationsService.create({ organization_id: user.organization_id, user_id: user.id, @@ -431,7 +468,8 @@ async function handlePOST(req: NextRequest) { text: text, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, - totalTokens: (usage.inputTokens || 0) + (usage.outputTokens || 0), + totalTokens: + (usage.inputTokens || 0) + (usage.outputTokens || 0), }, }); } @@ -444,9 +482,13 @@ async function handlePOST(req: NextRequest) { }); } catch (error) { await settleReservation?.(0); - logger.error("chat-api", "Error persisting messages or deducting credits", { - error: error instanceof Error ? error.message : "Unknown error", - }); + logger.error( + "chat-api", + "Error persisting messages or deducting credits", + { + error: error instanceof Error ? error.message : "Unknown error", + }, + ); if (usage && user.organization_id) { try { @@ -462,12 +504,15 @@ async function handlePOST(req: NextRequest) { input_cost: String(0), output_cost: String(0), is_successful: false, - error_message: error instanceof Error ? error.message : "Unknown error", + error_message: + error instanceof Error ? error.message : "Unknown error", }); if (apiKey) { const lastMessageParts = messages[messages.length - 1]?.parts; - const userPrompt = lastMessageParts ? extractTextFromParts(lastMessageParts) : ""; + const userPrompt = lastMessageParts + ? extractTextFromParts(lastMessageParts) + : ""; await generationsService.create({ organization_id: user.organization_id, user_id: user.id, @@ -477,14 +522,18 @@ async function handlePOST(req: NextRequest) { provider: provider, prompt: userPrompt, status: "failed", - error: error instanceof Error ? error.message : "Unknown error", + error: + error instanceof Error ? error.message : "Unknown error", usage_record_id: errorUsageRecord.id, completed_at: new Date(), }); } } catch (usageError) { logger.error("chat-api", "Error creating usage record", { - error: usageError instanceof Error ? usageError.message : "Unknown error", + error: + usageError instanceof Error + ? usageError.message + : "Unknown error", }); } } @@ -508,7 +557,10 @@ async function handlePOST(req: NextRequest) { const status = getErrorStatusCode(error); return new Response( JSON.stringify({ - error: status === 500 ? "Failed to process chat" : getSafeErrorMessage(error), + error: + status === 500 + ? "Failed to process chat" + : getSafeErrorMessage(error), }), { status, diff --git a/app/api/v1/connections/[platform]/route.ts b/app/api/v1/connections/[platform]/route.ts index c3bd3cc38..ffecbdce4 100644 --- a/app/api/v1/connections/[platform]/route.ts +++ b/app/api/v1/connections/[platform]/route.ts @@ -18,7 +18,9 @@ type RouteParams = Promise<{ platform: string; }>; -async function resolvePlatform(params: RouteParams): Promise<"twilio" | "blooio" | null> { +async function resolvePlatform( + params: RouteParams, +): Promise<"twilio" | "blooio" | null> { const { platform } = await params; const normalized = platform.toLowerCase(); @@ -36,7 +38,10 @@ export async function POST( const platform = await resolvePlatform(context.params); if (!platform) { - return NextResponse.json({ error: "Unsupported platform" }, { status: 404 }); + return NextResponse.json( + { error: "Unsupported platform" }, + { status: 404 }, + ); } if (platform === "twilio") { @@ -53,7 +58,10 @@ export async function DELETE( const platform = await resolvePlatform(context.params); if (!platform) { - return NextResponse.json({ error: "Unsupported platform" }, { status: 404 }); + return NextResponse.json( + { error: "Unsupported platform" }, + { status: 404 }, + ); } if (platform === "twilio") { @@ -71,7 +79,10 @@ export async function PATCH( const platform = await resolvePlatform(context.params); if (!platform) { - return NextResponse.json({ error: "Unsupported platform" }, { status: 404 }); + return NextResponse.json( + { error: "Unsupported platform" }, + { status: 404 }, + ); } if (platform === "twilio") { diff --git a/app/api/v1/containers/[id]/deployments/route.ts b/app/api/v1/containers/[id]/deployments/route.ts index 92925b819..a32bbf5a3 100644 --- a/app/api/v1/containers/[id]/deployments/route.ts +++ b/app/api/v1/containers/[id]/deployments/route.ts @@ -14,13 +14,19 @@ export const dynamic = "force-dynamic"; * @param params - Route parameters containing the container ID. * @returns Deployment history with status, costs, and metadata. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { id } = await params; const { user } = await requireAuthOrApiKeyWithOrg(request); // Verify container belongs to user's organization - const container = await containersService.getById(id, user.organization_id!); + const container = await containersService.getById( + id, + user.organization_id!, + ); if (!container) { return NextResponse.json( @@ -36,10 +42,15 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ // Note: This needs a custom query since we're filtering by type and metadata // For now, we'll use the repository's list method and filter in memory // A better approach would be to add a specific repository method for this - const allRecords = await usageRecordsRepository.listByOrganization(user.organization_id!, 50); + const allRecords = await usageRecordsRepository.listByOrganization( + user.organization_id!, + 50, + ); // Filter for container deployments - const deployments = allRecords.filter((record) => record.type === "container_deployment"); + const deployments = allRecords.filter( + (record) => record.type === "container_deployment", + ); // Filter for this specific container interface DeploymentMetadata { @@ -53,7 +64,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const containerDeployments = deployments.filter((d) => { const metadata = (d.metadata as DeploymentMetadata | null) ?? {}; - return metadata.container_id === id || metadata.container_name === container.name; + return ( + metadata.container_id === id || + metadata.container_name === container.name + ); }); // Enhance with container status snapshots @@ -112,7 +126,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch deployment history", + error: + error instanceof Error + ? error.message + : "Failed to fetch deployment history", }, { status: 500 }, ); diff --git a/app/api/v1/containers/[id]/health/route.ts b/app/api/v1/containers/[id]/health/route.ts index 1ca8cb3d7..3d67833b7 100644 --- a/app/api/v1/containers/[id]/health/route.ts +++ b/app/api/v1/containers/[id]/health/route.ts @@ -14,13 +14,19 @@ export const dynamic = "force-dynamic"; * @param params - Route parameters containing the container ID. * @returns Health status including response time, status code, and error information. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: containerId } = await params; // Verify container belongs to user's organization - const container = await containersRepository.findById(containerId, user.organization_id!); + const container = await containersRepository.findById( + containerId, + user.organization_id!, + ); if (!container) { return NextResponse.json( @@ -39,7 +45,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: "Unable to perform health check - container may not have a URL", + error: + "Unable to perform health check - container may not have a URL", }, { status: 400 }, ); @@ -63,7 +70,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to check container health", + error: + error instanceof Error + ? error.message + : "Failed to check container health", }, { status: 500 }, ); diff --git a/app/api/v1/containers/[id]/metrics/route.ts b/app/api/v1/containers/[id]/metrics/route.ts index d697a643f..ccd8b4233 100644 --- a/app/api/v1/containers/[id]/metrics/route.ts +++ b/app/api/v1/containers/[id]/metrics/route.ts @@ -3,7 +3,10 @@ * Fetches CloudWatch metrics for ECS containers */ -import { CloudWatchClient, GetMetricStatisticsCommand } from "@aws-sdk/client-cloudwatch"; +import { + CloudWatchClient, + GetMetricStatisticsCommand, +} from "@aws-sdk/client-cloudwatch"; import { NextRequest, NextResponse } from "next/server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { getContainer } from "@/lib/services/containers"; @@ -29,7 +32,10 @@ interface ContainerMetrics { * @param params - Route parameters containing the container ID. * @returns Container metrics with utilization data. */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { id } = await params; const { user } = await requireAuthOrApiKeyWithOrg(request); @@ -92,7 +98,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch container metrics", + error: + error instanceof Error + ? error.message + : "Failed to fetch container metrics", }, { status: 500 }, ); @@ -137,21 +146,57 @@ async function getContainerMetrics( const serviceName = container.ecs_service_arn.split("/").pop() || ""; // Fetch multiple metrics in parallel - const [cpuData, memoryData, networkRxData, networkTxData] = await Promise.allSettled([ - fetchMetric(client, "CPUUtilization", clusterName, serviceName, startTime, now), - fetchMetric(client, "MemoryUtilization", clusterName, serviceName, startTime, now), - fetchMetric(client, "NetworkRxBytes", clusterName, serviceName, startTime, now), - fetchMetric(client, "NetworkTxBytes", clusterName, serviceName, startTime, now), - ]); + const [cpuData, memoryData, networkRxData, networkTxData] = + await Promise.allSettled([ + fetchMetric( + client, + "CPUUtilization", + clusterName, + serviceName, + startTime, + now, + ), + fetchMetric( + client, + "MemoryUtilization", + clusterName, + serviceName, + startTime, + now, + ), + fetchMetric( + client, + "NetworkRxBytes", + clusterName, + serviceName, + startTime, + now, + ), + fetchMetric( + client, + "NetworkTxBytes", + clusterName, + serviceName, + startTime, + now, + ), + ]); // Extract values with fallbacks - const cpu_utilization = cpuData.status === "fulfilled" && cpuData.value ? cpuData.value : 0; + const cpu_utilization = + cpuData.status === "fulfilled" && cpuData.value ? cpuData.value : 0; const memory_utilization = - memoryData.status === "fulfilled" && memoryData.value ? memoryData.value : 0; + memoryData.status === "fulfilled" && memoryData.value + ? memoryData.value + : 0; const network_rx_bytes = - networkRxData.status === "fulfilled" && networkRxData.value ? networkRxData.value : 0; + networkRxData.status === "fulfilled" && networkRxData.value + ? networkRxData.value + : 0; const network_tx_bytes = - networkTxData.status === "fulfilled" && networkTxData.value ? networkTxData.value : 0; + networkTxData.status === "fulfilled" && networkTxData.value + ? networkTxData.value + : 0; return { cpu_utilization, @@ -206,7 +251,10 @@ async function fetchMetric( // Get the latest datapoint const latest = datapoints.reduce((prev, current) => { - return (current.Timestamp || new Date(0)) > (prev.Timestamp || new Date(0)) ? current : prev; + return (current.Timestamp || new Date(0)) > + (prev.Timestamp || new Date(0)) + ? current + : prev; }); return latest.Average || 0; diff --git a/app/api/v1/containers/[id]/route.ts b/app/api/v1/containers/[id]/route.ts index 0b3f97ece..c35119551 100644 --- a/app/api/v1/containers/[id]/route.ts +++ b/app/api/v1/containers/[id]/route.ts @@ -25,7 +25,10 @@ export const dynamic = "force-dynamic"; * GET /api/v1/containers/[id] * Get container details */ -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: containerId } = await params; @@ -51,7 +54,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch container", + error: + error instanceof Error ? error.message : "Failed to fetch container", }, { status: 500 }, ); @@ -68,7 +72,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ * - Refunds remaining credits (prorated) * - Cleans up database records */ -async function handleDELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { +async function handleDELETE( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { if (!context) { return NextResponse.json( { success: false, error: "Missing route parameters" }, @@ -135,7 +142,10 @@ async function handleDELETE(request: NextRequest, context: { params: Promise<{ i // Step 3: Release ALB priority try { - await dbPriorityManager.releasePriority(container.organization_id, container.project_name); + await dbPriorityManager.releasePriority( + container.organization_id, + container.project_name, + ); } catch (priorityError) { logger.error(`Failed to release ALB priority:`, priorityError); // Non-critical - continue with cleanup @@ -213,7 +223,8 @@ async function handleDELETE(request: NextRequest, context: { params: Promise<{ i return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to delete container", + error: + error instanceof Error ? error.message : "Failed to delete container", }, { status: 500 }, ); @@ -234,7 +245,10 @@ export const DELETE = withRateLimit(handleDELETE, RateLimitPresets.STANDARD); * * Note: Updates trigger a CloudFormation stack update which takes 5-10 minutes */ -export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id: containerId } = await params; @@ -337,7 +351,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json( { success: false, - error: "No valid updates provided. Updatable fields: cpu, memory, port, ecr_image_uri", + error: + "No valid updates provided. Updatable fields: cpu, memory, port, ecr_image_uri", }, { status: 400 }, ); @@ -355,7 +370,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< userId: container.organization_id, projectName: container.project_name, userEmail: container.name, - containerImage: updates.containerImage || (container.metadata?.ecr_image_uri as string), + containerImage: + updates.containerImage || + (container.metadata?.ecr_image_uri as string), containerPort: updates.containerPort || container.port, containerCpu: updates.containerCpu || container.cpu, containerMemory: updates.containerMemory || container.memory, @@ -381,7 +398,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< if (updates.containerCpu) dbUpdates.cpu = updates.containerCpu; if (updates.containerMemory) dbUpdates.memory = updates.containerMemory; if (updates.containerPort) dbUpdates.port = updates.containerPort; - if (updates.containerImage) dbUpdates.ecr_image_uri = updates.containerImage; + if (updates.containerImage) + dbUpdates.ecr_image_uri = updates.containerImage; // Update container in database const updatedContainer = await containersService.update( @@ -409,14 +427,18 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< // Mark as failed await updateContainerStatus(containerId, "failed", { - errorMessage: cfError instanceof Error ? cfError.message : "Update failed", + errorMessage: + cfError instanceof Error ? cfError.message : "Update failed", deploymentLog: `CloudFormation update failed: ${cfError instanceof Error ? cfError.message : "Unknown error"}`, }); return NextResponse.json( { success: false, - error: cfError instanceof Error ? cfError.message : "CloudFormation update failed", + error: + cfError instanceof Error + ? cfError.message + : "CloudFormation update failed", }, { status: 500 }, ); @@ -426,7 +448,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to update container", + error: + error instanceof Error ? error.message : "Failed to update container", }, { status: 500 }, ); diff --git a/app/api/v1/containers/credentials/route.ts b/app/api/v1/containers/credentials/route.ts index 7370b3385..962044b3b 100644 --- a/app/api/v1/containers/credentials/route.ts +++ b/app/api/v1/containers/credentials/route.ts @@ -37,7 +37,8 @@ async function handleECRCredentials(request: NextRequest) { const ecrManager = getECRManager(); // Generate ECR repository name - const repositoryName = `elizaos/${user.organization_id}/${projectId}`.toLowerCase(); + const repositoryName = + `elizaos/${user.organization_id}/${projectId}`.toLowerCase(); // Create or get ECR repository const repository = await ecrManager.createRepository(repositoryName); @@ -66,7 +67,10 @@ async function handleECRCredentials(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to get ECR credentials", + error: + error instanceof Error + ? error.message + : "Failed to get ECR credentials", }, { status: 500 }, ); @@ -74,4 +78,7 @@ async function handleECRCredentials(request: NextRequest) { } // Export rate-limited handler -export const POST = withRateLimit(handleECRCredentials, RateLimitPresets.STRICT); +export const POST = withRateLimit( + handleECRCredentials, + RateLimitPresets.STRICT, +); diff --git a/app/api/v1/containers/quota/route.ts b/app/api/v1/containers/quota/route.ts index 9de627333..248258dd1 100644 --- a/app/api/v1/containers/quota/route.ts +++ b/app/api/v1/containers/quota/route.ts @@ -49,7 +49,9 @@ export async function GET(request: NextRequest) { }); // Calculate current daily costs for all running containers - const runningContainers = existingContainers.filter((c) => c.status === "running"); + const runningContainers = existingContainers.filter( + (c) => c.status === "running", + ); const currentDailyBurn = runningContainers.reduce((total, container) => { return ( total + @@ -64,7 +66,9 @@ export async function GET(request: NextRequest) { // Calculate days of runway const currentBalance = Number(user.organization.credit_balance); const daysOfRunway = - currentDailyBurn > 0 ? Math.floor(currentBalance / currentDailyBurn) : Infinity; + currentDailyBurn > 0 + ? Math.floor(currentBalance / currentDailyBurn) + : Infinity; return NextResponse.json({ success: true, @@ -100,7 +104,8 @@ export async function GET(request: NextRequest) { }, limits: { maxImageSize: CONTAINER_LIMITS.MAX_IMAGE_SIZE_BYTES, - maxInstancesPerContainer: CONTAINER_LIMITS.MAX_INSTANCES_PER_CONTAINER, + maxInstancesPerContainer: + CONTAINER_LIMITS.MAX_INSTANCES_PER_CONTAINER, maxEnvVars: CONTAINER_LIMITS.MAX_ENV_VARS, }, }, diff --git a/app/api/v1/containers/route.ts b/app/api/v1/containers/route.ts index 3c241749f..aafd91c0a 100644 --- a/app/api/v1/containers/route.ts +++ b/app/api/v1/containers/route.ts @@ -4,7 +4,10 @@ import { sanitizeErrorMessage } from "@/lib/analytics/posthog"; import { trackServerEvent } from "@/lib/analytics/posthog-server"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { isFeatureConfigured } from "@/lib/config/env-validator"; -import { CONTAINER_LIMITS, calculateDeploymentCost } from "@/lib/constants/pricing"; +import { + CONTAINER_LIMITS, + calculateDeploymentCost, +} from "@/lib/constants/pricing"; import { creditEventEmitter } from "@/lib/events/credit-events"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { QuotaExceededError } from "@/lib/services/container-quota"; @@ -72,7 +75,8 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to fetch containers", + error: + error instanceof Error ? error.message : "Failed to fetch containers", }, { status: 500 }, ); @@ -125,7 +129,9 @@ async function handleCreateContainer(request: NextRequest) { } // Validate each env var name and value size - for (const [key, value] of Object.entries(validatedData.environment_vars)) { + for (const [key, value] of Object.entries( + validatedData.environment_vars, + )) { // Validate key format if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { return NextResponse.json( @@ -169,7 +175,9 @@ async function handleCreateContainer(request: NextRequest) { try { const { getECRManager } = await import("@/lib/services/ecr"); const ecrManager = getECRManager(); - const imageExists = await ecrManager.verifyImageExists(validatedData.ecr_image_uri); + const imageExists = await ecrManager.verifyImageExists( + validatedData.ecr_image_uri, + ); if (!imageExists) { return NextResponse.json( @@ -186,13 +194,16 @@ async function handleCreateContainer(request: NextRequest) { } catch (error) { logger.error("Failed to verify ECR image:", error); // Log but don't block deployment - image might exist but verification failed - logger.warn("Proceeding with deployment despite image verification failure"); + logger.warn( + "Proceeding with deployment despite image verification failure", + ); } // Check if a container with this project_name already exists for this user const existingContainers = await listContainers(user.organization_id!); const existingProject = existingContainers.find( - (c) => c.user_id === user.id && c.project_name === validatedData.project_name, + (c) => + c.user_id === user.id && c.project_name === validatedData.project_name, ); const isUpdate = !!existingProject; @@ -445,10 +456,13 @@ async function handleCreateContainer(request: NextRequest) { stackName, }) .catch((err) => { - logger.warn("[CONTAINER DEPLOYMENT] Failed to send Discord notification", { - containerId: container.id, - error: err instanceof Error ? err.message : "Unknown error", - }); + logger.warn( + "[CONTAINER DEPLOYMENT] Failed to send Discord notification", + { + containerId: container.id, + error: err instanceof Error ? err.message : "Unknown error", + }, + ); }); // Track container deployment started in PostHog using internal UUID @@ -481,9 +495,14 @@ async function handleCreateContainer(request: NextRequest) { ); } catch (stackError) { const errorMessage = - stackError instanceof Error ? stackError.message : "CloudFormation stack creation failed"; + stackError instanceof Error + ? stackError.message + : "CloudFormation stack creation failed"; - logger.error(`❌ [handleCreateContainer] CloudFormation stack creation failed:`, stackError); + logger.error( + `❌ [handleCreateContainer] CloudFormation stack creation failed:`, + stackError, + ); // Update container status to failed await updateContainerStatus(container.id, "failed", { @@ -492,7 +511,11 @@ async function handleCreateContainer(request: NextRequest) { // Mark usage record as failed with error message try { - await usageService.markDeploymentFailed(container.id, user.organization_id!, errorMessage); + await usageService.markDeploymentFailed( + container.id, + user.organization_id!, + errorMessage, + ); } catch (usageError) { logger.error(`❌ Failed to update usage record:`, usageError); } @@ -562,12 +585,14 @@ async function handleCreateContainer(request: NextRequest) { // Handle duplicate container name errors (from unique constraint) if ( error instanceof Error && - (error.message.includes("unique constraint") || error.message.includes("duplicate key")) + (error.message.includes("unique constraint") || + error.message.includes("duplicate key")) ) { return NextResponse.json( { success: false, - error: "A container with this name already exists in your organization", + error: + "A container with this name already exists in your organization", }, { status: 409 }, ); @@ -577,7 +602,8 @@ async function handleCreateContainer(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to create container", + error: + error instanceof Error ? error.message : "Failed to create container", }, { status: 500 }, ); @@ -585,7 +611,10 @@ async function handleCreateContainer(request: NextRequest) { } // Export rate-limited handler for POST -export const POST = withRateLimit(handleCreateContainer, RateLimitPresets.CRITICAL); +export const POST = withRateLimit( + handleCreateContainer, + RateLimitPresets.CRITICAL, +); /** * Initiates CloudFormation stack creation SYNCHRONOUSLY @@ -597,7 +626,9 @@ async function initiateCloudFormationStack( config: z.infer, organizationId: string, ): Promise { - const { cloudFormationService } = await import("@/lib/services/cloudformation"); + const { cloudFormationService } = await import( + "@/lib/services/cloudformation" + ); // Update status to building await updateContainerStatus(containerId, "building", { @@ -605,7 +636,8 @@ async function initiateCloudFormationStack( }); // Check if shared infrastructure is deployed - const sharedInfraExists = await cloudFormationService.isSharedInfrastructureDeployed(); + const sharedInfraExists = + await cloudFormationService.isSharedInfrastructureDeployed(); if (!sharedInfraExists) { throw new Error( @@ -665,7 +697,10 @@ async function initiateCloudFormationStack( } } catch (error) { // If we can't check, assume stack doesn't exist and create new - logger.warn(`Failed to check if stack exists for ${containerId}, will create new:`, error); + logger.warn( + `Failed to check if stack exists for ${containerId}, will create new:`, + error, + ); stackActuallyExists = false; } } @@ -680,7 +715,10 @@ async function initiateCloudFormationStack( } // Get the stack name for storage - const stackName = cloudFormationService.getStackName(organizationId, config.project_name); + const stackName = cloudFormationService.getStackName( + organizationId, + config.project_name, + ); // Update container with stack name await updateContainerStatus(containerId, "deploying", { diff --git a/app/api/v1/credits/balance/route.ts b/app/api/v1/credits/balance/route.ts index 39d6a7e3d..8aa079987 100644 --- a/app/api/v1/credits/balance/route.ts +++ b/app/api/v1/credits/balance/route.ts @@ -8,7 +8,10 @@ */ import { type NextRequest, NextResponse } from "next/server"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { organizationsService } from "@/lib/services/organizations"; import { logger } from "@/lib/utils/logger"; diff --git a/app/api/v1/credits/checkout/route.ts b/app/api/v1/credits/checkout/route.ts index 15928c980..2e274b368 100644 --- a/app/api/v1/credits/checkout/route.ts +++ b/app/api/v1/credits/checkout/route.ts @@ -10,7 +10,10 @@ import { type NextRequest, NextResponse } from "next/server"; import type Stripe from "stripe"; import { z } from "zod"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { assertAllowedAbsoluteRedirectUrl, @@ -195,12 +198,18 @@ export async function POST(request: NextRequest) { ); } catch (error) { const errorMessage = - error instanceof Error ? error.message : "Failed to create checkout session"; + error instanceof Error + ? error.message + : "Failed to create checkout session"; const isValidationError = - errorMessage.includes("Invalid success_url") || errorMessage.includes("Invalid cancel_url"); + errorMessage.includes("Invalid success_url") || + errorMessage.includes("Invalid cancel_url"); if (isValidationError) { - return NextResponse.json({ error: errorMessage }, { status: 400, headers: corsHeaders }); + return NextResponse.json( + { error: errorMessage }, + { status: 400, headers: corsHeaders }, + ); } if (getErrorStatusCode(error) >= 500) { diff --git a/app/api/v1/credits/summary/route.ts b/app/api/v1/credits/summary/route.ts index f8cda6f1a..64398ed09 100644 --- a/app/api/v1/credits/summary/route.ts +++ b/app/api/v1/credits/summary/route.ts @@ -40,7 +40,9 @@ async function getSummaryHandler(request: NextRequest): Promise { }); // Get budgets for all agents - const agentBudgets = await agentBudgetService.getOrgBudgets(user.organization_id); + const agentBudgets = await agentBudgetService.getOrgBudgets( + user.organization_id, + ); const budgetMap = new Map(agentBudgets.map((b) => [b.agent_id, b])); // Get all apps owned by this org @@ -52,10 +54,11 @@ async function getSummaryHandler(request: NextRequest): Promise { const earnings = await redeemableEarningsService.getBalance(user.id); // Get recent transactions - const recentTransactions = await creditsService.listTransactionsByOrganization( - user.organization_id, - 10, - ); + const recentTransactions = + await creditsService.listTransactionsByOrganization( + user.organization_id, + 10, + ); // Build response const response = { @@ -69,8 +72,12 @@ async function getSummaryHandler(request: NextRequest): Promise { // Auto top-up settings autoTopUpEnabled: org.auto_top_up_enabled, - autoTopUpThreshold: org.auto_top_up_threshold ? Number(org.auto_top_up_threshold) : null, - autoTopUpAmount: org.auto_top_up_amount ? Number(org.auto_top_up_amount) : null, + autoTopUpThreshold: org.auto_top_up_threshold + ? Number(org.auto_top_up_threshold) + : null, + autoTopUpAmount: org.auto_top_up_amount + ? Number(org.auto_top_up_amount) + : null, hasPaymentMethod: !!org.stripe_default_payment_method, }, @@ -80,7 +87,9 @@ async function getSummaryHandler(request: NextRequest): Promise { const allocated = budget ? Number(budget.allocated_budget) : 0; const spent = budget ? Number(budget.spent_budget) : 0; const available = allocated - spent; - const dailyLimit = budget?.daily_limit ? Number(budget.daily_limit) : null; + const dailyLimit = budget?.daily_limit + ? Number(budget.daily_limit) + : null; const dailySpent = budget ? Number(budget.daily_spent) : 0; return { @@ -115,8 +124,14 @@ async function getSummaryHandler(request: NextRequest): Promise { total: agents.length, withBudget: agentBudgets.length, paused: agentBudgets.filter((b) => b.is_paused).length, - totalAllocated: agentBudgets.reduce((sum, b) => sum + Number(b.allocated_budget), 0), - totalSpent: agentBudgets.reduce((sum, b) => sum + Number(b.spent_budget), 0), + totalAllocated: agentBudgets.reduce( + (sum, b) => sum + Number(b.allocated_budget), + 0, + ), + totalSpent: agentBudgets.reduce( + (sum, b) => sum + Number(b.spent_budget), + 0, + ), totalAvailable: agentBudgets.reduce( (sum, b) => sum + (Number(b.allocated_budget) - Number(b.spent_budget)), 0, @@ -187,7 +202,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/credits/verify/route.ts b/app/api/v1/credits/verify/route.ts index e22df6128..fa8e01117 100644 --- a/app/api/v1/credits/verify/route.ts +++ b/app/api/v1/credits/verify/route.ts @@ -8,7 +8,10 @@ */ import { type NextRequest, NextResponse } from "next/server"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { requireStripe } from "@/lib/stripe"; import { logger } from "@/lib/utils/logger"; diff --git a/app/api/v1/cron/deployment-monitor/route.ts b/app/api/v1/cron/deployment-monitor/route.ts index 1b9b0fdba..5db4021a4 100644 --- a/app/api/v1/cron/deployment-monitor/route.ts +++ b/app/api/v1/cron/deployment-monitor/route.ts @@ -47,7 +47,9 @@ async function handleDeploymentMonitor(request: NextRequest) { }); } - logger.info(`[Deployment Monitor] Checking ${deployingContainers.length} containers`); + logger.info( + `[Deployment Monitor] Checking ${deployingContainers.length} containers`, + ); // Deployment timeout: 30 minutes is more than enough for CloudFormation (typically 8-12 min) const DEPLOYMENT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes @@ -257,7 +259,10 @@ async function handleDeploymentMonitor(request: NextRequest) { `[Deployment Monitor] Container ${container.id}: Stack ${stackName} is ${stackStatus.status}`, ); - if (stackStatus.status === "CREATE_COMPLETE" || stackStatus.status === "UPDATE_COMPLETE") { + if ( + stackStatus.status === "CREATE_COMPLETE" || + stackStatus.status === "UPDATE_COMPLETE" + ) { // Stack completed successfully! const outputs = await cloudFormationService.getStackOutputs( container.organization_id, @@ -303,12 +308,16 @@ async function handleDeploymentMonitor(request: NextRequest) { const deploymentDuration = container.created_at ? Date.now() - new Date(container.created_at).getTime() : undefined; - trackServerEvent(container.user_id, "container_deploy_completed", { - container_id: container.id, - container_name: container.name, - deployment_time_ms: deploymentDuration, - container_url: outputs.containerUrl, - }); + trackServerEvent( + container.user_id, + "container_deploy_completed", + { + container_id: container.id, + container_name: container.name, + deployment_time_ms: deploymentDuration, + container_url: outputs.containerUrl, + }, + ); } results.push({ @@ -326,7 +335,10 @@ async function handleDeploymentMonitor(request: NextRequest) { // Still mark usage record as successful since stack completed try { - await usageService.markDeploymentSuccessful(container.id, container.organization_id); + await usageService.markDeploymentSuccessful( + container.id, + container.organization_id, + ); } catch (usageError) { logger.error( `[Deployment Monitor] ❌ Failed to update usage record for container ${container.id}:`, @@ -350,7 +362,8 @@ async function handleDeploymentMonitor(request: NextRequest) { stackStatus.status === "UPDATE_ROLLBACK_COMPLETE" ) { // Stack failed - const failureReason = stackStatus.statusReason || "Stack creation failed"; + const failureReason = + stackStatus.statusReason || "Stack creation failed"; await updateContainerStatus(container.id, "failed", { errorMessage: failureReason, @@ -411,9 +424,14 @@ async function handleDeploymentMonitor(request: NextRequest) { container.organization_id, container.project_name, ); - logger.info(`[Deployment Monitor] Initiated cleanup of failed stack ${stackName}`); + logger.info( + `[Deployment Monitor] Initiated cleanup of failed stack ${stackName}`, + ); } catch (cleanupError) { - logger.warn(`[Deployment Monitor] Failed to cleanup stack ${stackName}:`, cleanupError); + logger.warn( + `[Deployment Monitor] Failed to cleanup stack ${stackName}:`, + cleanupError, + ); } results.push({ @@ -445,7 +463,10 @@ async function handleDeploymentMonitor(request: NextRequest) { stackName: container.cloudformation_stack_name, previousStatus: container.status, newStatus: null, - error: containerError instanceof Error ? containerError.message : "Unknown error", + error: + containerError instanceof Error + ? containerError.message + : "Unknown error", }); } } @@ -474,7 +495,8 @@ async function handleDeploymentMonitor(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Deployment monitor failed", + error: + error instanceof Error ? error.message : "Deployment monitor failed", }, { status: 500 }, ); diff --git a/app/api/v1/cron/health-check/route.ts b/app/api/v1/cron/health-check/route.ts index 9abcc2b1b..725b8f53e 100644 --- a/app/api/v1/cron/health-check/route.ts +++ b/app/api/v1/cron/health-check/route.ts @@ -28,7 +28,9 @@ async function handleHealthCheck(request: NextRequest) { const authError = verifyCronSecret(request, "[Health Check Cron]"); if (authError) return authError; - logger.info("[Health Check Cron] Starting scheduled container health check"); + logger.info( + "[Health Check Cron] Starting scheduled container health check", + ); const results = await monitorAllContainers({ checkIntervalMs: 60000, diff --git a/app/api/v1/cron/process-provisioning-jobs/route.ts b/app/api/v1/cron/process-provisioning-jobs/route.ts index b1aab752b..d8c629476 100644 --- a/app/api/v1/cron/process-provisioning-jobs/route.ts +++ b/app/api/v1/cron/process-provisioning-jobs/route.ts @@ -11,7 +11,9 @@ function verifyCronSecret(request: NextRequest): boolean { const cronSecret = process.env.CRON_SECRET; if (!cronSecret) { - logger.error("[Provisioning Jobs] CRON_SECRET not configured - rejecting request for security"); + logger.error( + "[Provisioning Jobs] CRON_SECRET not configured - rejecting request for security", + ); return false; } @@ -60,7 +62,10 @@ async function handleProcessProvisioningJobs(request: NextRequest) { ip: request.headers.get("x-forwarded-for"), timestamp: new Date().toISOString(), }); - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); } logger.info("[Provisioning Jobs] Starting job processing cycle"); @@ -91,7 +96,10 @@ async function handleProcessProvisioningJobs(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Provisioning job processing failed", + error: + error instanceof Error + ? error.message + : "Provisioning job processing failed", }, { status: 500 }, ); diff --git a/app/api/v1/cron/refresh-model-catalog/route.ts b/app/api/v1/cron/refresh-model-catalog/route.ts index 7ab36e5d6..e0fe5c43d 100644 --- a/app/api/v1/cron/refresh-model-catalog/route.ts +++ b/app/api/v1/cron/refresh-model-catalog/route.ts @@ -39,7 +39,10 @@ async function handleRefresh(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Model catalog refresh failed", + error: + error instanceof Error + ? error.message + : "Model catalog refresh failed", }, { status: 500 }, ); diff --git a/app/api/v1/cron/refresh-pricing/route.ts b/app/api/v1/cron/refresh-pricing/route.ts index b50809ff5..48b152fff 100644 --- a/app/api/v1/cron/refresh-pricing/route.ts +++ b/app/api/v1/cron/refresh-pricing/route.ts @@ -31,7 +31,8 @@ async function handleRefresh(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Pricing refresh failed", + error: + error instanceof Error ? error.message : "Pricing refresh failed", }, { status: 500 }, ); diff --git a/app/api/v1/dashboard/route.ts b/app/api/v1/dashboard/route.ts index ea733c6a8..dd333b497 100644 --- a/app/api/v1/dashboard/route.ts +++ b/app/api/v1/dashboard/route.ts @@ -29,20 +29,32 @@ export async function GET(request: NextRequest) { // PERFORMANCE: Parallelized all data fetches including 24h usage stats const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); - const [generationStats, userCharacters, containers, apiKeys, userRooms, usageStats] = - await Promise.all([ - generationsService.getStats(organizationId), - charactersService.listByUser(user.id), - listContainers(organizationId), - apiKeysService.listByOrganization(organizationId), - roomsService.getRoomsForEntity(user.id), - usageService.getStatsByOrganization(organizationId, twentyFourHoursAgo, new Date()), - ]); + const [ + generationStats, + userCharacters, + containers, + apiKeys, + userRooms, + usageStats, + ] = await Promise.all([ + generationsService.getStats(organizationId), + charactersService.listByUser(user.id), + listContainers(organizationId), + apiKeysService.listByOrganization(organizationId), + roomsService.getRoomsForEntity(user.id), + usageService.getStatsByOrganization( + organizationId, + twentyFourHoursAgo, + new Date(), + ), + ]); const chatRoomCount = userRooms.length; const totalGenerations = generationStats.totalGenerations; - const imageGenerations = generationStats.byType.find((t) => t.type === "image")?.count || 0; - const videoGenerations = generationStats.byType.find((t) => t.type === "video")?.count || 0; + const imageGenerations = + generationStats.byType.find((t) => t.type === "image")?.count || 0; + const videoGenerations = + generationStats.byType.find((t) => t.type === "video")?.count || 0; const apiCalls24h = usageStats.totalRequests; // Fetch agent stats in batch @@ -60,7 +72,9 @@ export async function GET(request: NextRequest) { if (characterIds.length > 0) { try { const statsMap = - await characterDeploymentDiscoveryService.getCharacterStatisticsBatch(characterIds); + await characterDeploymentDiscoveryService.getCharacterStatisticsBatch( + characterIds, + ); statsMap.forEach((stats, id) => { agentStatsMap.set(id, { roomCount: stats.roomCount, diff --git a/app/api/v1/device-bus/devices/[deviceId]/intents/route.ts b/app/api/v1/device-bus/devices/[deviceId]/intents/route.ts index 72bb72615..fd496e2e5 100644 --- a/app/api/v1/device-bus/devices/[deviceId]/intents/route.ts +++ b/app/api/v1/device-bus/devices/[deviceId]/intents/route.ts @@ -40,16 +40,26 @@ export async function GET( const url = new URL(request.url); const sinceParam = url.searchParams.get("since"); - const since = sinceParam ? new Date(sinceParam) : new Date(Date.now() - DEFAULT_LOOKBACK_MS); + const since = sinceParam + ? new Date(sinceParam) + : new Date(Date.now() - DEFAULT_LOOKBACK_MS); if (Number.isNaN(since.getTime())) { - return NextResponse.json({ error: "Invalid 'since' parameter" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid 'since' parameter" }, + { status: 400 }, + ); } const rows = await dbRead .select() .from(deviceIntents) - .where(and(eq(deviceIntents.user_id, user.id), gt(deviceIntents.created_at, since))) + .where( + and( + eq(deviceIntents.user_id, user.id), + gt(deviceIntents.created_at, since), + ), + ) .orderBy(deviceIntents.created_at); // Update presence heartbeat for this device. @@ -61,7 +71,9 @@ export async function GET( // Record that this device has observed these intents (non-atomic; good // enough for poll — proper dedup/ack is a follow-up with WebSocket). for (const row of rows) { - const delivered = Array.isArray(row.delivered_to) ? [...row.delivered_to] : []; + const delivered = Array.isArray(row.delivered_to) + ? [...row.delivered_to] + : []; if (!delivered.includes(deviceId)) { delivered.push(deviceId); await dbWrite diff --git a/app/api/v1/device-bus/devices/route.ts b/app/api/v1/device-bus/devices/route.ts index c78e3bc37..236cadb25 100644 --- a/app/api/v1/device-bus/devices/route.ts +++ b/app/api/v1/device-bus/devices/route.ts @@ -63,7 +63,10 @@ export async function POST(request: NextRequest): Promise { userId: user.id, platform, }); - return NextResponse.json({ error: "Failed to register device" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to register device" }, + { status: 500 }, + ); } return NextResponse.json({ diff --git a/app/api/v1/device-bus/intents/route.ts b/app/api/v1/device-bus/intents/route.ts index a5a1e3f45..8758747dd 100644 --- a/app/api/v1/device-bus/intents/route.ts +++ b/app/api/v1/device-bus/intents/route.ts @@ -64,7 +64,10 @@ export async function POST(request: NextRequest): Promise { userId: targetUserId, kind, }); - return NextResponse.json({ error: "Failed to publish intent" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to publish intent" }, + { status: 500 }, + ); } return NextResponse.json({ diff --git a/app/api/v1/discord/callback/route.ts b/app/api/v1/discord/callback/route.ts index 40d9d86ad..acb5a6169 100644 --- a/app/api/v1/discord/callback/route.ts +++ b/app/api/v1/discord/callback/route.ts @@ -26,7 +26,10 @@ const LOOPBACK_REDIRECT_ORIGINS = [ "https://127.0.0.1:*", ] as const; -function resolveDiscordAvatarUrl(userId: string, avatarHash: string | null): string | undefined { +function resolveDiscordAvatarUrl( + userId: string, + avatarHash: string | null, +): string | undefined { if (!avatarHash) { return undefined; } @@ -45,7 +48,10 @@ function resolveOAuthReturnTarget( if (managedFlow && returnUrl) { if (returnUrl.startsWith("/")) { - return new URL(sanitizeRelativeRedirectPath(returnUrl, fallbackPath), baseUrl); + return new URL( + sanitizeRelativeRedirectPath(returnUrl, fallbackPath), + baseUrl, + ); } try { @@ -80,7 +86,9 @@ export async function GET(request: NextRequest): Promise { decodedState = discordAutomationService.decodeOAuthState(state); returnTarget = resolveOAuthReturnTarget( baseUrl, - typeof decodedState.returnUrl === "string" ? decodedState.returnUrl : undefined, + typeof decodedState.returnUrl === "string" + ? decodedState.returnUrl + : undefined, decodedState.flow === "milady-managed", ); } catch { @@ -144,7 +152,8 @@ export async function GET(request: NextRequest): Promise { organizationId: decodedState.organizationId, binding: { mode: "cloud-managed", - applicationId: discordAutomationService.getApplicationId() ?? undefined, + applicationId: + discordAutomationService.getApplicationId() ?? undefined, guildId: result.guildId ?? guildId, guildName: result.guildName || "", adminDiscordUserId: result.discordUser.id, diff --git a/app/api/v1/discord/channels/refresh/route.ts b/app/api/v1/discord/channels/refresh/route.ts index a351728c3..5a98e1a0a 100644 --- a/app/api/v1/discord/channels/refresh/route.ts +++ b/app/api/v1/discord/channels/refresh/route.ts @@ -29,11 +29,17 @@ export async function POST(request: NextRequest): Promise { { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } // Verify the guild belongs to this organization - const guild = await discordAutomationService.getGuild(user.organization_id, body.guildId); + const guild = await discordAutomationService.getGuild( + user.organization_id, + body.guildId, + ); if (!guild) { return NextResponse.json({ error: "Guild not found" }, { status: 404 }); } diff --git a/app/api/v1/discord/channels/route.ts b/app/api/v1/discord/channels/route.ts index eef67328e..14d796d19 100644 --- a/app/api/v1/discord/channels/route.ts +++ b/app/api/v1/discord/channels/route.ts @@ -22,12 +22,18 @@ export async function GET(request: NextRequest): Promise { } // Verify the guild belongs to this organization - const guild = await discordAutomationService.getGuild(user.organization_id, guildId); + const guild = await discordAutomationService.getGuild( + user.organization_id, + guildId, + ); if (!guild) { return NextResponse.json({ error: "Guild not found" }, { status: 404 }); } - const channels = await discordAutomationService.getChannels(user.organization_id, guildId); + const channels = await discordAutomationService.getChannels( + user.organization_id, + guildId, + ); return NextResponse.json({ channels: channels.map((c) => ({ diff --git a/app/api/v1/discord/connections/[id]/route.ts b/app/api/v1/discord/connections/[id]/route.ts index cf308cb7e..a56f37d80 100644 --- a/app/api/v1/discord/connections/[id]/route.ts +++ b/app/api/v1/discord/connections/[id]/route.ts @@ -6,7 +6,10 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { discordConnectionsRepository, userCharactersRepository } from "@/db/repositories"; +import { + discordConnectionsRepository, + userCharactersRepository, +} from "@/db/repositories"; import { DiscordConnectionMetadataSchema } from "@/db/schemas/discord-connections"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { logger } from "@/lib/utils/logger"; @@ -33,18 +36,27 @@ const UpdateConnectionSchema = z.object({ * GET /api/v1/discord/connections/[id] * Get a single Discord connection by ID. */ -export async function GET(request: NextRequest, context: RouteContext): Promise { +export async function GET( + request: NextRequest, + context: RouteContext, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await context.params; const connection = await discordConnectionsRepository.findById(id); if (!connection) { - return NextResponse.json({ success: false, error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Connection not found" }, + { status: 404 }, + ); } if (connection.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Connection not found" }, + { status: 404 }, + ); } return NextResponse.json({ @@ -74,25 +86,37 @@ export async function GET(request: NextRequest, context: RouteContext): Promise< * PATCH /api/v1/discord/connections/[id] * Update a Discord connection (character, metadata, active status). */ -export async function PATCH(request: NextRequest, context: RouteContext): Promise { +export async function PATCH( + request: NextRequest, + context: RouteContext, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await context.params; const connection = await discordConnectionsRepository.findById(id); if (!connection) { - return NextResponse.json({ success: false, error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Connection not found" }, + { status: 404 }, + ); } if (connection.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Connection not found" }, + { status: 404 }, + ); } let body: unknown; try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON body" }, + { status: 400 }, + ); } const validation = UpdateConnectionSchema.safeParse(body); @@ -113,7 +137,10 @@ export async function PATCH(request: NextRequest, context: RouteContext): Promis if (data.characterId) { const character = await userCharactersRepository.findById(data.characterId); if (!character) { - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } if (character.organization_id !== user.organization_id) { return NextResponse.json( @@ -193,18 +220,27 @@ export async function PATCH(request: NextRequest, context: RouteContext): Promis * DELETE /api/v1/discord/connections/[id] * Delete a Discord connection. */ -export async function DELETE(request: NextRequest, context: RouteContext): Promise { +export async function DELETE( + request: NextRequest, + context: RouteContext, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await context.params; const connection = await discordConnectionsRepository.findById(id); if (!connection) { - return NextResponse.json({ success: false, error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Connection not found" }, + { status: 404 }, + ); } if (connection.organization_id !== user.organization_id) { - return NextResponse.json({ success: false, error: "Connection not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Connection not found" }, + { status: 404 }, + ); } const deleted = await discordConnectionsRepository.delete(id); diff --git a/app/api/v1/discord/connections/route.ts b/app/api/v1/discord/connections/route.ts index f18e3794c..b2a069a45 100644 --- a/app/api/v1/discord/connections/route.ts +++ b/app/api/v1/discord/connections/route.ts @@ -7,7 +7,10 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { discordConnectionsRepository, userCharactersRepository } from "@/db/repositories"; +import { + discordConnectionsRepository, + userCharactersRepository, +} from "@/db/repositories"; import { DISCORD_DEFAULT_INTENTS, DiscordConnectionMetadataSchema, @@ -37,7 +40,9 @@ const CreateConnectionSchema = z.object({ export async function GET(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); - const connections = await discordConnectionsRepository.findByOrganizationId(user.organization_id); + const connections = await discordConnectionsRepository.findByOrganizationId( + user.organization_id, + ); // Return connections without sensitive token data return NextResponse.json({ @@ -77,7 +82,10 @@ export async function POST(request: NextRequest): Promise { try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON body" }, + { status: 400 }, + ); } const validation = CreateConnectionSchema.safeParse(body); @@ -97,7 +105,10 @@ export async function POST(request: NextRequest): Promise { // Verify character exists and belongs to the organization const character = await userCharactersRepository.findById(data.characterId); if (!character) { - return NextResponse.json({ success: false, error: "Character not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Character not found" }, + { status: 404 }, + ); } if (character.organization_id !== user.organization_id) { return NextResponse.json( @@ -125,7 +136,9 @@ export async function POST(request: NextRequest): Promise { } catch (error) { // Handle PostgreSQL unique constraint violation (discord_connections_org_app_unique_idx) const isUniqueViolation = - error instanceof Error && "code" in error && (error as { code: string }).code === "23505"; + error instanceof Error && + "code" in error && + (error as { code: string }).code === "23505"; if (isUniqueViolation) { // Fetch existing connection to provide helpful response @@ -164,6 +177,7 @@ export async function POST(request: NextRequest): Promise { metadata: connection.metadata, createdAt: connection.created_at, }, - message: "Connection created. The gateway will pick it up within 30 seconds.", + message: + "Connection created. The gateway will pick it up within 30 seconds.", }); } diff --git a/app/api/v1/discord/disconnect/route.ts b/app/api/v1/discord/disconnect/route.ts index cf2ab4c7b..221716307 100644 --- a/app/api/v1/discord/disconnect/route.ts +++ b/app/api/v1/discord/disconnect/route.ts @@ -30,17 +30,26 @@ export async function POST(request: NextRequest): Promise { { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } if (body.guildId) { // Disconnect specific guild - const guild = await discordAutomationService.getGuild(user.organization_id, body.guildId); + const guild = await discordAutomationService.getGuild( + user.organization_id, + body.guildId, + ); if (!guild) { return NextResponse.json({ error: "Guild not found" }, { status: 404 }); } - const result = await discordAutomationService.disconnect(user.organization_id, body.guildId); + const result = await discordAutomationService.disconnect( + user.organization_id, + body.guildId, + ); if (!result.success) { return NextResponse.json({ error: result.error }, { status: 500 }); diff --git a/app/api/v1/discord/oauth/route.ts b/app/api/v1/discord/oauth/route.ts index 5ad7bcd2d..c1e66b7c5 100644 --- a/app/api/v1/discord/oauth/route.ts +++ b/app/api/v1/discord/oauth/route.ts @@ -17,7 +17,10 @@ export async function GET(request: NextRequest): Promise { // Check if Discord is configured if (!discordAutomationService.isOAuthConfigured()) { - return NextResponse.json({ error: "Discord integration not configured" }, { status: 503 }); + return NextResponse.json( + { error: "Discord integration not configured" }, + { status: 503 }, + ); } const { searchParams } = new URL(request.url); diff --git a/app/api/v1/discord/status/route.ts b/app/api/v1/discord/status/route.ts index d7518771c..9b94e009b 100644 --- a/app/api/v1/discord/status/route.ts +++ b/app/api/v1/discord/status/route.ts @@ -28,7 +28,9 @@ export async function GET(request: NextRequest): Promise { }); } - const status = await discordAutomationService.getConnectionStatus(user.organization_id); + const status = await discordAutomationService.getConnectionStatus( + user.organization_id, + ); return NextResponse.json({ // configured = can users add bot to new servers (OAuth flow) diff --git a/app/api/v1/discovery/route.ts b/app/api/v1/discovery/route.ts index e54cb699e..f370bb3bb 100644 --- a/app/api/v1/discovery/route.ts +++ b/app/api/v1/discovery/route.ts @@ -66,7 +66,9 @@ interface DiscoveryResponse { function resolveDiscoverySource(baseUrl: string): ServiceSource { try { const host = new URL(baseUrl).hostname.toLowerCase(); - return host === "localhost" || host === "127.0.0.1" || host.endsWith(".local") + return host === "localhost" || + host === "127.0.0.1" || + host.endsWith(".local") ? "local" : "cloud"; } catch { @@ -94,7 +96,9 @@ function getServiceScore(service: DiscoveredService): number { ); } -function dedupeDiscoveredServices(services: DiscoveredService[]): DiscoveredService[] { +function dedupeDiscoveredServices( + services: DiscoveredService[], +): DiscoveredService[] { const unique = new Map(); for (const service of services) { @@ -167,7 +171,10 @@ export const GET = withRateLimit(async (request: NextRequest) => { const params = parseResult.data; // Generate cache key from params - const paramHash = createHash("md5").update(JSON.stringify(params)).digest("hex").substring(0, 12); + const paramHash = createHash("md5") + .update(JSON.stringify(params)) + .digest("hex") + .substring(0, 12); const cacheKey = CacheKeys.discovery.list(paramHash); // Use cache for discovery results @@ -206,7 +213,9 @@ export const GET = withRateLimit(async (request: NextRequest) => { if (params.query) { const query = params.query.toLowerCase(); filtered = filtered.filter( - (s) => s.name.toLowerCase().includes(query) || s.description.toLowerCase().includes(query), + (s) => + s.name.toLowerCase().includes(query) || + s.description.toLowerCase().includes(query), ); } @@ -222,12 +231,16 @@ export const GET = withRateLimit(async (request: NextRequest) => { // Filter by categories if (params.categories?.length) { - filtered = filtered.filter((s) => s.category && params.categories!.includes(s.category)); + filtered = filtered.filter( + (s) => s.category && params.categories!.includes(s.category), + ); } // Filter by tags if (params.tags?.length) { - filtered = filtered.filter((s) => s.tags.some((tag) => params.tags!.includes(tag))); + filtered = filtered.filter((s) => + s.tags.some((tag) => params.tags!.includes(tag)), + ); } filtered = dedupeDiscoveredServices(filtered); @@ -265,8 +278,11 @@ export const GET = withRateLimit(async (request: NextRequest) => { /** * Fetch local public agents */ -async function fetchLocalAgents(params: z.infer): Promise { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; +async function fetchLocalAgents( + params: z.infer, +): Promise { + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const source = resolveDiscoverySource(baseUrl); // Get all public characters @@ -278,13 +294,17 @@ async function fetchLocalAgents(params: z.infer): Promise char.name.toLowerCase().includes(query) || - (typeof char.bio === "string" && char.bio.toLowerCase().includes(query)) || - (Array.isArray(char.bio) && char.bio.some((b) => b.toLowerCase().includes(query))), + (typeof char.bio === "string" && + char.bio.toLowerCase().includes(query)) || + (Array.isArray(char.bio) && + char.bio.some((b) => b.toLowerCase().includes(query))), ); } if (params.categories?.length) { - characters = characters.filter((char) => params.categories?.includes(char.category ?? "")); + characters = characters.filter((char) => + params.categories?.includes(char.category ?? ""), + ); } // Apply pagination @@ -328,8 +348,11 @@ async function fetchLocalAgents(params: z.infer): Promise): Promise { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; +async function fetchLocalMcps( + params: z.infer, +): Promise { + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const source = resolveDiscoverySource(baseUrl); const mcps = await userMcpsService.listPublic({ diff --git a/app/api/v1/embeddings/route.ts b/app/api/v1/embeddings/route.ts index 94dbac945..f2451f4a2 100644 --- a/app/api/v1/embeddings/route.ts +++ b/app/api/v1/embeddings/route.ts @@ -13,15 +13,27 @@ import { embed, embedMany } from "ai"; import type { NextRequest } from "next/server"; import { getErrorStatusCode, getSafeErrorMessage } from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { enforceOrgRateLimit, RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; -import { estimateTokens, getProviderFromModel, normalizeModelName } from "@/lib/pricing"; +import { + enforceOrgRateLimit, + RateLimitPresets, + withRateLimit, +} from "@/lib/middleware/rate-limit"; +import { + estimateTokens, + getProviderFromModel, + normalizeModelName, +} from "@/lib/pricing"; import { getAiProviderConfigurationError, getTextEmbeddingModel, hasTextEmbeddingProviderConfigured, resolveEmbeddingProviderSource, } from "@/lib/providers/language-model"; -import { billUsage, InsufficientCreditsError, reserveCredits } from "@/lib/services/ai-billing"; +import { + billUsage, + InsufficientCreditsError, + reserveCredits, +} from "@/lib/services/ai-billing"; import { usageService } from "@/lib/services/usage"; import { logger } from "@/lib/utils/logger"; @@ -46,7 +58,10 @@ async function handlePOST(req: NextRequest) { // Per-org tier rate limit if (user.organization_id) { - const orgRateLimited = await enforceOrgRateLimit(user.organization_id, "embeddings"); + const orgRateLimited = await enforceOrgRateLimit( + user.organization_id, + "embeddings", + ); if (orgRateLimited) return orgRateLimited; } @@ -82,7 +97,10 @@ async function handlePOST(req: NextRequest) { ); } - if (typeof request.input === "string" && request.input.trim().length === 0) { + if ( + typeof request.input === "string" && + request.input.trim().length === 0 + ) { return Response.json( { error: { @@ -115,7 +133,9 @@ async function handlePOST(req: NextRequest) { } // Estimate tokens for reservation - const inputText = Array.isArray(request.input) ? request.input.join(" ") : request.input; + const inputText = Array.isArray(request.input) + ? request.input.join(" ") + : request.input; const estimatedInputTokens = estimateTokens(inputText); // Reserve credits BEFORE making API call @@ -249,7 +269,10 @@ async function handlePOST(req: NextRequest) { { error: { message, - type: status === 401 || status === 403 ? "authentication_error" : "api_error", + type: + status === 401 || status === 403 + ? "authentication_error" + : "api_error", }, }, { status }, diff --git a/app/api/v1/gallery/route.ts b/app/api/v1/gallery/route.ts index a8b7e0019..73c31a7fd 100644 --- a/app/api/v1/gallery/route.ts +++ b/app/api/v1/gallery/route.ts @@ -89,7 +89,9 @@ export async function GET(request: NextRequest) { logger.error("[GALLERY API] Error:", error); const status = getErrorStatusCode(error); const errorMessage = - status === 500 ? "Failed to fetch gallery items" : getSafeErrorMessage(error); + status === 500 + ? "Failed to fetch gallery items" + : getSafeErrorMessage(error); return NextResponse.json({ error: errorMessage }, { status }); } diff --git a/app/api/v1/generate-image/route.ts b/app/api/v1/generate-image/route.ts index 703f766e7..3d0872b11 100644 --- a/app/api/v1/generate-image/route.ts +++ b/app/api/v1/generate-image/route.ts @@ -3,7 +3,10 @@ import { streamText } from "ai"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { requireAuthOrApiKey } from "@/lib/auth"; -import { getAnonymousUser, getOrCreateAnonymousUser } from "@/lib/auth-anonymous"; +import { + getAnonymousUser, + getOrCreateAnonymousUser, +} from "@/lib/auth-anonymous"; import { uploadBase64Image } from "@/lib/blob"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { getProviderFromModel } from "@/lib/pricing"; @@ -182,11 +185,13 @@ async function handlePOST(req: NextRequest) { } // Validate and select image model - const isModelAllowed = requestedModel && ALLOWED_IMAGE_MODELS.includes(requestedModel); + const isModelAllowed = + requestedModel && ALLOWED_IMAGE_MODELS.includes(requestedModel); const imageModel = isModelAllowed ? requestedModel : DEFAULT_IMAGE_MODEL; const imageProvider = getImageProvider(imageModel); const imagePricingDimensions = - getSupportedImageModelDefinition(imageModel)?.defaultDimensions ?? undefined; + getSupportedImageModelDefinition(imageModel)?.defaultDimensions ?? + undefined; // Calculate total cost based on number of images const estimatedCost = ( @@ -248,17 +253,23 @@ async function handlePOST(req: NextRequest) { if (stylePreset && stylePreset !== "none") { const styleDescriptions: Record = { none: "", - photographic: "in a photographic style with realistic lighting and details", - "digital-art": "in a digital art style with vibrant colors and modern aesthetics", - "comic-book": "in a comic book style with bold lines and dramatic shading", - "fantasy-art": "in a fantasy art style with magical and ethereal elements", - "analog-film": "in an analog film photography style with film grain and vintage tones", + photographic: + "in a photographic style with realistic lighting and details", + "digital-art": + "in a digital art style with vibrant colors and modern aesthetics", + "comic-book": + "in a comic book style with bold lines and dramatic shading", + "fantasy-art": + "in a fantasy art style with magical and ethereal elements", + "analog-film": + "in an analog film photography style with film grain and vintage tones", "neon-punk": "in a neon punk cyberpunk style with glowing neon colors", isometric: "in an isometric perspective style with geometric precision", "low-poly": "in a low-poly 3D style with geometric facets", origami: "in an origami paper-folding style", "line-art": "in a clean line art style with minimal shading", - cinematic: "in a cinematic style with dramatic lighting and composition", + cinematic: + "in a cinematic style with dramatic lighting and composition", "3d-model": "as a high-quality 3D rendered model", }; @@ -382,7 +393,11 @@ async function handlePOST(req: NextRequest) { // silent failures when gateway aliases route to Google models without the "google/" prefix. const providerOpts = isOpenAIModel ? {} - : { providerOptions: { google: { responseModalities: ["TEXT", "IMAGE"] } } }; + : { + providerOptions: { + google: { responseModalities: ["TEXT", "IMAGE"] }, + }, + }; const result = streamText({ ...streamConfig, ...providerOpts }); for await (const delta of result.fullStream) { @@ -421,7 +436,9 @@ async function handlePOST(req: NextRequest) { } } catch (streamError) { const errorMessage = - streamError instanceof Error ? streamError.message : String(streamError); + streamError instanceof Error + ? streamError.message + : String(streamError); if ( errorMessage.includes("Unauthenticated") || @@ -446,11 +463,15 @@ async function handlePOST(req: NextRequest) { } // Generate multiple images in parallel - const imagePromises = Array.from({ length: numImages }, () => generateSingleImage()); + const imagePromises = Array.from({ length: numImages }, () => + generateSingleImage(), + ); const results = await Promise.all(imagePromises); // Filter out any failed generations - const successfulResults = results.filter((r): r is NonNullable => r !== null); + const successfulResults = results.filter( + (r): r is NonNullable => r !== null, + ); if (successfulResults.length === 0) { // Reconcile with 0 cost (full refund) @@ -492,7 +513,10 @@ async function handlePOST(req: NextRequest) { ); } - return Response.json({ error: "No images were generated" }, { status: 500 }); + return Response.json( + { error: "No images were generated" }, + { status: 500 }, + ); } // Calculate actual cost based on successful images and reconcile @@ -733,8 +757,12 @@ async function handlePOST(req: NextRequest) { numImages: successfulResults.length, }); } catch (error) { - logger.error("[Generate Image] Error:", error instanceof Error ? error.message : String(error)); - const errorMessage = error instanceof Error ? error.message : "Image generation failed"; + logger.error( + "[Generate Image] Error:", + error instanceof Error ? error.message : String(error), + ); + const errorMessage = + error instanceof Error ? error.message : "Image generation failed"; if (generationId) { try { @@ -746,7 +774,9 @@ async function handlePOST(req: NextRequest) { } catch (updateError) { logger.error( "[Generate Image] Failed to update generation record:", - updateError instanceof Error ? updateError.message : String(updateError), + updateError instanceof Error + ? updateError.message + : String(updateError), ); } } @@ -754,7 +784,10 @@ async function handlePOST(req: NextRequest) { return Response.json( { error: errorMessage }, { - status: error instanceof Error && error.message.includes("API key") ? 401 : 500, + status: + error instanceof Error && error.message.includes("API key") + ? 401 + : 500, }, ); } diff --git a/app/api/v1/generate-prompts/route.ts b/app/api/v1/generate-prompts/route.ts index cb2042aa6..5528adcd1 100644 --- a/app/api/v1/generate-prompts/route.ts +++ b/app/api/v1/generate-prompts/route.ts @@ -80,7 +80,10 @@ Random seed: ${promptSeed}`, } catch (error) { logger.error("[Generate Prompts] Error:", error); const status = getErrorStatusCode(error); - const errorMessage = status === 500 ? "Failed to generate prompts" : getSafeErrorMessage(error); + const errorMessage = + status === 500 + ? "Failed to generate prompts" + : getSafeErrorMessage(error); return new Response( JSON.stringify({ error: errorMessage, diff --git a/app/api/v1/generate-video/route.ts b/app/api/v1/generate-video/route.ts index e6579dbcc..bdf000cb3 100644 --- a/app/api/v1/generate-video/route.ts +++ b/app/api/v1/generate-video/route.ts @@ -142,13 +142,17 @@ async function handlePOST(request: NextRequest) { let blobFileSize: bigint | null = null; const fileExtension = - data.video.content_type?.split("/")[1] || data.video.file_name?.split(".").pop() || "mp4"; + data.video.content_type?.split("/")[1] || + data.video.file_name?.split(".").pop() || + "mp4"; try { // Always upload to our storage - videos come from Fal.ai if (!isFalAiUrl(data.video.url)) { // If for some reason it's not a Fal.ai URL, log a warning but still upload - console.warn(`[VIDEO GENERATION] Unexpected non-Fal.ai URL: ${data.video.url}`); + console.warn( + `[VIDEO GENERATION] Unexpected non-Fal.ai URL: ${data.video.url}`, + ); } const uploadResult = await uploadFromUrl(data.video.url, { @@ -161,7 +165,10 @@ async function handlePOST(request: NextRequest) { blobUrl = uploadResult.url; blobFileSize = BigInt(uploadResult.size); } catch (blobError) { - logger.error("[VIDEO GENERATION] Failed to upload to Vercel Blob:", blobError); + logger.error( + "[VIDEO GENERATION] Failed to upload to Vercel Blob:", + blobError, + ); // Reconcile with 0 cost (full refund) - video generated but storage failed await reservation.reconcile(0); return NextResponse.json( @@ -253,10 +260,13 @@ async function handlePOST(request: NextRequest) { cost: billing.totalCost, }) .catch((err) => { - logger.warn("[VIDEO GENERATION] Failed to send Discord notification", { - generationId, - error: err instanceof Error ? err.message : "Unknown error", - }); + logger.warn( + "[VIDEO GENERATION] Failed to send Discord notification", + { + generationId, + error: err instanceof Error ? err.message : "Unknown error", + }, + ); }); } @@ -283,10 +293,14 @@ async function handlePOST(request: NextRequest) { const status = getErrorStatusCode(error); if (status !== 500) { - return NextResponse.json({ error: getSafeErrorMessage(error) }, { status }); + return NextResponse.json( + { error: getSafeErrorMessage(error) }, + { status }, + ); } - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; // If reservation was made, refund the reservation when generation fails if (reservation) { @@ -334,7 +348,10 @@ async function handlePOST(request: NextRequest) { }); } } catch (authError) { - logger.error("[VIDEO GENERATION] Auth error during fallback logging:", authError); + logger.error( + "[VIDEO GENERATION] Auth error during fallback logging:", + authError, + ); } return NextResponse.json( @@ -348,7 +365,10 @@ async function handlePOST(request: NextRequest) { } } -const rateLimitedHandlePOST = withRateLimit(handlePOST, RateLimitPresets.CRITICAL); +const rateLimitedHandlePOST = withRateLimit( + handlePOST, + RateLimitPresets.CRITICAL, +); export async function POST(request: NextRequest) { try { diff --git a/app/api/v1/jobs/[jobId]/route.ts b/app/api/v1/jobs/[jobId]/route.ts index 7c4294b1e..527ff2e9b 100644 --- a/app/api/v1/jobs/[jobId]/route.ts +++ b/app/api/v1/jobs/[jobId]/route.ts @@ -41,10 +41,16 @@ export async function GET( const { jobId } = await params; - const job = await provisioningJobService.getJobForOrg(jobId, organizationId); + const job = await provisioningJobService.getJobForOrg( + jobId, + organizationId, + ); if (!job) { - return NextResponse.json({ success: false, error: "Job not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Job not found" }, + { status: 404 }, + ); } return NextResponse.json({ diff --git a/app/api/v1/market/candles/[chain]/[address]/route.ts b/app/api/v1/market/candles/[chain]/[address]/route.ts index 618f7ec78..0bd54345f 100644 --- a/app/api/v1/market/candles/[chain]/[address]/route.ts +++ b/app/api/v1/market/candles/[chain]/[address]/route.ts @@ -1,8 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { isValidAddress, isValidChain } from "@/lib/services/proxy/services/address-validation"; -import { marketDataConfig, marketDataHandler } from "@/lib/services/proxy/services/market-data"; +import { + isValidAddress, + isValidChain, +} from "@/lib/services/proxy/services/address-validation"; +import { + marketDataConfig, + marketDataHandler, +} from "@/lib/services/proxy/services/market-data"; export const maxDuration = 30; const CORS_METHODS = "GET, OPTIONS"; diff --git a/app/api/v1/market/portfolio/[chain]/[address]/route.ts b/app/api/v1/market/portfolio/[chain]/[address]/route.ts index 33f26ba14..b08e8ce52 100644 --- a/app/api/v1/market/portfolio/[chain]/[address]/route.ts +++ b/app/api/v1/market/portfolio/[chain]/[address]/route.ts @@ -1,8 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { isValidAddress, isValidChain } from "@/lib/services/proxy/services/address-validation"; -import { marketDataConfig, marketDataHandler } from "@/lib/services/proxy/services/market-data"; +import { + isValidAddress, + isValidChain, +} from "@/lib/services/proxy/services/address-validation"; +import { + marketDataConfig, + marketDataHandler, +} from "@/lib/services/proxy/services/market-data"; export const maxDuration = 30; const CORS_METHODS = "GET, OPTIONS"; diff --git a/app/api/v1/market/price/[chain]/[address]/route.ts b/app/api/v1/market/price/[chain]/[address]/route.ts index 5859ee11c..ffbce5382 100644 --- a/app/api/v1/market/price/[chain]/[address]/route.ts +++ b/app/api/v1/market/price/[chain]/[address]/route.ts @@ -21,8 +21,14 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { isValidAddress, isValidChain } from "@/lib/services/proxy/services/address-validation"; -import { marketDataConfig, marketDataHandler } from "@/lib/services/proxy/services/market-data"; +import { + isValidAddress, + isValidChain, +} from "@/lib/services/proxy/services/address-validation"; +import { + marketDataConfig, + marketDataHandler, +} from "@/lib/services/proxy/services/market-data"; // WHY 30s maxDuration: // - Vercel serverless functions default to 10s diff --git a/app/api/v1/market/token/[chain]/[address]/route.ts b/app/api/v1/market/token/[chain]/[address]/route.ts index afefd3991..e29830086 100644 --- a/app/api/v1/market/token/[chain]/[address]/route.ts +++ b/app/api/v1/market/token/[chain]/[address]/route.ts @@ -1,8 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { isValidAddress, isValidChain } from "@/lib/services/proxy/services/address-validation"; -import { marketDataConfig, marketDataHandler } from "@/lib/services/proxy/services/market-data"; +import { + isValidAddress, + isValidChain, +} from "@/lib/services/proxy/services/address-validation"; +import { + marketDataConfig, + marketDataHandler, +} from "@/lib/services/proxy/services/market-data"; export const maxDuration = 30; const CORS_METHODS = "GET, OPTIONS"; diff --git a/app/api/v1/market/trades/[chain]/[address]/route.ts b/app/api/v1/market/trades/[chain]/[address]/route.ts index 91befd58e..f5e2a57e8 100644 --- a/app/api/v1/market/trades/[chain]/[address]/route.ts +++ b/app/api/v1/market/trades/[chain]/[address]/route.ts @@ -1,8 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { isValidAddress, isValidChain } from "@/lib/services/proxy/services/address-validation"; -import { marketDataConfig, marketDataHandler } from "@/lib/services/proxy/services/market-data"; +import { + isValidAddress, + isValidChain, +} from "@/lib/services/proxy/services/address-validation"; +import { + marketDataConfig, + marketDataHandler, +} from "@/lib/services/proxy/services/market-data"; export const maxDuration = 30; const CORS_METHODS = "GET, OPTIONS"; diff --git a/app/api/v1/mcps/[mcpId]/publish/route.ts b/app/api/v1/mcps/[mcpId]/publish/route.ts index b09d85f5c..6ebabd949 100644 --- a/app/api/v1/mcps/[mcpId]/publish/route.ts +++ b/app/api/v1/mcps/[mcpId]/publish/route.ts @@ -16,11 +16,17 @@ export const dynamic = "force-dynamic"; * POST /api/v1/mcps/[mcpId]/publish * Publish MCP (make it live and discoverable) */ -export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function POST( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const authResult = await requireAuthOrApiKeyWithOrg(request); const { mcpId } = await ctx.params; - const mcp = await userMcpsService.publish(mcpId, authResult.user.organization_id); + const mcp = await userMcpsService.publish( + mcpId, + authResult.user.organization_id, + ); logger.info("[API] Published user MCP", { id: mcpId, @@ -30,7 +36,8 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: return NextResponse.json({ mcp, - message: "MCP published successfully. It is now discoverable in the registry.", + message: + "MCP published successfully. It is now discoverable in the registry.", }); } @@ -38,11 +45,17 @@ export async function POST(request: NextRequest, ctx: { params: Promise<{ mcpId: * DELETE /api/v1/mcps/[mcpId]/publish * Unpublish MCP (back to draft) */ -export async function DELETE(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function DELETE( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const authResult = await requireAuthOrApiKeyWithOrg(request); const { mcpId } = await ctx.params; - const mcp = await userMcpsService.unpublish(mcpId, authResult.user.organization_id); + const mcp = await userMcpsService.unpublish( + mcpId, + authResult.user.organization_id, + ); logger.info("[API] Unpublished user MCP", { id: mcpId, @@ -64,7 +77,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/mcps/[mcpId]/route.ts b/app/api/v1/mcps/[mcpId]/route.ts index f8a85b6e5..db152bf16 100644 --- a/app/api/v1/mcps/[mcpId]/route.ts +++ b/app/api/v1/mcps/[mcpId]/route.ts @@ -70,7 +70,10 @@ const updateMcpSchema = z.object({ * GET /api/v1/mcps/[mcpId] * Get MCP details */ -export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function GET( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const authResult = await requireAuthOrApiKeyWithOrg(request); const { mcpId } = await ctx.params; @@ -81,7 +84,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: } // Check access - owner can see all, others can only see public - if (mcp.organization_id !== authResult.user.organization_id && !mcp.is_public) { + if ( + mcp.organization_id !== authResult.user.organization_id && + !mcp.is_public + ) { return NextResponse.json({ error: "MCP not found" }, { status: 404 }); } @@ -92,7 +98,8 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: } // Get endpoint URL - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const endpointUrl = userMcpsService.getEndpointUrl(mcp, baseUrl); return NextResponse.json({ @@ -109,7 +116,10 @@ export async function GET(request: NextRequest, ctx: { params: Promise<{ mcpId: * PUT /api/v1/mcps/[mcpId] * Update MCP */ -export async function PUT(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function PUT( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const authResult = await requireAuthOrApiKeyWithOrg(request); const { mcpId } = await ctx.params; @@ -129,7 +139,11 @@ export async function PUT(request: NextRequest, ctx: { params: Promise<{ mcpId: ); } - const mcp = await userMcpsService.update(mcpId, authResult.user.organization_id, validation.data); + const mcp = await userMcpsService.update( + mcpId, + authResult.user.organization_id, + validation.data, + ); logger.info("[API] Updated user MCP", { id: mcpId, @@ -143,7 +157,10 @@ export async function PUT(request: NextRequest, ctx: { params: Promise<{ mcpId: * DELETE /api/v1/mcps/[mcpId] * Delete MCP */ -export async function DELETE(request: NextRequest, ctx: { params: Promise<{ mcpId: string }> }) { +export async function DELETE( + request: NextRequest, + ctx: { params: Promise<{ mcpId: string }> }, +) { const authResult = await requireAuthOrApiKeyWithOrg(request); const { mcpId } = await ctx.params; @@ -166,7 +183,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/mcps/route.ts b/app/api/v1/mcps/route.ts index 77c280cac..4e3905632 100644 --- a/app/api/v1/mcps/route.ts +++ b/app/api/v1/mcps/route.ts @@ -74,7 +74,9 @@ const createMcpSchema = z.object({ const listMcpsSchema = z.object({ category: z.string().max(30).optional(), search: z.string().max(100).optional(), - status: z.enum(["draft", "pending_review", "live", "suspended", "deprecated"]).optional(), + status: z + .enum(["draft", "pending_review", "live", "suspended", "deprecated"]) + .optional(), scope: z.enum(["own", "public", "all"]).optional().default("own"), limit: z.coerce.number().int().min(1).max(100).optional().default(50), offset: z.coerce.number().int().min(0).optional().default(0), @@ -128,7 +130,8 @@ export async function POST(request: NextRequest) { } catch (error) { return NextResponse.json( { - error: error instanceof Error ? error.message : "Unsafe external endpoint", + error: + error instanceof Error ? error.message : "Unsafe external endpoint", }, { status: 400 }, ); @@ -187,11 +190,14 @@ export async function GET(request: NextRequest) { }); } else if (scope === "own") { // List user's own MCPs - mcps = await userMcpsService.listByOrganization(authResult.user.organization_id, { - status, - limit, - offset, - }); + mcps = await userMcpsService.listByOrganization( + authResult.user.organization_id, + { + status, + limit, + offset, + }, + ); } else { // List all (own + public) const [ownMcps, publicMcps] = await Promise.all([ @@ -231,7 +237,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/messages/route.ts b/app/api/v1/messages/route.ts index 4fd5043eb..03d02aa1f 100644 --- a/app/api/v1/messages/route.ts +++ b/app/api/v1/messages/route.ts @@ -55,7 +55,9 @@ type AnthropicTextBlock = { type: "text"; text: string }; type AnthropicImageBlock = { type: "image"; - source: { type: "url"; url: string } | { type: "base64"; media_type: string; data: string }; + source: + | { type: "url"; url: string } + | { type: "base64"; media_type: string; data: string }; }; type AnthropicToolUseBlock = { @@ -84,7 +86,9 @@ interface AnthropicMessageParam { content: string | AnthropicContentBlock[]; } -type AnthropicSystemParam = string | Array<{ type: "text"; text: string; cache_control?: unknown }>; +type AnthropicSystemParam = + | string + | Array<{ type: "text"; text: string; cache_control?: unknown }>; interface AnthropicTool { name: string; @@ -112,7 +116,11 @@ interface AnthropicMessagesRequest { tool_choice?: AnthropicToolChoice; } -type AnthropicStopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use"; +type AnthropicStopReason = + | "end_turn" + | "max_tokens" + | "stop_sequence" + | "tool_use"; type ToolNameMap = Map; type AppCreditsInfo = { @@ -158,7 +166,9 @@ function inferImageMediaType(urlOrType: string): string { return "image/jpeg"; } -function normalizeSystemPrompt(system: AnthropicSystemParam | undefined): string | undefined { +function normalizeSystemPrompt( + system: AnthropicSystemParam | undefined, +): string | undefined { if (!system) return undefined; if (typeof system === "string") return system; return system.map((block) => block.text).join("\n\n"); @@ -166,7 +176,12 @@ function normalizeSystemPrompt(system: AnthropicSystemParam | undefined): string function mapToolChoice( toolChoice: AnthropicToolChoice | undefined, -): "auto" | "none" | "required" | { type: "tool"; toolName: string } | undefined { +): + | "auto" + | "none" + | "required" + | { type: "tool"; toolName: string } + | undefined { if (!toolChoice) return undefined; if (toolChoice.type === "auto") return "auto"; if (toolChoice.type === "none") return "none"; @@ -234,7 +249,9 @@ function serializeToolResultContent( return content; } -function toToolResultOutput(content: string | AnthropicContentBlock[]): ToolResultPart["output"] { +function toToolResultOutput( + content: string | AnthropicContentBlock[], +): ToolResultPart["output"] { const serialized = serializeToolResultContent(content); if (typeof serialized === "string") { @@ -247,7 +264,10 @@ function toToolResultOutput(content: string | AnthropicContentBlock[]): ToolResu }; } -function trackToolNames(content: string | AnthropicContentBlock[], toolNames: ToolNameMap): void { +function trackToolNames( + content: string | AnthropicContentBlock[], + toolNames: ToolNameMap, +): void { if (typeof content === "string") return; for (const block of content) { @@ -257,7 +277,9 @@ function trackToolNames(content: string | AnthropicContentBlock[], toolNames: To } } -function anthropicMessagesToModelMessages(messages: AnthropicMessageParam[]): ModelMessage[] { +function anthropicMessagesToModelMessages( + messages: AnthropicMessageParam[], +): ModelMessage[] { const modelMessages: ModelMessage[] = []; const toolNames = new Map(); @@ -362,7 +384,10 @@ function anthropicMessagesToModelMessages(messages: AnthropicMessageParam[]): Mo const assistantMessage: AssistantModelMessage = { role: "assistant", - content: assistantParts.length > 0 ? assistantParts : [{ type: "text", text: "" }], + content: + assistantParts.length > 0 + ? assistantParts + : [{ type: "text", text: "" }], }; modelMessages.push(assistantMessage); } @@ -379,7 +404,9 @@ function getMessageContentForEstimate(message: AnthropicMessageParam): string { if (block.type === "tool_use") return JSON.stringify(block.input); if (block.type === "tool_result") { const serialized = serializeToolResultContent(block.content); - return typeof serialized === "string" ? serialized : JSON.stringify(serialized); + return typeof serialized === "string" + ? serialized + : JSON.stringify(serialized); } return ""; }) @@ -440,8 +467,14 @@ function addCorsHeaders(response: Response): Response { }); } -function anthropicError(type: string, message: string, status: number): Response { - return addCorsHeaders(Response.json({ type: "error", error: { type, message } }, { status })); +function anthropicError( + type: string, + message: string, + status: number, +): Response { + return addCorsHeaders( + Response.json({ type: "error", error: { type, message } }, { status }), + ); } async function handlePOST(req: NextRequest) { @@ -462,7 +495,9 @@ async function handlePOST(req: NextRequest) { const appId = req.headers.get("X-App-Id"); let useAppCredits = false; - let monetizedApp: NonNullable>> | null = null; + let monetizedApp: NonNullable< + Awaited> + > | null = null; if (appId) { monetizedApp = (await appsService.getById(appId)) ?? null; useAppCredits = Boolean(monetizedApp?.monetization_enabled); @@ -480,7 +515,11 @@ async function handlePOST(req: NextRequest) { } const request = body as AnthropicMessagesRequest; - if (!request.model || request.max_tokens == null || !request.messages?.length) { + if ( + !request.model || + request.max_tokens == null || + !request.messages?.length + ) { return anthropicError( "invalid_request_error", "Missing required fields: model, max_tokens, messages", @@ -501,16 +540,23 @@ async function handlePOST(req: NextRequest) { ); } - const lastUserMessage = request.messages.filter((message) => message.role === "user").pop(); + const lastUserMessage = request.messages + .filter((message) => message.role === "user") + .pop(); if (lastUserMessage) { const content = getMessageContentForEstimate(lastUserMessage); if (content) { - contentModerationService.moderateInBackground(content, user.id, undefined, (result) => { - logger.warn("[Messages API] Async moderation detected violation", { - userId: user.id, - categories: result.flaggedCategories, - }); - }); + contentModerationService.moderateInBackground( + content, + user.id, + undefined, + (result) => { + logger.warn("[Messages API] Async moderation detected violation", { + userId: user.id, + categories: result.flaggedCategories, + }); + }, + ); } } @@ -538,7 +584,10 @@ async function handlePOST(req: NextRequest) { estimatedOutputTokens, billingSource, ); - const costWithMarkup = await appCreditsService.calculateCostWithMarkup(appId, totalCost); + const costWithMarkup = await appCreditsService.calculateCostWithMarkup( + appId, + totalCost, + ); const balanceCheck = await appCreditsService.checkBalance( appId, user.id, @@ -658,7 +707,12 @@ async function handleNonStream( startTime: number, safeParams: ReturnType, tools: ReturnType, - toolChoice: "auto" | "none" | "required" | { type: "tool"; toolName: string } | undefined, + toolChoice: + | "auto" + | "none" + | "required" + | { type: "tool"; toolName: string } + | undefined, abortSignal: AbortSignal | undefined, timeoutMs: number, settleReservation: (actualCost: number) => Promise, @@ -670,12 +724,17 @@ async function handleNonStream( const cotBudget = resolveAnthropicThinkingBudgetTokens(model, process.env); // Passing resolved budget to mergeAnthropicCotProviderOptions short-circuits its internal resolution const cotOptions = - cotBudget != null ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) : {}; + cotBudget != null + ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) + : {}; // When CoT is active, max_tokens must include room for both thinking AND response tokens const MIN_RESPONSE_BUFFER = 4096; const effectiveMaxTokens = cotBudget != null - ? Math.max(request.max_tokens ?? MIN_RESPONSE_BUFFER, cotBudget + MIN_RESPONSE_BUFFER) + ? Math.max( + request.max_tokens ?? MIN_RESPONSE_BUFFER, + cotBudget + MIN_RESPONSE_BUFFER, + ) : request.max_tokens; try { @@ -757,7 +816,11 @@ async function handleNonStream( } const hasToolCalls = Boolean(result.toolCalls?.length); - const stopReason = mapFinishReason(result.finishReason, result.rawFinishReason, hasToolCalls); + const stopReason = mapFinishReason( + result.finishReason, + result.rawFinishReason, + hasToolCalls, + ); const stopSequence = resolveStopSequence( stopReason, result.rawFinishReason, @@ -798,7 +861,12 @@ async function handleStream( estimatedInputTokens: number, safeParams: ReturnType, tools: ReturnType, - toolChoice: "auto" | "none" | "required" | { type: "tool"; toolName: string } | undefined, + toolChoice: + | "auto" + | "none" + | "required" + | { type: "tool"; toolName: string } + | undefined, abortSignal: AbortSignal | undefined, timeoutMs: number, settleReservation: (actualCost: number) => Promise, @@ -811,12 +879,17 @@ async function handleStream( const cotBudget = resolveAnthropicThinkingBudgetTokens(model, process.env); // Passing resolved budget to mergeAnthropicCotProviderOptions short-circuits its internal resolution const cotOptions = - cotBudget != null ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) : {}; + cotBudget != null + ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) + : {}; // When CoT is active, max_tokens must include room for both thinking AND response tokens const MIN_RESPONSE_BUFFER = 4096; const effectiveMaxTokens = cotBudget != null - ? Math.max(request.max_tokens ?? MIN_RESPONSE_BUFFER, cotBudget + MIN_RESPONSE_BUFFER) + ? Math.max( + request.max_tokens ?? MIN_RESPONSE_BUFFER, + cotBudget + MIN_RESPONSE_BUFFER, + ) : request.max_tokens; const result = streamText({ @@ -1093,7 +1166,11 @@ async function handleStream( } } - const stopReason = mapFinishReason(finishReason, rawFinishReason, sawToolCalls); + const stopReason = mapFinishReason( + finishReason, + rawFinishReason, + sawToolCalls, + ); const stopSequence = resolveStopSequence( stopReason, rawFinishReason, diff --git a/app/api/v1/milady/agents/[agentId]/api/wallet/[...path]/route.ts b/app/api/v1/milady/agents/[agentId]/api/wallet/[...path]/route.ts index 42d646bea..e43bd849a 100644 --- a/app/api/v1/milady/agents/[agentId]/api/wallet/[...path]/route.ts +++ b/app/api/v1/milady/agents/[agentId]/api/wallet/[...path]/route.ts @@ -39,14 +39,19 @@ async function proxyToAgent( // Reject multi-segment paths to prevent path traversal if (path.length !== 1 || path[0].includes("..")) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Invalid wallet path" }, { status: 400 }), + NextResponse.json( + { success: false, error: "Invalid wallet path" }, + { status: 400 }, + ), CORS_METHODS, ); } const walletPath = path[0]; // Forward validated query string (e.g. ?limit=20 for steward-tx-records) - const query = request.nextUrl.search ? request.nextUrl.search.slice(1) : undefined; + const query = request.nextUrl.search + ? request.nextUrl.search.slice(1) + : undefined; // Read POST body if present (with size limit and content-type check) let body: string | null = null; @@ -64,7 +69,10 @@ async function proxyToAgent( body = await request.text(); if (body.length > 1_048_576) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Request body too large" }, { status: 413 }), + NextResponse.json( + { success: false, error: "Request body too large" }, + { status: 413 }, + ), CORS_METHODS, ); } @@ -103,7 +111,8 @@ async function proxyToAgent( // Forward status + body from agent response directly const responseBody = await agentResponse.text(); - const contentType = agentResponse.headers.get("content-type") ?? "application/json"; + const contentType = + agentResponse.headers.get("content-type") ?? "application/json"; return applyCorsHeaders( new Response(responseBody, { diff --git a/app/api/v1/milady/agents/[agentId]/backups/route.ts b/app/api/v1/milady/agents/[agentId]/backups/route.ts index 4e5bd0c62..477a8ad4c 100644 --- a/app/api/v1/milady/agents/[agentId]/backups/route.ts +++ b/app/api/v1/milady/agents/[agentId]/backups/route.ts @@ -24,7 +24,10 @@ export async function GET( const { user } = await requireAuthOrApiKeyWithOrg(request); const { agentId } = await params; - const backups = await miladySandboxService.listBackups(agentId, user.organization_id); + const backups = await miladySandboxService.listBackups( + agentId, + user.organization_id, + ); return applyCorsHeaders( NextResponse.json({ diff --git a/app/api/v1/milady/agents/[agentId]/bridge/route.ts b/app/api/v1/milady/agents/[agentId]/bridge/route.ts index a838ac31c..f864c1e58 100644 --- a/app/api/v1/milady/agents/[agentId]/bridge/route.ts +++ b/app/api/v1/milady/agents/[agentId]/bridge/route.ts @@ -43,7 +43,11 @@ export async function POST( if (!parsed.success) { return applyCorsHeaders( NextResponse.json( - { success: false, error: "Invalid JSON-RPC request", details: parsed.error.issues }, + { + success: false, + error: "Invalid JSON-RPC request", + details: parsed.error.issues, + }, { status: 400 }, ), CORS_METHODS, @@ -51,7 +55,11 @@ export async function POST( } const rpcRequest = parsed.data as BridgeRequest; - const response = await miladySandboxService.bridge(agentId, user.organization_id, rpcRequest); + const response = await miladySandboxService.bridge( + agentId, + user.organization_id, + rpcRequest, + ); return applyCorsHeaders(NextResponse.json(response), CORS_METHODS); } catch (error) { diff --git a/app/api/v1/milady/agents/[agentId]/discord/oauth/route.ts b/app/api/v1/milady/agents/[agentId]/discord/oauth/route.ts index fa54c2d8a..2f3624095 100644 --- a/app/api/v1/milady/agents/[agentId]/discord/oauth/route.ts +++ b/app/api/v1/milady/agents/[agentId]/discord/oauth/route.ts @@ -40,7 +40,10 @@ function resolveManagedReturnUrl(rawValue: string | undefined): string { } if (rawValue.startsWith("/")) { - return new URL(sanitizeRelativeRedirectPath(rawValue, defaultPath), baseUrl).toString(); + return new URL( + sanitizeRelativeRedirectPath(rawValue, defaultPath), + baseUrl, + ).toString(); } return assertAllowedAbsoluteRedirectUrl(rawValue, [ @@ -67,10 +70,16 @@ export async function POST( ); } - const sandbox = await miladySandboxService.getAgent(agentId, user.organization_id); + const sandbox = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!sandbox) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -80,7 +89,11 @@ export async function POST( if (!parsed.success) { return applyCorsHeaders( NextResponse.json( - { success: false, error: "Invalid request", details: parsed.error.issues }, + { + success: false, + error: "Invalid request", + details: parsed.error.issues, + }, { status: 400 }, ), CORS_METHODS, diff --git a/app/api/v1/milady/agents/[agentId]/discord/route.ts b/app/api/v1/milady/agents/[agentId]/discord/route.ts index cc171a7ac..47814a24a 100644 --- a/app/api/v1/milady/agents/[agentId]/discord/route.ts +++ b/app/api/v1/milady/agents/[agentId]/discord/route.ts @@ -30,12 +30,18 @@ export async function GET( if (!status) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } - return applyCorsHeaders(NextResponse.json({ success: true, data: status }), CORS_METHODS); + return applyCorsHeaders( + NextResponse.json({ success: true, data: status }), + CORS_METHODS, + ); } catch (error) { return applyCorsHeaders(errorToResponse(error), CORS_METHODS); } diff --git a/app/api/v1/milady/agents/[agentId]/github/link/route.ts b/app/api/v1/milady/agents/[agentId]/github/link/route.ts index b062a7931..51c91171e 100644 --- a/app/api/v1/milady/agents/[agentId]/github/link/route.ts +++ b/app/api/v1/milady/agents/[agentId]/github/link/route.ts @@ -38,7 +38,10 @@ export async function POST( const parsed = linkSchema.safeParse(body); if (!parsed.success) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "connectionId is required" }, { status: 400 }), + NextResponse.json( + { success: false, error: "connectionId is required" }, + { status: 400 }, + ), CORS_METHODS, ); } @@ -53,7 +56,10 @@ export async function POST( if (!connection) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "OAuth connection not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "OAuth connection not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -72,7 +78,10 @@ export async function POST( agentId, organizationId: user.organization_id, binding: { - mode: connection.connectionRole === "owner" ? "shared-owner" : "cloud-managed", + mode: + connection.connectionRole === "owner" + ? "shared-owner" + : "cloud-managed", connectionId, connectionRole: connection.connectionRole, source: connection.source, diff --git a/app/api/v1/milady/agents/[agentId]/github/oauth/route.ts b/app/api/v1/milady/agents/[agentId]/github/oauth/route.ts index c216b1fa0..fe04800ae 100644 --- a/app/api/v1/milady/agents/[agentId]/github/oauth/route.ts +++ b/app/api/v1/milady/agents/[agentId]/github/oauth/route.ts @@ -5,7 +5,10 @@ import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; // redirect-validation not needed — GitHub uses generic OAuth callback which // restricts to ALLOWED_REDIRECT_PATHS; we always redirect to a cloud path. -import { getProvider, isProviderConfigured } from "@/lib/services/oauth/provider-registry"; +import { + getProvider, + isProviderConfigured, +} from "@/lib/services/oauth/provider-registry"; import { initiateOAuth2 } from "@/lib/services/oauth/providers"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; @@ -74,10 +77,16 @@ export async function POST( ); } - const sandbox = await miladySandboxService.getAgent(agentId, user.organization_id); + const sandbox = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!sandbox) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -87,7 +96,11 @@ export async function POST( if (!parsed.success) { return applyCorsHeaders( NextResponse.json( - { success: false, error: "Invalid request", details: parsed.error.issues }, + { + success: false, + error: "Invalid request", + details: parsed.error.issues, + }, { status: 400 }, ), CORS_METHODS, @@ -95,10 +108,15 @@ export async function POST( } const scopes = parsed.data.scopes || provider.defaultScopes || []; - const redirectUrl = resolveManagedReturnUrl(agentId, user.organization_id, user.id, { - postMessage: parsed.data.postMessage, - returnUrl: parsed.data.returnUrl, - }); + const redirectUrl = resolveManagedReturnUrl( + agentId, + user.organization_id, + user.id, + { + postMessage: parsed.data.postMessage, + returnUrl: parsed.data.returnUrl, + }, + ); const result = await initiateOAuth2(provider, { organizationId: user.organization_id, diff --git a/app/api/v1/milady/agents/[agentId]/github/route.ts b/app/api/v1/milady/agents/[agentId]/github/route.ts index 688d4d3a6..c47f71305 100644 --- a/app/api/v1/milady/agents/[agentId]/github/route.ts +++ b/app/api/v1/milady/agents/[agentId]/github/route.ts @@ -27,12 +27,18 @@ export async function GET( if (!status) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } - return applyCorsHeaders(NextResponse.json({ success: true, data: status }), CORS_METHODS); + return applyCorsHeaders( + NextResponse.json({ success: true, data: status }), + CORS_METHODS, + ); } catch (error) { return applyCorsHeaders(errorToResponse(error), CORS_METHODS); } diff --git a/app/api/v1/milady/agents/[agentId]/github/token/route.ts b/app/api/v1/milady/agents/[agentId]/github/token/route.ts index b1135713c..b7b208b90 100644 --- a/app/api/v1/milady/agents/[agentId]/github/token/route.ts +++ b/app/api/v1/milady/agents/[agentId]/github/token/route.ts @@ -35,7 +35,10 @@ export async function GET( if (!result) { return applyCorsHeaders( NextResponse.json( - { success: false, error: "No GitHub connection found for this agent" }, + { + success: false, + error: "No GitHub connection found for this agent", + }, { status: 404 }, ), CORS_METHODS, diff --git a/app/api/v1/milady/agents/[agentId]/pairing-token/route.ts b/app/api/v1/milady/agents/[agentId]/pairing-token/route.ts index 8410f93f2..6a04d12aa 100644 --- a/app/api/v1/milady/agents/[agentId]/pairing-token/route.ts +++ b/app/api/v1/milady/agents/[agentId]/pairing-token/route.ts @@ -29,11 +29,17 @@ export async function POST( const { user } = await requireAuthOrApiKeyWithOrg(request); const { agentId } = await params; - const sandbox = await miladySandboxesRepository.findByIdAndOrg(agentId, user.organization_id); + const sandbox = await miladySandboxesRepository.findByIdAndOrg( + agentId, + user.organization_id, + ); if (!sandbox) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -41,7 +47,10 @@ export async function POST( if (sandbox.status !== "running") { return applyCorsHeaders( NextResponse.json( - { success: false, error: "Agent must be running to generate pairing token" }, + { + success: false, + error: "Agent must be running to generate pairing token", + }, { status: 400 }, ), CORS_METHODS, @@ -76,14 +85,19 @@ export async function POST( success: true, data: { token: pairingToken, - redirectUrl: supportsUiTokenPairing ? `${webUiUrl}/pair?token=${pairingToken}` : webUiUrl, + redirectUrl: supportsUiTokenPairing + ? `${webUiUrl}/pair?token=${pairingToken}` + : webUiUrl, expiresIn: 60, }, }), CORS_METHODS, ); - response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); + response.headers.set( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate", + ); response.headers.set("Pragma", "no-cache"); response.headers.set("Expires", "0"); diff --git a/app/api/v1/milady/agents/[agentId]/provision/route.ts b/app/api/v1/milady/agents/[agentId]/provision/route.ts index 470c47099..fbeac5fcb 100644 --- a/app/api/v1/milady/agents/[agentId]/provision/route.ts +++ b/app/api/v1/milady/agents/[agentId]/provision/route.ts @@ -37,7 +37,10 @@ function sanitizeProvisionFailureMessage( return "Provisioning failed"; } -function sanitizeEnqueueFailureMessage(error: string, status: 404 | 409 | 500): string { +function sanitizeEnqueueFailureMessage( + error: string, + status: 404 | 409 | 500, +): string { if (status !== 500) { return error; } @@ -79,15 +82,25 @@ export async function POST( }); // Fast path: check if already running (no job needed) - const existing = await miladySandboxService.getAgentForWrite(agentId, user.organization_id!); + const existing = await miladySandboxService.getAgentForWrite( + agentId, + user.organization_id!, + ); if (!existing) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } - if (existing.status === "running" && existing.bridge_url && existing.health_url) { + if ( + existing.status === "running" && + existing.bridge_url && + existing.health_url + ) { return applyCorsHeaders( NextResponse.json({ success: true, @@ -128,11 +141,17 @@ export async function POST( // ── Sync fallback (legacy) ──────────────────────────────────────── if (sync) { - const result = await miladySandboxService.provision(agentId, user.organization_id!); + const result = await miladySandboxService.provision( + agentId, + user.organization_id!, + ); if (!result.success) { const status = getProvisionFailureStatus(result.error); - const clientError = sanitizeProvisionFailureMessage(result.error, status); + const clientError = sanitizeProvisionFailureMessage( + result.error, + status, + ); if (status === 500) { logger.error("[milady-api] Sync provision failed", { @@ -173,7 +192,8 @@ export async function POST( NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Invalid webhook URL", + error: + error instanceof Error ? error.message : "Invalid webhook URL", }, { status: 400 }, ), diff --git a/app/api/v1/milady/agents/[agentId]/restore/route.ts b/app/api/v1/milady/agents/[agentId]/restore/route.ts index 8e2f14f04..69ff2e765 100644 --- a/app/api/v1/milady/agents/[agentId]/restore/route.ts +++ b/app/api/v1/milady/agents/[agentId]/restore/route.ts @@ -61,7 +61,8 @@ export async function POST( ? 404 : result.error === "No backup found" ? 404 - : result.error === "Stopped agents can only restore the latest backup" + : result.error === + "Stopped agents can only restore the latest backup" ? 409 : 500; diff --git a/app/api/v1/milady/agents/[agentId]/resume/route.ts b/app/api/v1/milady/agents/[agentId]/resume/route.ts index 28fe6ed88..60a0f7683 100644 --- a/app/api/v1/milady/agents/[agentId]/resume/route.ts +++ b/app/api/v1/milady/agents/[agentId]/resume/route.ts @@ -47,10 +47,16 @@ export async function POST( async: !sync, }); - const agent = await miladySandboxService.getAgentForWrite(agentId, user.organization_id); + const agent = await miladySandboxService.getAgentForWrite( + agentId, + user.organization_id, + ); if (!agent) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -94,7 +100,10 @@ export async function POST( } if (sync) { - const result = await miladySandboxService.provision(agentId, user.organization_id); + const result = await miladySandboxService.provision( + agentId, + user.organization_id, + ); if (!result.success) { const status = @@ -104,7 +113,10 @@ export async function POST( ? 409 : 500; return applyCorsHeaders( - NextResponse.json({ success: false, error: result.error ?? "Resume failed" }, { status }), + NextResponse.json( + { success: false, error: result.error ?? "Resume failed" }, + { status }, + ), CORS_METHODS, ); } @@ -134,7 +146,8 @@ export async function POST( NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Invalid webhook URL", + error: + error instanceof Error ? error.message : "Invalid webhook URL", }, { status: 400 }, ), @@ -144,14 +157,15 @@ export async function POST( } try { - const { job, created } = await provisioningJobService.enqueueMiladyProvisionOnce({ - agentId, - organizationId: user.organization_id, - userId: user.id, - agentName: agent.agent_name ?? agentId, - webhookUrl, - expectedUpdatedAt: agent.updated_at, - }); + const { job, created } = + await provisioningJobService.enqueueMiladyProvisionOnce({ + agentId, + organizationId: user.organization_id, + userId: user.id, + agentName: agent.agent_name ?? agentId, + webhookUrl, + expectedUpdatedAt: agent.updated_at, + }); return applyCorsHeaders( NextResponse.json( @@ -188,7 +202,10 @@ export async function POST( : 500; return applyCorsHeaders( NextResponse.json( - { success: false, error: status === 500 ? "Failed to resume agent" : message }, + { + success: false, + error: status === 500 ? "Failed to resume agent" : message, + }, { status }, ), CORS_METHODS, diff --git a/app/api/v1/milady/agents/[agentId]/route.ts b/app/api/v1/milady/agents/[agentId]/route.ts index 642fe1c15..66cf90705 100644 --- a/app/api/v1/milady/agents/[agentId]/route.ts +++ b/app/api/v1/milady/agents/[agentId]/route.ts @@ -36,10 +36,16 @@ export async function GET( const { user } = await requireAuthOrApiKeyWithOrg(request); const { agentId } = await params; - const agent = await miladySandboxService.getAgent(agentId, user.organization_id); + const agent = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!agent) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -68,10 +74,13 @@ export async function GET( if (!tokenAddress) { const cfg = agent.agent_config as Record | null; tokenAddress = - typeof cfg?.tokenContractAddress === "string" ? cfg.tokenContractAddress : null; + typeof cfg?.tokenContractAddress === "string" + ? cfg.tokenContractAddress + : null; tokenChain = typeof cfg?.chain === "string" ? cfg.chain : null; tokenName = typeof cfg?.tokenName === "string" ? cfg.tokenName : null; - tokenTicker = typeof cfg?.tokenTicker === "string" ? cfg.tokenTicker : null; + tokenTicker = + typeof cfg?.tokenTicker === "string" ? cfg.tokenTicker : null; } // Resolve wallet info — Docker agents use Steward, others use Privy @@ -94,7 +103,10 @@ export async function GET( walletStatus = "pending"; } } catch (err) { - logger.warn(`[milady-api] Steward wallet lookup failed for ${agentId}`, { err }); + logger.warn( + `[milady-api] Steward wallet lookup failed for ${agentId}`, + { err }, + ); } } @@ -173,10 +185,16 @@ export async function PATCH( } if (parsed.data.action === "shutdown" || parsed.data.action === "suspend") { - const agent = await miladySandboxService.getAgentForWrite(agentId, user.organization_id); + const agent = await miladySandboxService.getAgentForWrite( + agentId, + user.organization_id, + ); if (!agent) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -199,7 +217,10 @@ export async function PATCH( ); } - const result = await miladySandboxService.shutdown(agentId, user.organization_id); + const result = await miladySandboxService.shutdown( + agentId, + user.organization_id, + ); if (!result.success) { const status = result.error === "Agent not found" @@ -242,7 +263,10 @@ export async function PATCH( } return applyCorsHeaders( - NextResponse.json({ success: false, error: "Unsupported action" }, { status: 400 }), + NextResponse.json( + { success: false, error: "Unsupported action" }, + { status: 400 }, + ), CORS_METHODS, ); } catch (error) { @@ -263,7 +287,10 @@ export async function DELETE( const { user } = await requireAuthOrApiKeyWithOrg(request); const { agentId } = await params; - const deleted = await miladySandboxService.deleteAgent(agentId, user.organization_id); + const deleted = await miladySandboxService.deleteAgent( + agentId, + user.organization_id, + ); if (!deleted.success) { const status = deleted.error === "Agent not found" @@ -278,8 +305,12 @@ export async function DELETE( } const characterId = deleted.deletedSandbox.character_id; - const sandboxConfig = deleted.deletedSandbox.agent_config as Record | null; - const reusesExistingCharacter = reusesExistingMiladyCharacter(sandboxConfig); + const sandboxConfig = deleted.deletedSandbox.agent_config as Record< + string, + unknown + > | null; + const reusesExistingCharacter = + reusesExistingMiladyCharacter(sandboxConfig); if (characterId && !reusesExistingCharacter) { try { @@ -289,11 +320,17 @@ export async function DELETE( characterId, }); } catch (characterErr) { - logger.warn("[milady-api] Failed to clean up linked character after delete", { - agentId, - characterId, - error: characterErr instanceof Error ? characterErr.message : String(characterErr), - }); + logger.warn( + "[milady-api] Failed to clean up linked character after delete", + { + agentId, + characterId, + error: + characterErr instanceof Error + ? characterErr.message + : String(characterErr), + }, + ); } } diff --git a/app/api/v1/milady/agents/[agentId]/snapshot/route.ts b/app/api/v1/milady/agents/[agentId]/snapshot/route.ts index e5d31abb5..b191d00fb 100644 --- a/app/api/v1/milady/agents/[agentId]/snapshot/route.ts +++ b/app/api/v1/milady/agents/[agentId]/snapshot/route.ts @@ -24,7 +24,11 @@ export async function POST( const { user } = await requireAuthOrApiKeyWithOrg(request); const { agentId } = await params; - const result = await miladySandboxService.snapshot(agentId, user.organization_id, "manual"); + const result = await miladySandboxService.snapshot( + agentId, + user.organization_id, + "manual", + ); if (!result.success) { return applyCorsHeaders( diff --git a/app/api/v1/milady/agents/[agentId]/stream/route.ts b/app/api/v1/milady/agents/[agentId]/stream/route.ts index 1450401b2..f0233dd9e 100644 --- a/app/api/v1/milady/agents/[agentId]/stream/route.ts +++ b/app/api/v1/milady/agents/[agentId]/stream/route.ts @@ -48,10 +48,16 @@ export async function POST( const parsed = streamRequestSchema.safeParse(body); if (!parsed.success) { return applyCorsHeaders( - new Response(JSON.stringify({ error: "Invalid request", details: parsed.error.issues }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }), + new Response( + JSON.stringify({ + error: "Invalid request", + details: parsed.error.issues, + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ), CORS_METHODS, ); } diff --git a/app/api/v1/milady/agents/[agentId]/suspend/route.ts b/app/api/v1/milady/agents/[agentId]/suspend/route.ts index bfb0293cf..2c2810e69 100644 --- a/app/api/v1/milady/agents/[agentId]/suspend/route.ts +++ b/app/api/v1/milady/agents/[agentId]/suspend/route.ts @@ -38,10 +38,16 @@ export async function POST( orgId: user.organization_id, }); - const agent = await miladySandboxService.getAgentForWrite(agentId, user.organization_id); + const agent = await miladySandboxService.getAgentForWrite( + agentId, + user.organization_id, + ); if (!agent) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -61,7 +67,10 @@ export async function POST( ); } - const result = await miladySandboxService.shutdown(agentId, user.organization_id); + const result = await miladySandboxService.shutdown( + agentId, + user.organization_id, + ); if (!result.success) { const status = @@ -71,7 +80,10 @@ export async function POST( ? 409 : 500; return applyCorsHeaders( - NextResponse.json({ success: false, error: result.error ?? "Suspend failed" }, { status }), + NextResponse.json( + { success: false, error: result.error ?? "Suspend failed" }, + { status }, + ), CORS_METHODS, ); } @@ -87,7 +99,8 @@ export async function POST( data: { agentId, action: "suspend", - message: "Agent suspended with snapshot. Use resume or provision to restart.", + message: + "Agent suspended with snapshot. Use resume or provision to restart.", previousStatus: agent.status, }, }), diff --git a/app/api/v1/milady/agents/[agentId]/wallet/route.ts b/app/api/v1/milady/agents/[agentId]/wallet/route.ts index ba16bb4d1..55a6aa542 100644 --- a/app/api/v1/milady/agents/[agentId]/wallet/route.ts +++ b/app/api/v1/milady/agents/[agentId]/wallet/route.ts @@ -36,10 +36,16 @@ export async function GET( const { agentId } = await params; // Verify the agent belongs to this user's org - const agent = await miladySandboxService.getAgent(agentId, user.organization_id); + const agent = await miladySandboxService.getAgent( + agentId, + user.organization_id, + ); if (!agent) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -51,7 +57,10 @@ export async function GET( where: eq(agentServerWallets.character_id, agent.character_id), }); if (walletRecord) { - privyWallet = { address: walletRecord.address, chain_type: walletRecord.chain_type }; + privyWallet = { + address: walletRecord.address, + chain_type: walletRecord.chain_type, + }; } } @@ -90,7 +99,9 @@ export async function GET( } // Steward unreachable — fall through to privy wallet if available - logger.warn(`[wallet-api] Steward unreachable for agent ${agentId}, falling back to DB`); + logger.warn( + `[wallet-api] Steward unreachable for agent ${agentId}, falling back to DB`, + ); } // Privy / DB fallback @@ -104,7 +115,10 @@ export async function GET( walletProvider: "privy", walletStatus: "active", balance: null, // Privy doesn't expose balance via our API - chain: privyWallet.chain_type === "evm" ? "base" : privyWallet.chain_type, + chain: + privyWallet.chain_type === "evm" + ? "base" + : privyWallet.chain_type, }, }), CORS_METHODS, diff --git a/app/api/v1/milady/agents/route.ts b/app/api/v1/milady/agents/route.ts index a972dcae1..decf7b2d7 100644 --- a/app/api/v1/milady/agents/route.ts +++ b/app/api/v1/milady/agents/route.ts @@ -39,11 +39,18 @@ export async function GET(request: NextRequest) { const agents = await miladySandboxService.listAgents(user.organization_id); const characterIds = Array.from( - new Set(agents.map((a) => a.character_id).filter((id): id is string => id != null)), + new Set( + agents + .map((a) => a.character_id) + .filter((id): id is string => id != null), + ), ); const characters = characterIds.length > 0 - ? await userCharactersRepository.findByIdsInOrganization(characterIds, user.organization_id) + ? await userCharactersRepository.findByIdsInOrganization( + characterIds, + user.organization_id, + ) : []; const charMap = new Map(characters.map((c) => [c.id, c])); @@ -66,10 +73,19 @@ export async function GET(request: NextRequest) { updatedAt: a.updated_at, // Canonical token linkage token_address: - char?.token_address ?? (cfg?.tokenContractAddress as string | undefined) ?? null, - token_chain: char?.token_chain ?? (cfg?.chain as string | undefined) ?? null, - token_name: char?.token_name ?? (cfg?.tokenName as string | undefined) ?? null, - token_ticker: char?.token_ticker ?? (cfg?.tokenTicker as string | undefined) ?? null, + char?.token_address ?? + (cfg?.tokenContractAddress as string | undefined) ?? + null, + token_chain: + char?.token_chain ?? (cfg?.chain as string | undefined) ?? null, + token_name: + char?.token_name ?? + (cfg?.tokenName as string | undefined) ?? + null, + token_ticker: + char?.token_ticker ?? + (cfg?.tokenTicker as string | undefined) ?? + null, }; }), }), @@ -128,10 +144,11 @@ export async function POST(request: NextRequest) { } if (parsed.data.characterId) { - const character = await userCharactersRepository.findByIdInOrganizationForWrite( - parsed.data.characterId, - user.organization_id, - ); + const character = + await userCharactersRepository.findByIdInOrganizationForWrite( + parsed.data.characterId, + user.organization_id, + ); if (!character) { return applyCorsHeaders( @@ -149,7 +166,9 @@ export async function POST(request: NextRequest) { // Strip reserved __milady* keys from user-supplied agentConfig to prevent // callers from spoofing internal lifecycle flags. - const sanitizedConfig = stripReservedMiladyConfigKeys(parsed.data.agentConfig); + const sanitizedConfig = stripReservedMiladyConfigKeys( + parsed.data.agentConfig, + ); const managedEnvironment = await prepareManagedMiladyEnvironment({ existingEnv: parsed.data.environmentVars, organizationId: user.organization_id, diff --git a/app/api/v1/milady/gateway-relay/sessions/[sessionId]/next/route.ts b/app/api/v1/milady/gateway-relay/sessions/[sessionId]/next/route.ts index cd8c015d1..059187026 100644 --- a/app/api/v1/milady/gateway-relay/sessions/[sessionId]/next/route.ts +++ b/app/api/v1/milady/gateway-relay/sessions/[sessionId]/next/route.ts @@ -25,11 +25,20 @@ export async function GET( const session = await miladyGatewayRelayService.getSession(sessionId); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } - if (session.organizationId !== user.organization_id || session.userId !== user.id) { - return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 }); + if ( + session.organizationId !== user.organization_id || + session.userId !== user.id + ) { + return NextResponse.json( + { success: false, error: "Forbidden" }, + { status: 403 }, + ); } const requestEnvelope = await miladyGatewayRelayService.pollNextRequest( diff --git a/app/api/v1/milady/gateway-relay/sessions/[sessionId]/responses/route.ts b/app/api/v1/milady/gateway-relay/sessions/[sessionId]/responses/route.ts index 84de2b469..9a555bfdd 100644 --- a/app/api/v1/milady/gateway-relay/sessions/[sessionId]/responses/route.ts +++ b/app/api/v1/milady/gateway-relay/sessions/[sessionId]/responses/route.ts @@ -33,18 +33,31 @@ export async function POST( const session = await miladyGatewayRelayService.getSession(sessionId); if (!session) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } - if (session.organizationId !== user.organization_id || session.userId !== user.id) { - return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 }); + if ( + session.organizationId !== user.organization_id || + session.userId !== user.id + ) { + return NextResponse.json( + { success: false, error: "Forbidden" }, + { status: 403 }, + ); } const body = await request.json().catch(() => ({})); const parsed = respondSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( - { success: false, error: "Invalid request", details: parsed.error.issues }, + { + success: false, + error: "Invalid request", + details: parsed.error.issues, + }, { status: 400 }, ); } @@ -56,7 +69,10 @@ export async function POST( }); if (!accepted) { - return NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ); } return NextResponse.json({ success: true }); diff --git a/app/api/v1/milady/gateway-relay/sessions/[sessionId]/route.ts b/app/api/v1/milady/gateway-relay/sessions/[sessionId]/route.ts index 211b80592..6aea83b7a 100644 --- a/app/api/v1/milady/gateway-relay/sessions/[sessionId]/route.ts +++ b/app/api/v1/milady/gateway-relay/sessions/[sessionId]/route.ts @@ -15,8 +15,14 @@ export async function DELETE( const session = await miladyGatewayRelayService.getSession(sessionId); if (session) { - if (session.organizationId !== user.organization_id || session.userId !== user.id) { - return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 }); + if ( + session.organizationId !== user.organization_id || + session.userId !== user.id + ) { + return NextResponse.json( + { success: false, error: "Forbidden" }, + { status: 403 }, + ); } await miladyGatewayRelayService.disconnectSession(sessionId); diff --git a/app/api/v1/milady/github-oauth-complete/route.ts b/app/api/v1/milady/github-oauth-complete/route.ts index dfca7c4ea..f4f714a0b 100644 --- a/app/api/v1/milady/github-oauth-complete/route.ts +++ b/app/api/v1/milady/github-oauth-complete/route.ts @@ -26,7 +26,8 @@ export const dynamic = "force-dynamic"; * 4. The connection is validated against the org_id before reading */ export async function GET(request: NextRequest): Promise { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const dashboardUrl = `${baseUrl}/dashboard/settings?tab=agents`; const agentId = request.nextUrl.searchParams.get("agent_id"); @@ -100,7 +101,13 @@ export async function GET(request: NextRequest): Promise { }); } - if (!agentId || !organizationId || !userId || !connectionId || githubConnected !== "true") { + if ( + !agentId || + !organizationId || + !userId || + !connectionId || + githubConnected !== "true" + ) { logger.warn("[managed-github] OAuth completion missing required params", { hasAgentId: !!agentId, hasOrgId: !!organizationId, @@ -116,7 +123,10 @@ export async function GET(request: NextRequest): Promise { try { // Validate agent belongs to the org that initiated the OAuth flow - const sandbox = await miladySandboxesRepository.findByIdAndOrg(agentId, organizationId); + const sandbox = await miladySandboxesRepository.findByIdAndOrg( + agentId, + organizationId, + ); if (!sandbox) { logger.error("[managed-github] Agent not found or org mismatch", { agentId, @@ -204,7 +214,10 @@ export async function GET(request: NextRequest): Promise { }); return respond({ status: "error", - message: error instanceof Error ? error.message : "Failed to link GitHub to agent", + message: + error instanceof Error + ? error.message + : "Failed to link GitHub to agent", }); } } diff --git a/app/api/v1/milady/google/accounts/route.ts b/app/api/v1/milady/google/accounts/route.ts index 42c101050..2db8731de 100644 --- a/app/api/v1/milady/google/accounts/route.ts +++ b/app/api/v1/milady/google/accounts/route.ts @@ -7,23 +7,36 @@ export const maxDuration = 30; export async function GET(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const rawSide = request.nextUrl.searchParams.get("side"); if (rawSide !== null && rawSide !== "owner" && rawSide !== "agent") { - return NextResponse.json({ error: "side must be owner or agent." }, { status: 400 }); + return NextResponse.json( + { error: "side must be owner or agent." }, + { status: 400 }, + ); } - const accounts = await miladyGoogleRouteDeps.listManagedGoogleConnectorAccounts({ - organizationId: user.organization_id, - userId: user.id, - side: rawSide === "owner" || rawSide === "agent" ? rawSide : undefined, - }); + const accounts = + await miladyGoogleRouteDeps.listManagedGoogleConnectorAccounts({ + organizationId: user.organization_id, + userId: user.id, + side: rawSide === "owner" || rawSide === "agent" ? rawSide : undefined, + }); return NextResponse.json(accounts); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to list Google accounts." }, + { + error: + error instanceof Error + ? error.message + : "Failed to list Google accounts.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/calendar/events/[eventId]/route.ts b/app/api/v1/milady/google/calendar/events/[eventId]/route.ts index c15200e2b..05f227a41 100644 --- a/app/api/v1/milady/google/calendar/events/[eventId]/route.ts +++ b/app/api/v1/milady/google/calendar/events/[eventId]/route.ts @@ -30,7 +30,8 @@ const patchRequestSchema = z.object({ export async function PATCH(request: NextRequest, { params }: RouteParams) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const { eventId } = await params; const parsed = patchRequestSchema.safeParse(await request.json()); if (!parsed.success) { @@ -61,11 +62,17 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to update Google Calendar event.", + error: + error instanceof Error + ? error.message + : "Failed to update Google Calendar event.", }, { status: 500 }, ); @@ -74,7 +81,8 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { export async function DELETE(request: NextRequest, { params }: RouteParams) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const { eventId } = await params; const sideRaw = request.nextUrl.searchParams.get("side"); const calendarIdRaw = request.nextUrl.searchParams.get("calendarId"); @@ -102,11 +110,17 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to delete Google Calendar event.", + error: + error instanceof Error + ? error.message + : "Failed to delete Google Calendar event.", }, { status: 500 }, ); diff --git a/app/api/v1/milady/google/calendar/events/route.ts b/app/api/v1/milady/google/calendar/events/route.ts index d74b4d772..8f7df3fd2 100644 --- a/app/api/v1/milady/google/calendar/events/route.ts +++ b/app/api/v1/milady/google/calendar/events/route.ts @@ -26,11 +26,15 @@ const requestSchema = z.object({ export async function POST(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const parsed = requestSchema.safeParse(await request.json()); if (!parsed.success) { return NextResponse.json( - { error: "Invalid calendar event request.", details: parsed.error.issues }, + { + error: "Invalid calendar event request.", + details: parsed.error.issues, + }, { status: 400 }, ); } @@ -53,10 +57,18 @@ export async function POST(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to create Google Calendar event." }, + { + error: + error instanceof Error + ? error.message + : "Failed to create Google Calendar event.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/calendar/feed/route.ts b/app/api/v1/milady/google/calendar/feed/route.ts index 4f9ef7513..268a93514 100644 --- a/app/api/v1/milady/google/calendar/feed/route.ts +++ b/app/api/v1/milady/google/calendar/feed/route.ts @@ -7,7 +7,8 @@ export const maxDuration = 30; export async function GET(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const searchParams = request.nextUrl.searchParams; const rawSide = searchParams.get("side"); const calendarId = searchParams.get("calendarId")?.trim() || "primary"; @@ -16,10 +17,16 @@ export async function GET(request: NextRequest) { const timeZone = searchParams.get("timeZone")?.trim() || "UTC"; if (rawSide !== null && rawSide !== "owner" && rawSide !== "agent") { - return NextResponse.json({ error: "side must be owner or agent." }, { status: 400 }); + return NextResponse.json( + { error: "side must be owner or agent." }, + { status: 400 }, + ); } if (!timeMin || !timeMax) { - return NextResponse.json({ error: "timeMin and timeMax are required." }, { status: 400 }); + return NextResponse.json( + { error: "timeMin and timeMax are required." }, + { status: 400 }, + ); } return NextResponse.json( @@ -35,10 +42,18 @@ export async function GET(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to fetch Google Calendar." }, + { + error: + error instanceof Error + ? error.message + : "Failed to fetch Google Calendar.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/connect/initiate/route.ts b/app/api/v1/milady/google/connect/initiate/route.ts index d620f94f9..b32f2f1a3 100644 --- a/app/api/v1/milady/google/connect/initiate/route.ts +++ b/app/api/v1/milady/google/connect/initiate/route.ts @@ -24,11 +24,17 @@ const requestSchema = z.object({ export async function POST(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); - const parsed = requestSchema.safeParse(await request.json().catch(() => ({}))); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const parsed = requestSchema.safeParse( + await request.json().catch(() => ({})), + ); if (!parsed.success) { return NextResponse.json( - { error: "Invalid Google connector request.", details: parsed.error.issues }, + { + error: "Invalid Google connector request.", + details: parsed.error.issues, + }, { status: 400 }, ); } @@ -43,10 +49,18 @@ export async function POST(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to initiate Google OAuth." }, + { + error: + error instanceof Error + ? error.message + : "Failed to initiate Google OAuth.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/disconnect/route.ts b/app/api/v1/milady/google/disconnect/route.ts index b31a9ef27..84f69f240 100644 --- a/app/api/v1/milady/google/disconnect/route.ts +++ b/app/api/v1/milady/google/disconnect/route.ts @@ -13,8 +13,11 @@ const requestSchema = z.object({ export async function POST(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); - const parsed = requestSchema.safeParse(await request.json().catch(() => ({}))); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const parsed = requestSchema.safeParse( + await request.json().catch(() => ({})), + ); if (!parsed.success) { return NextResponse.json( { error: "Invalid disconnect request.", details: parsed.error.issues }, @@ -31,10 +34,18 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: true }); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to disconnect Google." }, + { + error: + error instanceof Error + ? error.message + : "Failed to disconnect Google.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/gmail/message-send/route.ts b/app/api/v1/milady/google/gmail/message-send/route.ts index adcb15c46..f39f0411b 100644 --- a/app/api/v1/milady/google/gmail/message-send/route.ts +++ b/app/api/v1/milady/google/gmail/message-send/route.ts @@ -17,11 +17,15 @@ const requestSchema = z.object({ export async function POST(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const parsed = requestSchema.safeParse(await request.json()); if (!parsed.success) { return NextResponse.json( - { error: "Invalid Gmail message send request.", details: parsed.error.issues }, + { + error: "Invalid Gmail message send request.", + details: parsed.error.issues, + }, { status: 400 }, ); } @@ -39,10 +43,18 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: true }); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to send Gmail message." }, + { + error: + error instanceof Error + ? error.message + : "Failed to send Gmail message.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/gmail/read/route.ts b/app/api/v1/milady/google/gmail/read/route.ts index 82ea736e5..7112aeb8d 100644 --- a/app/api/v1/milady/google/gmail/read/route.ts +++ b/app/api/v1/milady/google/gmail/read/route.ts @@ -7,15 +7,22 @@ export const maxDuration = 30; export async function GET(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const rawSide = request.nextUrl.searchParams.get("side"); const messageId = request.nextUrl.searchParams.get("messageId"); if (rawSide !== null && rawSide !== "owner" && rawSide !== "agent") { - return NextResponse.json({ error: "side must be owner or agent." }, { status: 400 }); + return NextResponse.json( + { error: "side must be owner or agent." }, + { status: 400 }, + ); } if (!messageId || messageId.trim().length === 0) { - return NextResponse.json({ error: "messageId is required." }, { status: 400 }); + return NextResponse.json( + { error: "messageId is required." }, + { status: 400 }, + ); } return NextResponse.json( @@ -28,10 +35,18 @@ export async function GET(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to read Gmail message." }, + { + error: + error instanceof Error + ? error.message + : "Failed to read Gmail message.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/gmail/reply-send/route.ts b/app/api/v1/milady/google/gmail/reply-send/route.ts index 99f1aae39..e6efce595 100644 --- a/app/api/v1/milady/google/gmail/reply-send/route.ts +++ b/app/api/v1/milady/google/gmail/reply-send/route.ts @@ -18,7 +18,8 @@ const requestSchema = z.object({ export async function POST(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const parsed = requestSchema.safeParse(await request.json()); if (!parsed.success) { return NextResponse.json( @@ -41,10 +42,18 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: true }); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to send Gmail reply." }, + { + error: + error instanceof Error + ? error.message + : "Failed to send Gmail reply.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/gmail/search/route.ts b/app/api/v1/milady/google/gmail/search/route.ts index 9dd7d5776..7fcd2cecb 100644 --- a/app/api/v1/milady/google/gmail/search/route.ts +++ b/app/api/v1/milady/google/gmail/search/route.ts @@ -7,19 +7,28 @@ export const maxDuration = 30; export async function GET(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const rawSide = request.nextUrl.searchParams.get("side"); const rawQuery = request.nextUrl.searchParams.get("query"); const rawMaxResults = request.nextUrl.searchParams.get("maxResults"); if (rawSide !== null && rawSide !== "owner" && rawSide !== "agent") { - return NextResponse.json({ error: "side must be owner or agent." }, { status: 400 }); + return NextResponse.json( + { error: "side must be owner or agent." }, + { status: 400 }, + ); } const query = rawQuery?.trim() ?? ""; if (query.length === 0) { - return NextResponse.json({ error: "query is required." }, { status: 400 }); + return NextResponse.json( + { error: "query is required." }, + { status: 400 }, + ); } const maxResults = - rawMaxResults && rawMaxResults.trim().length > 0 ? Number.parseInt(rawMaxResults, 10) : 12; + rawMaxResults && rawMaxResults.trim().length > 0 + ? Number.parseInt(rawMaxResults, 10) + : 12; if (!Number.isFinite(maxResults) || maxResults <= 0) { return NextResponse.json( { error: "maxResults must be a positive integer." }, @@ -38,10 +47,16 @@ export async function GET(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to search Gmail." }, + { + error: + error instanceof Error ? error.message : "Failed to search Gmail.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/gmail/triage/route.ts b/app/api/v1/milady/google/gmail/triage/route.ts index 2179d9dda..e3b4a4051 100644 --- a/app/api/v1/milady/google/gmail/triage/route.ts +++ b/app/api/v1/milady/google/gmail/triage/route.ts @@ -7,14 +7,20 @@ export const maxDuration = 30; export async function GET(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const rawSide = request.nextUrl.searchParams.get("side"); const rawMaxResults = request.nextUrl.searchParams.get("maxResults"); if (rawSide !== null && rawSide !== "owner" && rawSide !== "agent") { - return NextResponse.json({ error: "side must be owner or agent." }, { status: 400 }); + return NextResponse.json( + { error: "side must be owner or agent." }, + { status: 400 }, + ); } const maxResults = - rawMaxResults && rawMaxResults.trim().length > 0 ? Number.parseInt(rawMaxResults, 10) : 12; + rawMaxResults && rawMaxResults.trim().length > 0 + ? Number.parseInt(rawMaxResults, 10) + : 12; if (!Number.isFinite(maxResults) || maxResults <= 0) { return NextResponse.json( { error: "maxResults must be a positive integer." }, @@ -32,10 +38,18 @@ export async function GET(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to fetch Gmail triage." }, + { + error: + error instanceof Error + ? error.message + : "Failed to fetch Gmail triage.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/google/status/route.ts b/app/api/v1/milady/google/status/route.ts index e955029e0..08e9765c4 100644 --- a/app/api/v1/milady/google/status/route.ts +++ b/app/api/v1/milady/google/status/route.ts @@ -7,10 +7,14 @@ export const maxDuration = 30; export async function GET(request: NextRequest) { try { - const { user } = await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); + const { user } = + await miladyGoogleRouteDeps.requireAuthOrApiKeyWithOrg(request); const rawSide = request.nextUrl.searchParams.get("side"); if (rawSide !== null && rawSide !== "owner" && rawSide !== "agent") { - return NextResponse.json({ error: "side must be owner or agent." }, { status: 400 }); + return NextResponse.json( + { error: "side must be owner or agent." }, + { status: 400 }, + ); } return NextResponse.json( await miladyGoogleRouteDeps.getManagedGoogleConnectorStatus({ @@ -21,10 +25,18 @@ export async function GET(request: NextRequest) { ); } catch (error) { if (error instanceof miladyGoogleRouteDeps.MiladyGoogleConnectorError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to resolve Google status." }, + { + error: + error instanceof Error + ? error.message + : "Failed to resolve Google status.", + }, { status: 500 }, ); } diff --git a/app/api/v1/milady/lifeops/github-complete/route.ts b/app/api/v1/milady/lifeops/github-complete/route.ts index a9e2700d6..9bddf3013 100644 --- a/app/api/v1/milady/lifeops/github-complete/route.ts +++ b/app/api/v1/milady/lifeops/github-complete/route.ts @@ -4,7 +4,8 @@ import { createLifeOpsGithubReturnResponse } from "@/lib/services/milady-github- export const dynamic = "force-dynamic"; export async function GET(request: NextRequest): Promise { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const githubConnected = request.nextUrl.searchParams.get("github_connected"); const githubError = request.nextUrl.searchParams.get("github_error"); const connectionId = request.nextUrl.searchParams.get("connection_id"); @@ -34,7 +35,9 @@ export async function GET(request: NextRequest): Promise { returnUrl, }); } - return NextResponse.redirect(`${dashboardUrl}&github_error=${encodeURIComponent(githubError)}`); + return NextResponse.redirect( + `${dashboardUrl}&github_error=${encodeURIComponent(githubError)}`, + ); } if (githubConnected !== "true" || !connectionId) { @@ -57,12 +60,17 @@ export async function GET(request: NextRequest): Promise { returnUrl, }); } - return NextResponse.redirect(`${dashboardUrl}&github_error=${encodeURIComponent(message)}`); + return NextResponse.redirect( + `${dashboardUrl}&github_error=${encodeURIComponent(message)}`, + ); } if (postMessage || returnUrl) { return createLifeOpsGithubReturnResponse({ - title: target === "agent" ? "Agent GitHub connected" : "LifeOps GitHub connected", + title: + target === "agent" + ? "Agent GitHub connected" + : "LifeOps GitHub connected", message: target === "agent" ? "GitHub is connected and ready to link to this agent." diff --git a/app/api/v1/models/[...model]/route.ts b/app/api/v1/models/[...model]/route.ts index 291cc162a..91ef53c60 100644 --- a/app/api/v1/models/[...model]/route.ts +++ b/app/api/v1/models/[...model]/route.ts @@ -3,7 +3,10 @@ import type { NextRequest } from "next/server"; import { requireAuthOrApiKey } from "@/lib/auth"; import { getGroqCatalogModel, isGroqNativeModel } from "@/lib/models"; -import { getProviderForModel, hasGroqProviderConfigured } from "@/lib/providers"; +import { + getProviderForModel, + hasGroqProviderConfigured, +} from "@/lib/providers"; import { getCachedGatewayModelById } from "@/lib/services/model-catalog"; import { logger } from "@/lib/utils/logger"; @@ -18,7 +21,10 @@ export const dynamic = "force-dynamic"; * @param context - Route context containing model segments as an array. * @returns Model details from the provider gateway. */ -export async function GET(request: NextRequest, context: { params: Promise<{ model: string[] }> }) { +export async function GET( + request: NextRequest, + context: { params: Promise<{ model: string[] }> }, +) { try { await requireAuthOrApiKey(request); @@ -68,10 +74,13 @@ export async function GET(request: NextRequest, context: { params: Promise<{ mod return Response.json(cachedModel); } } catch (error) { - logger.warn("Error reading cached model catalog, falling back to provider", { - model, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "Error reading cached model catalog, falling back to provider", + { + model, + error: error instanceof Error ? error.message : String(error), + }, + ); } const provider = getProviderForModel(model); diff --git a/app/api/v1/models/status/route.ts b/app/api/v1/models/status/route.ts index 3f6046a53..690da0f41 100644 --- a/app/api/v1/models/status/route.ts +++ b/app/api/v1/models/status/route.ts @@ -77,26 +77,40 @@ export async function POST(request: NextRequest) { const { modelIds } = body as { modelIds: string[] }; if (!Array.isArray(modelIds) || modelIds.length === 0) { - return Response.json({ error: "modelIds array is required" }, { status: 400 }); + return Response.json( + { error: "modelIds array is required" }, + { status: 400 }, + ); } if (modelIds.length > 50) { - return Response.json({ error: "Maximum 50 models can be checked at once" }, { status: 400 }); + return Response.json( + { error: "Maximum 50 models can be checked at once" }, + { status: 400 }, + ); } // Validate each modelId is a non-empty string if (!modelIds.every((id) => typeof id === "string" && id.length > 0)) { - return Response.json({ error: "Each modelId must be a non-empty string" }, { status: 400 }); + return Response.json( + { error: "Each modelId must be a non-empty string" }, + { status: 400 }, + ); } const gatewayConfigured = hasGatewayProviderConfigured(); const groqConfigured = hasGroqProviderConfigured(); if (!hasAnyAiProviderConfigured()) { - return Response.json({ error: getAiProviderConfigurationError() }, { status: 503 }); + return Response.json( + { error: getAiProviderConfigurationError() }, + { status: 503 }, + ); } - const gatewayModelIds = new Set((await getCachedMergedModelCatalog()).map((model) => model.id)); + const gatewayModelIds = new Set( + (await getCachedMergedModelCatalog()).map((model) => model.id), + ); // Check availability for each requested model const results: ModelAvailability[] = modelIds.map((modelId) => { @@ -114,7 +128,9 @@ export async function POST(request: NextRequest) { return { modelId, available: groqConfigured, - reason: groqConfigured ? undefined : "Groq models are not configured on this deployment", + reason: groqConfigured + ? undefined + : "Groq models are not configured on this deployment", }; } @@ -154,6 +170,9 @@ export async function POST(request: NextRequest) { }); } catch (error) { logger.error("Error fetching model status:", error); - return Response.json({ error: "Failed to check model status" }, { status: 500 }); + return Response.json( + { error: "Failed to check model status" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/oauth/[platform]/initiate/route.ts b/app/api/v1/oauth/[platform]/initiate/route.ts index 1ed981f47..8e22729c1 100644 --- a/app/api/v1/oauth/[platform]/initiate/route.ts +++ b/app/api/v1/oauth/[platform]/initiate/route.ts @@ -19,7 +19,10 @@ import { LOOPBACK_REDIRECT_ORIGINS, } from "@/lib/security/redirect-validation"; import { OAuthError } from "@/lib/services/oauth"; -import { getProvider, isProviderConfigured } from "@/lib/services/oauth/provider-registry"; +import { + getProvider, + isProviderConfigured, +} from "@/lib/services/oauth/provider-registry"; import { initiateOAuth2 } from "@/lib/services/oauth/providers"; import { logger } from "@/lib/utils/logger"; @@ -38,9 +41,15 @@ interface RouteParams { }>; } -async function handleInitiate(request: NextRequest, context: RouteParams): Promise { +async function handleInitiate( + request: NextRequest, + context: RouteParams, +): Promise { if (!context) { - return NextResponse.json({ error: "Missing route params" }, { status: 400 }); + return NextResponse.json( + { error: "Missing route params" }, + { status: 400 }, + ); } const { platform } = await context.params; const platformLower = platform.toLowerCase(); @@ -106,7 +115,8 @@ async function handleInitiate(request: NextRequest, context: RouteParams): Promi // Empty body is fine, use defaults } - const redirectUrl = body.redirectUrl || "/dashboard/settings?tab=connections"; + const redirectUrl = + body.redirectUrl || "/dashboard/settings?tab=connections"; if (redirectUrl.startsWith("http")) { const allowedAbsoluteOrigins = [ ...getDefaultPlatformRedirectOrigins(), @@ -182,7 +192,9 @@ async function handleInitiate(request: NextRequest, context: RouteParams): Promi } if (error instanceof OAuthError) { - return NextResponse.json(error.toResponse(), { status: error.httpStatus }); + return NextResponse.json(error.toResponse(), { + status: error.httpStatus, + }); } return NextResponse.json( diff --git a/app/api/v1/oauth/callback/route.ts b/app/api/v1/oauth/callback/route.ts index 6be701bdb..b84106be2 100644 --- a/app/api/v1/oauth/callback/route.ts +++ b/app/api/v1/oauth/callback/route.ts @@ -9,7 +9,10 @@ export async function GET(request: NextRequest): Promise { const provider = request.nextUrl.searchParams.get("provider")?.toLowerCase(); if (!provider) { - return NextResponse.json({ error: "provider query parameter is required" }, { status: 400 }); + return NextResponse.json( + { error: "provider query parameter is required" }, + { status: 400 }, + ); } return handleGenericOAuthCallback(request, { diff --git a/app/api/v1/oauth/connect/route.ts b/app/api/v1/oauth/connect/route.ts index a97b55e66..39d12dba5 100644 --- a/app/api/v1/oauth/connect/route.ts +++ b/app/api/v1/oauth/connect/route.ts @@ -41,12 +41,16 @@ export async function POST(request: NextRequest) { try { body = (await request.json()) as ConnectRequestBody; } catch { - return NextResponse.json(validationErrorResponse("Invalid JSON body"), { status: 400 }); + return NextResponse.json(validationErrorResponse("Invalid JSON body"), { + status: 400, + }); } if (!isValidString(body.platform)) { return NextResponse.json( - validationErrorResponse("platform is required and must be a non-empty string"), + validationErrorResponse( + "platform is required and must be a non-empty string", + ), { status: 400 }, ); } @@ -82,7 +86,9 @@ export async function POST(request: NextRequest) { } if (error instanceof OAuthError) { - return NextResponse.json(error.toResponse(), { status: error.httpStatus }); + return NextResponse.json(error.toResponse(), { + status: error.httpStatus, + }); } return NextResponse.json(internalErrorResponse(), { status: 500 }); diff --git a/app/api/v1/oauth/connections/[id]/route.ts b/app/api/v1/oauth/connections/[id]/route.ts index 6f94f9a13..380da6fb0 100644 --- a/app/api/v1/oauth/connections/[id]/route.ts +++ b/app/api/v1/oauth/connections/[id]/route.ts @@ -6,7 +6,12 @@ import { NextRequest, NextResponse } from "next/server"; import { ApiError } from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { Errors, internalErrorResponse, OAuthError, oauthService } from "@/lib/services/oauth"; +import { + Errors, + internalErrorResponse, + OAuthError, + oauthService, +} from "@/lib/services/oauth"; import { invalidateOAuthState } from "@/lib/services/oauth/invalidation"; import { logger } from "@/lib/utils/logger"; @@ -18,11 +23,19 @@ async function getAccessibleConnection( userId: string, connectionId: string, ) { - const connections = await oauthService.listConnections({ organizationId, userId }); - return connections.find((connection) => connection.id === connectionId) || null; + const connections = await oauthService.listConnections({ + organizationId, + userId, + }); + return ( + connections.find((connection) => connection.id === connectionId) || null + ); } -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { const { id: connectionId } = await params; let organizationId: string | undefined; @@ -35,7 +48,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ connectionId, }); - const connection = await getAccessibleConnection(organizationId, user.id, connectionId); + const connection = await getAccessibleConnection( + organizationId, + user.id, + connectionId, + ); if (!connection) { const error = Errors.connectionNotFound(connectionId); @@ -61,10 +78,15 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } if (error instanceof OAuthError) { - return NextResponse.json(error.toResponse(), { status: error.httpStatus }); + return NextResponse.json(error.toResponse(), { + status: error.httpStatus, + }); } - return NextResponse.json(internalErrorResponse("Failed to get connection"), { status: 500 }); + return NextResponse.json( + internalErrorResponse("Failed to get connection"), + { status: 500 }, + ); } } @@ -86,7 +108,11 @@ export async function DELETE( connectionId, }); - const connection = await getAccessibleConnection(organizationId, userId, connectionId); + const connection = await getAccessibleConnection( + organizationId, + userId, + connectionId, + ); if (!connection) { const error = Errors.connectionNotFound(connectionId); return NextResponse.json(error.toResponse(), { status: 404 }); @@ -97,7 +123,9 @@ export async function DELETE( connectionId: connection.id, }); - await invalidateOAuthState(organizationId, "oauth", userId, { skipVersionBump: true }); + await invalidateOAuthState(organizationId, "oauth", userId, { + skipVersionBump: true, + }); return NextResponse.json({ success: true }); } catch (error) { @@ -112,9 +140,14 @@ export async function DELETE( } if (error instanceof OAuthError) { - return NextResponse.json(error.toResponse(), { status: error.httpStatus }); + return NextResponse.json(error.toResponse(), { + status: error.httpStatus, + }); } - return NextResponse.json(internalErrorResponse("Failed to revoke connection"), { status: 500 }); + return NextResponse.json( + internalErrorResponse("Failed to revoke connection"), + { status: 500 }, + ); } } diff --git a/app/api/v1/oauth/connections/route.ts b/app/api/v1/oauth/connections/route.ts index bae2ba40e..210da1846 100644 --- a/app/api/v1/oauth/connections/route.ts +++ b/app/api/v1/oauth/connections/route.ts @@ -7,7 +7,11 @@ import { NextRequest, NextResponse } from "next/server"; import { ApiError } from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; -import { internalErrorResponse, OAuthError, oauthService } from "@/lib/services/oauth"; +import { + internalErrorResponse, + OAuthError, + oauthService, +} from "@/lib/services/oauth"; import { logger } from "@/lib/utils/logger"; export const dynamic = "force-dynamic"; @@ -18,7 +22,9 @@ export async function GET(request: NextRequest) { const platform = searchParams.get("platform") || undefined; const rawConnectionRole = searchParams.get("connectionRole"); const connectionRole = - rawConnectionRole === "owner" || rawConnectionRole === "agent" ? rawConnectionRole : undefined; + rawConnectionRole === "owner" || rawConnectionRole === "agent" + ? rawConnectionRole + : undefined; let organizationId: string | undefined; if (rawConnectionRole && !connectionRole) { @@ -68,11 +74,16 @@ export async function GET(request: NextRequest) { } if (error instanceof OAuthError) { - return NextResponse.json(error.toResponse(), { status: error.httpStatus }); + return NextResponse.json(error.toResponse(), { + status: error.httpStatus, + }); } - return NextResponse.json(internalErrorResponse("Failed to list OAuth connections"), { - status: 500, - }); + return NextResponse.json( + internalErrorResponse("Failed to list OAuth connections"), + { + status: 500, + }, + ); } } diff --git a/app/api/v1/oauth/generic-callback.ts b/app/api/v1/oauth/generic-callback.ts index 083fffcf1..1cdb011af 100644 --- a/app/api/v1/oauth/generic-callback.ts +++ b/app/api/v1/oauth/generic-callback.ts @@ -10,7 +10,10 @@ import { import { connectionEnforcementService } from "@/lib/services/eliza-app"; import { entitySettingsCache } from "@/lib/services/entity-settings/cache"; import { incrementOAuthVersion } from "@/lib/services/oauth/cache-version"; -import { getProvider, isProviderConfigured } from "@/lib/services/oauth/provider-registry"; +import { + getProvider, + isProviderConfigured, +} from "@/lib/services/oauth/provider-registry"; import { handleOAuth2Callback } from "@/lib/services/oauth/providers"; import { logger } from "@/lib/utils/logger"; @@ -26,7 +29,8 @@ export async function handleGenericOAuthCallback( const platformLower = platform.toLowerCase(); const searchParams = request.nextUrl.searchParams; - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const defaultRedirect = `${baseUrl}/dashboard/settings?tab=connections`; // Get provider configuration @@ -34,19 +38,25 @@ export async function handleGenericOAuthCallback( if (!provider) { logger.error(`[OAuth ${platform}] Unknown platform in callback`); - return NextResponse.redirect(appendParam(defaultRedirect, `oauth_error=unknown_platform`)); + return NextResponse.redirect( + appendParam(defaultRedirect, `oauth_error=unknown_platform`), + ); } // Check if provider uses generic routes if (!provider.useGenericRoutes) { logger.error(`[OAuth ${platform}] Callback received for legacy provider`); - return NextResponse.redirect(appendParam(defaultRedirect, `oauth_error=legacy_provider`)); + return NextResponse.redirect( + appendParam(defaultRedirect, `oauth_error=legacy_provider`), + ); } // Check if provider is configured if (!isProviderConfigured(provider)) { logger.error(`[OAuth ${platform}] Provider not configured in callback`); - return NextResponse.redirect(appendParam(defaultRedirect, `oauth_error=not_configured`)); + return NextResponse.redirect( + appendParam(defaultRedirect, `oauth_error=not_configured`), + ); } // Handle OAuth errors from provider @@ -69,7 +79,9 @@ export async function handleGenericOAuthCallback( if (!code || !state) { logger.error(`[OAuth ${platform}] Missing code or state in callback`); - return NextResponse.redirect(appendParam(defaultRedirect, `${platform}_error=missing_params`)); + return NextResponse.redirect( + appendParam(defaultRedirect, `${platform}_error=missing_params`), + ); } logger.info(`[OAuth ${platform}] Processing callback`, { @@ -85,19 +97,27 @@ export async function handleGenericOAuthCallback( ...LOOPBACK_REDIRECT_ORIGINS, ]; - const { target: redirectTarget, rejected } = resolveOAuthSuccessRedirectUrl({ - value: result.redirectUrl, - baseUrl, - fallbackPath: "/dashboard/settings?tab=connections", - allowedAbsoluteOrigins, - }); + const { target: redirectTarget, rejected } = resolveOAuthSuccessRedirectUrl( + { + value: result.redirectUrl, + baseUrl, + fallbackPath: "/dashboard/settings?tab=connections", + allowedAbsoluteOrigins, + }, + ); if (rejected) { - logger.error(`[OAuth ${platform}] SECURITY: Invalid redirect URL attempted`, { - redirectUrl: result.redirectUrl, - organizationId: result.organizationId, - ip: request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown", - }); + logger.error( + `[OAuth ${platform}] SECURITY: Invalid redirect URL attempted`, + { + redirectUrl: result.redirectUrl, + organizationId: result.organizationId, + ip: + request.headers.get("x-forwarded-for") || + request.headers.get("x-real-ip") || + "unknown", + }, + ); } // Add success parameters @@ -136,7 +156,11 @@ export async function handleGenericOAuthCallback( }); const errorMessage = - error instanceof Error ? encodeURIComponent(error.message) : "callback_failed"; - return NextResponse.redirect(appendParam(defaultRedirect, `${platform}_error=${errorMessage}`)); + error instanceof Error + ? encodeURIComponent(error.message) + : "callback_failed"; + return NextResponse.redirect( + appendParam(defaultRedirect, `${platform}_error=${errorMessage}`), + ); } } diff --git a/app/api/v1/oauth/initiate/route.ts b/app/api/v1/oauth/initiate/route.ts index 4171326f9..86fc65fc9 100644 --- a/app/api/v1/oauth/initiate/route.ts +++ b/app/api/v1/oauth/initiate/route.ts @@ -9,7 +9,10 @@ async function handle(request: NextRequest): Promise { const provider = request.nextUrl.searchParams.get("provider")?.toLowerCase(); if (!provider) { - return NextResponse.json({ error: "provider query parameter is required" }, { status: 400 }); + return NextResponse.json( + { error: "provider query parameter is required" }, + { status: 400 }, + ); } return initiateOAuth(request, { diff --git a/app/api/v1/oauth/status/route.ts b/app/api/v1/oauth/status/route.ts index 34366226e..fdeb0e137 100644 --- a/app/api/v1/oauth/status/route.ts +++ b/app/api/v1/oauth/status/route.ts @@ -31,7 +31,9 @@ async function getGoogleStatus( return { id: "google", name: "Google", - connected: connections.some((connection) => connection.status === "active"), + connected: connections.some( + (connection) => connection.status === "active", + ), }; } catch (error) { return { @@ -43,9 +45,12 @@ async function getGoogleStatus( } } -async function getTwilioStatus(organizationId: string): Promise { +async function getTwilioStatus( + organizationId: string, +): Promise { try { - const status = await twilioAutomationService.getConnectionStatus(organizationId); + const status = + await twilioAutomationService.getConnectionStatus(organizationId); return { id: "twilio", @@ -62,9 +67,12 @@ async function getTwilioStatus(organizationId: string): Promise { +async function getBlooioStatus( + organizationId: string, +): Promise { try { - const status = await blooioAutomationService.getConnectionStatus(organizationId); + const status = + await blooioAutomationService.getConnectionStatus(organizationId); return { id: "blooio", @@ -105,6 +113,9 @@ export async function GET(request: NextRequest): Promise { return NextResponse.json(error.toJSON(), { status: error.status }); } - return NextResponse.json({ error: "Failed to fetch OAuth status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch OAuth status" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/pricing/summary/route.ts b/app/api/v1/pricing/summary/route.ts index c3bf59c02..61d533d20 100644 --- a/app/api/v1/pricing/summary/route.ts +++ b/app/api/v1/pricing/summary/route.ts @@ -59,17 +59,24 @@ export async function GET() { ); const ttsCosts = await Promise.all( - ["elevenlabs/eleven_flash_v2_5", "elevenlabs/eleven_multilingual_v2"].map(async (model) => { - const cost = await calculateTTSCostFromCatalog({ model, characterCount: 1000 }); - return cost.totalCost; - }), + ["elevenlabs/eleven_flash_v2_5", "elevenlabs/eleven_multilingual_v2"].map( + async (model) => { + const cost = await calculateTTSCostFromCatalog({ + model, + characterCount: 1000, + }); + return cost.totalCost; + }, + ), ); const sttCost = await calculateSTTCostFromCatalog({ model: "elevenlabs/scribe_v1", durationSeconds: 60, }); - const instantClone = await calculateVoiceCloneCostFromCatalog({ cloneType: "instant" }); + const instantClone = await calculateVoiceCloneCostFromCatalog({ + cloneType: "instant", + }); const professionalClone = await calculateVoiceCloneCostFromCatalog({ cloneType: "professional", }); diff --git a/app/api/v1/proxy/birdeye/[...path]/route.ts b/app/api/v1/proxy/birdeye/[...path]/route.ts index 9bf07cd6d..7653373db 100644 --- a/app/api/v1/proxy/birdeye/[...path]/route.ts +++ b/app/api/v1/proxy/birdeye/[...path]/route.ts @@ -47,7 +47,10 @@ export async function GET( if (deductResult === null) { return Response.json( - { error: "Insufficient credits", topUpUrl: "https://www.elizacloud.ai/dashboard/billing" }, + { + error: "Insufficient credits", + topUpUrl: "https://www.elizacloud.ai/dashboard/billing", + }, { status: 402 }, ); } @@ -73,7 +76,8 @@ export async function GET( return new Response(body, { status: upstreamResponse.status, headers: { - "Content-Type": upstreamResponse.headers.get("Content-Type") ?? "application/json", + "Content-Type": + upstreamResponse.headers.get("Content-Type") ?? "application/json", }, }); } diff --git a/app/api/v1/proxy/evm-rpc/[chain]/route.ts b/app/api/v1/proxy/evm-rpc/[chain]/route.ts index 1b0ef2673..2d3418339 100644 --- a/app/api/v1/proxy/evm-rpc/[chain]/route.ts +++ b/app/api/v1/proxy/evm-rpc/[chain]/route.ts @@ -65,7 +65,11 @@ async function postHandler( // Support auth via query param for RPC clients that cannot set headers. const queryApiKey = request.nextUrl.searchParams.get("api_key"); let authRequest = request; - if (queryApiKey && !request.headers.get("authorization") && !request.headers.get("X-API-Key")) { + if ( + queryApiKey && + !request.headers.get("authorization") && + !request.headers.get("X-API-Key") + ) { const headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${queryApiKey}`); authRequest = new NextRequest(request.url, { @@ -127,7 +131,8 @@ async function postHandler( } if (requestCount > 0) { - const totalCost = proxyBillingService.getProxyCost("evm-rpc") * requestCount; + const totalCost = + proxyBillingService.getProxyCost("evm-rpc") * requestCount; const ok = await creditsService .deductCredits({ organizationId: organization_id, diff --git a/app/api/v1/proxy/solana-rpc/route.ts b/app/api/v1/proxy/solana-rpc/route.ts index a8f26548a..f3d79337a 100644 --- a/app/api/v1/proxy/solana-rpc/route.ts +++ b/app/api/v1/proxy/solana-rpc/route.ts @@ -20,7 +20,11 @@ export async function POST(request: NextRequest) { // Support auth via query param for @solana/web3.js Connection which can't send headers const queryApiKey = request.nextUrl.searchParams.get("api_key"); let authRequest = request; - if (queryApiKey && !request.headers.get("authorization") && !request.headers.get("X-API-Key")) { + if ( + queryApiKey && + !request.headers.get("authorization") && + !request.headers.get("X-API-Key") + ) { const headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${queryApiKey}`); authRequest = new NextRequest(request.url, { @@ -51,7 +55,8 @@ export async function POST(request: NextRequest) { } if (requestCount > 0) { - const totalCost = proxyBillingService.getProxyCost("solana-rpc") * requestCount; + const totalCost = + proxyBillingService.getProxyCost("solana-rpc") * requestCount; const ok = await creditsService .deductCredits({ organizationId: organization_id, @@ -67,7 +72,10 @@ export async function POST(request: NextRequest) { .catch(() => ({ success: false })); if (!ok.success) { return Response.json( - { error: "Insufficient credits", topUpUrl: "https://www.elizacloud.ai/dashboard/billing" }, + { + error: "Insufficient credits", + topUpUrl: "https://www.elizacloud.ai/dashboard/billing", + }, { status: 402 }, ); } diff --git a/app/api/v1/redemptions/[id]/route.ts b/app/api/v1/redemptions/[id]/route.ts index d3ca384b2..0132c34e6 100644 --- a/app/api/v1/redemptions/[id]/route.ts +++ b/app/api/v1/redemptions/[id]/route.ts @@ -19,14 +19,23 @@ async function getRedemptionHandler( ): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); if (!context) { - return NextResponse.json({ success: false, error: "Missing route params" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Missing route params" }, + { status: 400 }, + ); } const { id } = await context.params; - const redemption = await secureTokenRedemptionService.getRedemption(id, user.id); + const redemption = await secureTokenRedemptionService.getRedemption( + id, + user.id, + ); if (!redemption) { - return NextResponse.json({ success: false, error: "Redemption not found" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Redemption not found" }, + { status: 404 }, + ); } return NextResponse.json({ @@ -54,7 +63,10 @@ async function getRedemptionHandler( }); } -export const GET = withRateLimit(getRedemptionHandler, RateLimitPresets.STANDARD); +export const GET = withRateLimit( + getRedemptionHandler, + RateLimitPresets.STANDARD, +); /** * OPTIONS - CORS preflight @@ -65,7 +77,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/redemptions/balance/route.ts b/app/api/v1/redemptions/balance/route.ts index 577b561a4..21550e7dd 100644 --- a/app/api/v1/redemptions/balance/route.ts +++ b/app/api/v1/redemptions/balance/route.ts @@ -21,7 +21,10 @@ import { and, desc, eq, sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; import { dbRead } from "@/db/client"; -import { redeemableEarnings, redeemableEarningsLedger } from "@/db/schemas/redeemable-earnings"; +import { + redeemableEarnings, + redeemableEarningsLedger, +} from "@/db/schemas/redeemable-earnings"; import { tokenRedemptions } from "@/db/schemas/token-redemptions"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { SUPPLY_SHOCK_PROTECTION } from "@/lib/config/redemption-security"; @@ -144,7 +147,9 @@ async function getBalanceHandler(request: NextRequest): Promise { ? lastRedemption.created_at.getTime() : new Date(lastRedemption.created_at).getTime() : null; - const cooldownEndsAt = lastRedemptionTime ? new Date(lastRedemptionTime + cooldownMs) : null; + const cooldownEndsAt = lastRedemptionTime + ? new Date(lastRedemptionTime + cooldownMs) + : null; const isInCooldown = cooldownEndsAt && cooldownEndsAt > new Date(); // Check daily limit @@ -159,17 +164,27 @@ async function getBalanceHandler(request: NextRequest): Promise { AND created_at >= ${todayStart} `); - const dailyRedeemed = Number((dailyRedeemedResult.rows[0] as { total: string })?.total || 0); + const dailyRedeemed = Number( + (dailyRedeemedResult.rows[0] as { total: string })?.total || 0, + ); const dailyLimitRemaining = Math.max( 0, SUPPLY_SHOCK_PROTECTION.USER_DAILY_LIMIT_USD - dailyRedeemed, ); // Build balance - const availableBalance = earningsRecord ? Number(earningsRecord.available_balance) : 0; - const pendingBalance = earningsRecord ? Number(earningsRecord.total_pending) : 0; - const totalEarned = earningsRecord ? Number(earningsRecord.total_earned) : 0; - const totalPending = earningsRecord ? Number(earningsRecord.total_pending) : 0; + const availableBalance = earningsRecord + ? Number(earningsRecord.available_balance) + : 0; + const pendingBalance = earningsRecord + ? Number(earningsRecord.total_pending) + : 0; + const totalEarned = earningsRecord + ? Number(earningsRecord.total_earned) + : 0; + const totalPending = earningsRecord + ? Number(earningsRecord.total_pending) + : 0; // Determine eligibility let canRedeem = true; @@ -194,18 +209,20 @@ async function getBalanceHandler(request: NextRequest): Promise { })); // Format recent earnings (handle createdAt as Date or string) - const formattedRecentEarnings: RecentEarning[] = recentEarnings.map((e) => ({ - id: e.id, - source: (e.source || "miniapp") as "miniapp" | "agent" | "mcp", - sourceId: e.sourceId || "", - amount: Number(e.amount), - description: e.description || "", - createdAt: e.createdAt - ? e.createdAt instanceof Date - ? e.createdAt.toISOString() - : String(e.createdAt) - : "", - })); + const formattedRecentEarnings: RecentEarning[] = recentEarnings.map( + (e) => ({ + id: e.id, + source: (e.source || "miniapp") as "miniapp" | "agent" | "mcp", + sourceId: e.sourceId || "", + amount: Number(e.amount), + description: e.description || "", + createdAt: e.createdAt + ? e.createdAt instanceof Date + ? e.createdAt.toISOString() + : String(e.createdAt) + : "", + }), + ); return NextResponse.json({ success: true, @@ -220,7 +237,8 @@ async function getBalanceHandler(request: NextRequest): Promise { recentEarnings: formattedRecentEarnings, limits: { minRedemptionUsd: SUPPLY_SHOCK_PROTECTION.MIN_REDEMPTION_USD, - maxSingleRedemptionUsd: SUPPLY_SHOCK_PROTECTION.MAX_SINGLE_REDEMPTION_USD, + maxSingleRedemptionUsd: + SUPPLY_SHOCK_PROTECTION.MAX_SINGLE_REDEMPTION_USD, userDailyLimitUsd: SUPPLY_SHOCK_PROTECTION.USER_DAILY_LIMIT_USD, userHourlyLimitUsd: SUPPLY_SHOCK_PROTECTION.USER_HOURLY_LIMIT_USD, }, @@ -255,7 +273,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/redemptions/quote/route.ts b/app/api/v1/redemptions/quote/route.ts index 6cbc37d06..3279b069a 100644 --- a/app/api/v1/redemptions/quote/route.ts +++ b/app/api/v1/redemptions/quote/route.ts @@ -44,7 +44,10 @@ async function getQuoteHandler(request: NextRequest): Promise { // Validate network const validNetworks = ["ethereum", "base", "bnb", "solana"] as const; - if (!networkParam || !validNetworks.includes(networkParam as (typeof validNetworks)[number])) { + if ( + !networkParam || + !validNetworks.includes(networkParam as (typeof validNetworks)[number]) + ) { return NextResponse.json( { success: false, @@ -58,11 +61,15 @@ async function getQuoteHandler(request: NextRequest): Promise { const pointsAmount = pointsParam ? parseInt(pointsParam, 10) : 100; if (isNaN(pointsAmount) || pointsAmount < 1) { - return NextResponse.json({ success: false, error: "Invalid pointsAmount" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid pointsAmount" }, + { status: 400 }, + ); } // Check if payout system is configured and available for this network - const networkAvailability = await payoutStatusService.isNetworkAvailable(network); + const networkAvailability = + await payoutStatusService.isNetworkAvailable(network); if (!networkAvailability.available) { // Get available networks as alternatives const status = await payoutStatusService.getStatus(); @@ -93,7 +100,11 @@ async function getQuoteHandler(request: NextRequest): Promise { } // Get TWAP-based quote with all security checks - const quoteResult = await twapPriceOracle.getRedemptionQuote(network, pointsAmount, user.id); + const quoteResult = await twapPriceOracle.getRedemptionQuote( + network, + pointsAmount, + user.id, + ); if (!quoteResult.success) { return NextResponse.json( @@ -110,13 +121,17 @@ async function getQuoteHandler(request: NextRequest): Promise { const usdValue = quote.usdValue; // Calculate effective tokens (after safety spread) - const effectiveElizaAmount = calculateEffectiveTokens(usdValue, quote.twapPrice); + const effectiveElizaAmount = calculateEffectiveTokens( + usdValue, + quote.twapPrice, + ); // Check token availability - const availability = await secureTokenRedemptionService.checkTokenAvailability( - network, - effectiveElizaAmount, - ); + const availability = + await secureTokenRedemptionService.checkTokenAvailability( + network, + effectiveElizaAmount, + ); logger.debug("[Redemption Quote] TWAP quote generated", { network, @@ -165,7 +180,8 @@ async function getQuoteHandler(request: NextRequest): Promise { // Delays & requirements requiresDelay: quote.requiresDelay, delayUntil: quote.delayUntil?.toISOString(), - requiresAdminApproval: usdValue >= ADMIN_CONTROLS.ADMIN_APPROVAL_THRESHOLD_USD, + requiresAdminApproval: + usdValue >= ADMIN_CONTROLS.ADMIN_APPROVAL_THRESHOLD_USD, // Limits info limits: { @@ -173,7 +189,8 @@ async function getQuoteHandler(request: NextRequest): Promise { maxRedemptionUsd: SUPPLY_SHOCK_PROTECTION.MAX_SINGLE_REDEMPTION_USD, userDailyLimitUsd: SUPPLY_SHOCK_PROTECTION.USER_DAILY_LIMIT_USD, userHourlyLimitUsd: SUPPLY_SHOCK_PROTECTION.USER_HOURLY_LIMIT_USD, - largeRedemptionThresholdUsd: SUPPLY_SHOCK_PROTECTION.LARGE_REDEMPTION_THRESHOLD_USD, + largeRedemptionThresholdUsd: + SUPPLY_SHOCK_PROTECTION.LARGE_REDEMPTION_THRESHOLD_USD, adminApprovalThresholdUsd: ADMIN_CONTROLS.ADMIN_APPROVAL_THRESHOLD_USD, }, }, @@ -197,7 +214,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/redemptions/route.ts b/app/api/v1/redemptions/route.ts index 8865b5e86..21dfa6d92 100644 --- a/app/api/v1/redemptions/route.ts +++ b/app/api/v1/redemptions/route.ts @@ -60,14 +60,17 @@ const createRateLimitConfig = { * - Rate limiting * - Full audit trail */ -async function createRedemptionHandler(request: NextRequest): Promise { +async function createRedemptionHandler( + request: NextRequest, +): Promise { // Check emergency pause if (process.env.REDEMPTION_EMERGENCY_PAUSE === "true") { logger.warn("[Redemption API] Emergency pause active - rejecting request"); return NextResponse.json( { success: false, - error: "Redemptions are temporarily paused for maintenance. Please try again later.", + error: + "Redemptions are temporarily paused for maintenance. Please try again later.", paused: true, }, { status: 503 }, @@ -80,7 +83,10 @@ async function createRedemptionHandler(request: NextRequest): Promise try { body = await request.json(); } catch { - return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json( + { success: false, error: "Invalid JSON body" }, + { status: 400 }, + ); } const validation = CreateRedemptionSchema.safeParse(body); @@ -99,11 +105,18 @@ async function createRedemptionHandler(request: NextRequest): Promise ); } - const { appId, pointsAmount, network, payoutAddress, signature, idempotencyKey } = - validation.data; + const { + appId, + pointsAmount, + network, + payoutAddress, + signature, + idempotencyKey, + } = validation.data; // Check if payout system is available for this network - const networkAvailability = await payoutStatusService.isNetworkAvailable(network); + const networkAvailability = + await payoutStatusService.isNetworkAvailable(network); if (!networkAvailability.available) { const status = await payoutStatusService.getStatus(); const availableNetworks = status.networks @@ -140,7 +153,9 @@ async function createRedemptionHandler(request: NextRequest): Promise // Mask address for logging (security) const maskedAddress = - payoutAddress.length > 20 ? `${payoutAddress.slice(0, 6)}...${payoutAddress.slice(-4)}` : "***"; + payoutAddress.length > 20 + ? `${payoutAddress.slice(0, 6)}...${payoutAddress.slice(-4)}` + : "***"; logger.info("[Redemption API] Creating secure redemption request", { userId: user.id.slice(0, 8) + "...", @@ -173,7 +188,10 @@ async function createRedemptionHandler(request: NextRequest): Promise error: result.error, }); - return NextResponse.json({ success: false, error: result.error }, { status: 400 }); + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 }, + ); } logger.info("[Redemption API] Secure redemption request created", { @@ -205,7 +223,10 @@ async function listRedemptionsHandler(request: NextRequest): Promise { const limitParam = request.nextUrl.searchParams.get("limit"); const limit = limitParam ? Math.min(parseInt(limitParam, 10), 100) : 20; - const redemptions = await secureTokenRedemptionService.listUserRedemptions(user.id, limit); + const redemptions = await secureTokenRedemptionService.listUserRedemptions( + user.id, + limit, + ); return NextResponse.json({ success: true, @@ -232,8 +253,14 @@ async function listRedemptionsHandler(request: NextRequest): Promise { // Export rate-limited handlers // POST: CRITICAL rate limit (5 req/min) for financial operations // GET: STRICT rate limit (10 req/min) for read operations -export const POST = withRateLimit(createRedemptionHandler, createRateLimitConfig); -export const GET = withRateLimit(listRedemptionsHandler, RateLimitPresets.STRICT); +export const POST = withRateLimit( + createRedemptionHandler, + createRateLimitConfig, +); +export const GET = withRateLimit( + listRedemptionsHandler, + RateLimitPresets.STRICT, +); /** * OPTIONS - CORS preflight @@ -244,7 +271,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/redemptions/status/route.ts b/app/api/v1/redemptions/status/route.ts index ba8220963..27f49c289 100644 --- a/app/api/v1/redemptions/status/route.ts +++ b/app/api/v1/redemptions/status/route.ts @@ -80,7 +80,8 @@ export async function OPTIONS() { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, X-App-Id", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-API-Key, X-App-Id", }, }); } diff --git a/app/api/v1/referrals/apply/route.ts b/app/api/v1/referrals/apply/route.ts index e69bf98fe..3a53ea95a 100644 --- a/app/api/v1/referrals/apply/route.ts +++ b/app/api/v1/referrals/apply/route.ts @@ -1,6 +1,9 @@ import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { getErrorStatusCode, nextJsonFromCaughtErrorWithHeaders } from "@/lib/api/errors"; +import { + getErrorStatusCode, + nextJsonFromCaughtErrorWithHeaders, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { referralsService } from "@/lib/services/referrals"; @@ -57,7 +60,10 @@ async function handlePOST(request: NextRequest) { ? 409 : 400; - return NextResponse.json({ error: result.message }, { status, headers: corsHeaders }); + return NextResponse.json( + { error: result.message }, + { status, headers: corsHeaders }, + ); } return NextResponse.json(result, { headers: corsHeaders }); diff --git a/app/api/v1/referrals/route.ts b/app/api/v1/referrals/route.ts index d65a0dde4..10ffb2c24 100644 --- a/app/api/v1/referrals/route.ts +++ b/app/api/v1/referrals/route.ts @@ -17,11 +17,18 @@ * by auth + `referral_codes` unique(user_id). A stricter design would be POST to create + GET read-only. */ import { type NextRequest, NextResponse } from "next/server"; -import { AuthenticationError, ForbiddenError, getErrorStatusCode } from "@/lib/api/errors"; +import { + AuthenticationError, + ForbiddenError, + getErrorStatusCode, +} from "@/lib/api/errors"; import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { referralsService } from "@/lib/services/referrals"; -import { coerceNonNegativeIntegerCount, type ReferralMeResponse } from "@/lib/types/referral-me"; +import { + coerceNonNegativeIntegerCount, + type ReferralMeResponse, +} from "@/lib/types/referral-me"; import { getCorsHeaders } from "@/lib/utils/cors"; import { logger } from "@/lib/utils/logger"; @@ -42,7 +49,9 @@ const WALLET_AUTH_FAILURE_PATTERNS = [ * Used to convert untyped wallet errors to {@link AuthenticationError}. */ function isWalletAuthFailure(error: Error): boolean { - return WALLET_AUTH_FAILURE_PATTERNS.some((pattern) => error.message.includes(pattern)); + return WALLET_AUTH_FAILURE_PATTERNS.some((pattern) => + error.message.includes(pattern), + ); } /** @@ -94,7 +103,9 @@ async function handleGET(request: NextRequest) { const row = await referralsService.getOrCreateCode(user.id); if (row == null || typeof row !== "object") { - throw new Error("Referrals API: getOrCreateCode returned no referral row"); + throw new Error( + "Referrals API: getOrCreateCode returned no referral row", + ); } if (typeof row.code !== "string" || row.code.length === 0) { throw new Error("Referrals API: referral row missing code"); @@ -119,11 +130,17 @@ async function handleGET(request: NextRequest) { return NextResponse.json(body, { headers: corsHeaders }); } catch (error) { if (error instanceof ForbiddenError) { - return NextResponse.json({ error: error.message }, { status: 403, headers: corsHeaders }); + return NextResponse.json( + { error: error.message }, + { status: 403, headers: corsHeaders }, + ); } if (isUnauthorizedError(error)) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401, headers: corsHeaders }); + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders }, + ); } logger.error("[Referrals API] Error getting referral code", { diff --git a/app/api/v1/remote/pair/route.ts b/app/api/v1/remote/pair/route.ts index bfd91c59e..478830be6 100644 --- a/app/api/v1/remote/pair/route.ts +++ b/app/api/v1/remote/pair/route.ts @@ -53,20 +53,30 @@ export async function POST(request: NextRequest) { const agentId = typeof body.agentId === "string" ? body.agentId.trim() : ""; if (!agentId) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "agentId is required" }, { status: 400 }), + NextResponse.json( + { success: false, error: "agentId is required" }, + { status: 400 }, + ), CORS_METHODS, ); } const requesterIdentity = - typeof body.requesterIdentity === "string" && body.requesterIdentity.trim().length > 0 + typeof body.requesterIdentity === "string" && + body.requesterIdentity.trim().length > 0 ? body.requesterIdentity.trim() : user.id; - const sandbox = await miladySandboxesRepository.findByIdAndOrg(agentId, user.organization_id); + const sandbox = await miladySandboxesRepository.findByIdAndOrg( + agentId, + user.organization_id, + ); if (!sandbox) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -97,7 +107,10 @@ export async function POST(request: NextRequest) { }), CORS_METHODS, ); - response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); + response.headers.set( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate", + ); response.headers.set("Pragma", "no-cache"); response.headers.set("Expires", "0"); return response; diff --git a/app/api/v1/remote/sessions/[id]/revoke/route.ts b/app/api/v1/remote/sessions/[id]/revoke/route.ts index 24a9c0ea8..63c493982 100644 --- a/app/api/v1/remote/sessions/[id]/revoke/route.ts +++ b/app/api/v1/remote/sessions/[id]/revoke/route.ts @@ -19,15 +19,24 @@ export function OPTIONS() { return handleCorsOptions(CORS_METHODS); } -export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const { id } = await params; - const existing = await remoteSessionsRepository.findByIdAndOrg(id, user.organization_id); + const existing = await remoteSessionsRepository.findByIdAndOrg( + id, + user.organization_id, + ); if (!existing) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Session not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Session not found" }, + { status: 404 }, + ), CORS_METHODS, ); } @@ -36,16 +45,26 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return applyCorsHeaders( NextResponse.json({ success: true, - data: { id: existing.id, status: existing.status, alreadyEnded: true }, + data: { + id: existing.id, + status: existing.status, + alreadyEnded: true, + }, }), CORS_METHODS, ); } - const revoked = await remoteSessionsRepository.revoke(id, user.organization_id); + const revoked = await remoteSessionsRepository.revoke( + id, + user.organization_id, + ); if (!revoked) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Revoke failed" }, { status: 409 }), + NextResponse.json( + { success: false, error: "Revoke failed" }, + { status: 409 }, + ), CORS_METHODS, ); } @@ -53,7 +72,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return applyCorsHeaders( NextResponse.json({ success: true, - data: { id: revoked.id, status: revoked.status, endedAt: revoked.ended_at }, + data: { + id: revoked.id, + status: revoked.status, + endedAt: revoked.ended_at, + }, }), CORS_METHODS, ); diff --git a/app/api/v1/remote/sessions/route.ts b/app/api/v1/remote/sessions/route.ts index 85445fce6..1b2f38185 100644 --- a/app/api/v1/remote/sessions/route.ts +++ b/app/api/v1/remote/sessions/route.ts @@ -35,10 +35,16 @@ export async function GET(request: NextRequest) { ); } - const sandbox = await miladySandboxesRepository.findByIdAndOrg(agentId, user.organization_id); + const sandbox = await miladySandboxesRepository.findByIdAndOrg( + agentId, + user.organization_id, + ); if (!sandbox) { return applyCorsHeaders( - NextResponse.json({ success: false, error: "Agent not found" }, { status: 404 }), + NextResponse.json( + { success: false, error: "Agent not found" }, + { status: 404 }, + ), CORS_METHODS, ); } diff --git a/app/api/v1/reports/bug/route.ts b/app/api/v1/reports/bug/route.ts index 6264d8e2f..cfa74044c 100644 --- a/app/api/v1/reports/bug/route.ts +++ b/app/api/v1/reports/bug/route.ts @@ -62,9 +62,13 @@ async function handlePOST(request: NextRequest) { const reportId = `rpt_${randomUUID()}`; const receivedAt = new Date().toISOString(); const recipient = - process.env.BUG_REPORT_EMAIL_TO ?? process.env.SUPPORT_EMAIL ?? "developer@elizalabs.ai"; + process.env.BUG_REPORT_EMAIL_TO ?? + process.env.SUPPORT_EMAIL ?? + "developer@elizalabs.ai"; - const startupSummary = validated.startup ? JSON.stringify(validated.startup, null, 2) : undefined; + const startupSummary = validated.startup + ? JSON.stringify(validated.startup, null, 2) + : undefined; const html = ` @@ -112,8 +116,12 @@ async function handlePOST(request: NextRequest) { "", "Steps to Reproduce:", validated.stepsToReproduce, - validated.expectedBehavior ? `\nExpected Behavior:\n${validated.expectedBehavior}` : "", - validated.actualBehavior ? `\nActual Behavior:\n${validated.actualBehavior}` : "", + validated.expectedBehavior + ? `\nExpected Behavior:\n${validated.expectedBehavior}` + : "", + validated.actualBehavior + ? `\nActual Behavior:\n${validated.actualBehavior}` + : "", startupSummary ? `\nStartup Context:\n${startupSummary}` : "", validated.logs ? `\nLogs:\n${validated.logs}` : "", ] diff --git a/app/api/v1/responses/route.ts b/app/api/v1/responses/route.ts index 542327140..aba9dd72e 100644 --- a/app/api/v1/responses/route.ts +++ b/app/api/v1/responses/route.ts @@ -17,8 +17,15 @@ import type { NextRequest } from "next/server"; import { getErrorStatusCode, getSafeErrorMessage } from "@/lib/api/errors"; import { requireAuthOrApiKey } from "@/lib/auth"; -import { getAnonymousUser, getOrCreateAnonymousUser } from "@/lib/auth-anonymous"; -import { enforceOrgRateLimit, RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; +import { + getAnonymousUser, + getOrCreateAnonymousUser, +} from "@/lib/auth-anonymous"; +import { + enforceOrgRateLimit, + RateLimitPresets, + withRateLimit, +} from "@/lib/middleware/rate-limit"; import { isGroqNativeModel } from "@/lib/models"; import { calculateCost, @@ -28,7 +35,10 @@ import { isReasoningModel, normalizeModelName, } from "@/lib/pricing"; -import { getProviderForModelWithFallback, withProviderFallback } from "@/lib/providers"; +import { + getProviderForModelWithFallback, + withProviderFallback, +} from "@/lib/providers"; import { mergeGatewayGroqPreferenceWithAnthropicCot } from "@/lib/providers/anthropic-thinking"; import { getAiProviderConfigurationError, @@ -176,11 +186,14 @@ function normalizeToolChoice( }; } const unknownChoice = toolChoice as unknown as Record; - logger.warn("[Responses API] Unrecognized tool_choice shape, passing through unchanged", { - choiceType: unknownChoice?.type, - hasFunction: Boolean(unknownChoice?.function), - hasName: Boolean(unknownChoice?.name), - }); + logger.warn( + "[Responses API] Unrecognized tool_choice shape, passing through unchanged", + { + choiceType: unknownChoice?.type, + hasFunction: Boolean(unknownChoice?.function), + hasName: Boolean(unknownChoice?.name), + }, + ); return toolChoice as ChatCompletionsToolChoice; } @@ -255,7 +268,9 @@ function transformAISdkToOpenAI(aiSdkRequest: AISdkRequest): OpenAIChatRequest { } = aiSdkRequest; const normalizedInput = - typeof input === "string" ? [{ role: "user" as const, content: input }] : (input ?? []); + typeof input === "string" + ? [{ role: "user" as const, content: input }] + : (input ?? []); // Transform messages: fix content types for multimodal const transformedMessages = normalizedInput.map((msg, msgIndex) => { @@ -290,7 +305,8 @@ function transformAISdkToOpenAI(aiSdkRequest: AISdkRequest): OpenAIChatRequest { part.image_url !== null && typeof part.image_url.url === "string" ? part.image_url.url - : undefined) ?? (typeof part.image === "string" ? part.image : ""), + : undefined) ?? + (typeof part.image === "string" ? part.image : ""), }, }; } @@ -333,13 +349,17 @@ function transformAISdkToOpenAI(aiSdkRequest: AISdkRequest): OpenAIChatRequest { // Keep text blocks only if they have non-empty text if (typedPart.type === "text" || typedPart.type === "input_text") { const hasNonEmptyText = - typeof typedPart.text === "string" && typedPart.text.trim() !== ""; + typeof typedPart.text === "string" && + typedPart.text.trim() !== ""; if (!hasNonEmptyText) { - logger.debug("[Responses API] Filtering out empty text content block", { - messageIndex: msgIndex, - role: msg.role, - textValue: typedPart.text, - }); + logger.debug( + "[Responses API] Filtering out empty text content block", + { + messageIndex: msgIndex, + role: msg.role, + textValue: typedPart.text, + }, + ); } return hasNonEmptyText; } @@ -350,21 +370,27 @@ function transformAISdkToOpenAI(aiSdkRequest: AISdkRequest): OpenAIChatRequest { // Log if we filtered out content if (transformedContent.length < originalLength) { - logger.info("[Responses API] Filtered empty text blocks from content array", { - messageIndex: msgIndex, - role: msg.role, - originalParts: originalLength, - remainingParts: transformedContent.length, - }); + logger.info( + "[Responses API] Filtered empty text blocks from content array", + { + messageIndex: msgIndex, + role: msg.role, + originalParts: originalLength, + remainingParts: transformedContent.length, + }, + ); } // If content array is now empty or has only empty parts, convert to empty string // This will be caught by validation later if (transformedContent.length === 0) { - logger.warn("[Responses API] Content array became empty after filtering", { - messageIndex: msgIndex, - role: msg.role, - }); + logger.warn( + "[Responses API] Content array became empty after filtering", + { + messageIndex: msgIndex, + role: msg.role, + }, + ); return { ...msg, content: "" }; } @@ -389,8 +415,12 @@ function transformAISdkToOpenAI(aiSdkRequest: AISdkRequest): OpenAIChatRequest { type: "function", function: { name: tool.name, - ...(tool.description !== undefined ? { description: tool.description } : {}), - ...(tool.parameters !== undefined ? { parameters: tool.parameters } : {}), + ...(tool.description !== undefined + ? { description: tool.description } + : {}), + ...(tool.parameters !== undefined + ? { parameters: tool.parameters } + : {}), }, }; } @@ -399,11 +429,14 @@ function transformAISdkToOpenAI(aiSdkRequest: AISdkRequest): OpenAIChatRequest { // The `tool` value here is `never` per the discriminated union, but we // narrow back to a record for diagnostic field extraction. const unknownTool = tool as unknown as Record; - logger.warn("[Responses API] Unrecognized tool shape, passing through unchanged", { - toolType: unknownTool?.type, - hasFunction: Boolean(unknownTool?.function), - hasName: Boolean(unknownTool?.name), - }); + logger.warn( + "[Responses API] Unrecognized tool shape, passing through unchanged", + { + toolType: unknownTool?.type, + hasFunction: Boolean(unknownTool?.function), + hasName: Boolean(unknownTool?.name), + }, + ); // Intentional best-effort: forward the malformed tool unchanged so // the downstream provider surfaces the original validation error // rather than silently dropping the tool. The cast is a lie that @@ -486,7 +519,9 @@ function transformOpenAIToAISdk(openAIResponse: OpenAIChatResponse): object { if (typeof messageContent === "string") { // Simple string content - content = [{ type: "output_text", text: messageContent, annotations: [] }]; + content = [ + { type: "output_text", text: messageContent, annotations: [] }, + ]; } else if (Array.isArray(messageContent)) { // Already array (multimodal) content = (messageContent as Array).map((part: unknown) => { @@ -543,7 +578,8 @@ function transformOpenAIToAISdk(openAIResponse: OpenAIChatResponse): object { // Additional AI SDK expected fields error: null, // Preserve any provider metadata - ...("provider_metadata" in openAIResponse && openAIResponse.provider_metadata + ...("provider_metadata" in openAIResponse && + openAIResponse.provider_metadata ? { provider_metadata: openAIResponse.provider_metadata } : {}), }; @@ -607,7 +643,10 @@ function buildTrajectoryContext( const transcript = request.messages .filter((message) => message.role !== "system") - .map((message) => `${message.role}: ${stringifyMessageContent(message.content)}`) + .map( + (message) => + `${message.role}: ${stringifyMessageContent(message.content)}`, + ) .filter((value) => value.length > 0) .join("\n\n"); @@ -698,7 +737,9 @@ function sanitizeGatewayError(raw: unknown): { // The gateway often nests an `error` object inside `error` — peel it // once if we see that shape. const inner = - obj.error && typeof obj.error === "object" ? (obj.error as Record) : obj; + obj.error && typeof obj.error === "object" + ? (obj.error as Record) + : obj; const sanitized: ReturnType = { message: typeof inner.message === "string" && inner.message.length > 0 @@ -772,11 +813,13 @@ function isNativeResponsesPayload(body: Record): boolean { // upstream returns a coherent validation error rather than falling // through to the Chat Completions transform path which would choke // on the field entirely. - if (body.instructions !== undefined && body.instructions !== null) return true; + if (body.instructions !== undefined && body.instructions !== null) + return true; // gpt-5 and gpt-5.x (gpt-5, gpt-5-mini, gpt-5.1, gpt-5.2, gpt-5.3, // gpt-5.4, gpt-5.x-codex, ...). Any model whose id starts with "gpt-5" // uses the Responses API natively. - if (typeof body.model === "string" && /^gpt-5(\b|[-.])/.test(body.model)) return true; + if (typeof body.model === "string" && /^gpt-5(\b|[-.])/.test(body.model)) + return true; if (Array.isArray(body.tools)) { for (const tool of body.tools) { if (tool && typeof tool === "object") { @@ -943,35 +986,38 @@ async function handleNativeResponsesPassthrough( // pseudo-messages. If estimation fails, fall back to a small default. let estimatedCost = 0; try { - const pseudoMessages: Array<{ role: string; content: string }> = Array.isArray(body.input) - ? (body.input as unknown[]) - .map((item) => { - if (!item || typeof item !== "object") return null; - const rec = item as Record; - const content = rec.content; - if (typeof content === "string") { - return { - role: (rec.role as string) ?? "user", - content, - }; - } - if (Array.isArray(content)) { - const text = content - .map((p) => - p && typeof p === "object" && typeof (p as { text?: unknown }).text === "string" - ? (p as { text: string }).text - : "", - ) - .join(" "); - return { - role: (rec.role as string) ?? "user", - content: text, - }; - } - return null; - }) - .filter((m): m is { role: string; content: string } => m !== null) - : []; + const pseudoMessages: Array<{ role: string; content: string }> = + Array.isArray(body.input) + ? (body.input as unknown[]) + .map((item) => { + if (!item || typeof item !== "object") return null; + const rec = item as Record; + const content = rec.content; + if (typeof content === "string") { + return { + role: (rec.role as string) ?? "user", + content, + }; + } + if (Array.isArray(content)) { + const text = content + .map((p) => + p && + typeof p === "object" && + typeof (p as { text?: unknown }).text === "string" + ? (p as { text: string }).text + : "", + ) + .join(" "); + return { + role: (rec.role as string) ?? "user", + content: text, + }; + } + return null; + }) + .filter((m): m is { role: string; content: string } => m !== null) + : []; // pseudoMessages satisfies the looser // `Array<{ role: string; content: string | object }>` shape // estimateRequestCost expects — TypeScript widens `content` to @@ -979,7 +1025,9 @@ async function handleNativeResponsesPassthrough( estimatedCost = await estimateRequestCost( model, pseudoMessages, - typeof body.max_output_tokens === "number" ? body.max_output_tokens : undefined, + typeof body.max_output_tokens === "number" + ? body.max_output_tokens + : undefined, ); } catch (err) { logger.warn("[Responses API passthrough] estimateRequestCost failed", { @@ -1065,12 +1113,16 @@ async function handleNativeResponsesPassthrough( // end clients. if (err && typeof err === "object" && "error" in err && "status" in err) { const gwErr = err as { status: number; error: unknown }; - return Response.json({ error: sanitizeGatewayError(gwErr.error) }, { status: gwErr.status }); + return Response.json( + { error: sanitizeGatewayError(gwErr.error) }, + { status: gwErr.status }, + ); } return Response.json( { error: { - message: err instanceof Error ? err.message : "Upstream request failed", + message: + err instanceof Error ? err.message : "Upstream request failed", type: "api_error", code: "upstream_error", }, @@ -1113,7 +1165,8 @@ async function handleNativeResponsesPassthrough( // - NO BODY (headers-only upstream response, edge case): also // synchronously settle to reserved so the reservation isn't // stranded. - const upstreamContentType = upstreamResponse.headers.get("content-type") ?? ""; + const upstreamContentType = + upstreamResponse.headers.get("content-type") ?? ""; const isStreamingResponse = upstreamContentType.includes("text/event-stream"); let reconciledBody: ReadableStream | null = null; @@ -1121,7 +1174,9 @@ async function handleNativeResponsesPassthrough( // Streaming path: stream wrapper handles its own reconciliation // via the runReconciliation callback below. const providerName = getProviderFromModel(model); - const runReconciliation = async (usage: ResponsesUsage | null): Promise => { + const runReconciliation = async ( + usage: ResponsesUsage | null, + ): Promise => { if (!settleReservation) return; if (!usage) { try { @@ -1148,27 +1203,36 @@ async function handleNativeResponsesPassthrough( const actualCost = Math.min(totalCost, reservedAmount); await settleReservation(actualCost); } catch (err) { - logger.warn("[Responses API passthrough] cost calculation failed, settling to reserved", { - err, - }); + logger.warn( + "[Responses API passthrough] cost calculation failed, settling to reserved", + { + err, + }, + ); try { await settleReservation(reservedAmount); } catch (innerErr) { - logger.warn("[Responses API passthrough] fallback settle also failed", { err: innerErr }); + logger.warn( + "[Responses API passthrough] fallback settle also failed", + { err: innerErr }, + ); } } }; - reconciledBody = wrapWithUsageExtraction(upstreamResponse.body, (usage, reason) => { - logger.debug("[Responses API passthrough] stream terminated", { - userId: user.id, - reason, - sawUsage: usage !== null, - inputTokens: usage?.inputTokens, - outputTokens: usage?.outputTokens, - }); - void runReconciliation(usage); - }); + reconciledBody = wrapWithUsageExtraction( + upstreamResponse.body, + (usage, reason) => { + logger.debug("[Responses API passthrough] stream terminated", { + userId: user.id, + reason, + sawUsage: usage !== null, + inputTokens: usage?.inputTokens, + outputTokens: usage?.outputTokens, + }); + void runReconciliation(usage); + }, + ); } else if (settleReservation) { // Non-streaming OR no-body path: settle synchronously so a // serverless function freeze can't strand the reservation. @@ -1248,7 +1312,10 @@ async function handlePOST(req: NextRequest) { const contentLengthHeader = req.headers.get("content-length"); if (contentLengthHeader) { const contentLength = Number.parseInt(contentLengthHeader, 10); - if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSES_BODY_BYTES) { + if ( + Number.isFinite(contentLength) && + contentLength > MAX_RESPONSES_BODY_BYTES + ) { logger.warn("[Responses API] rejecting oversized request body", { contentLength, limit: MAX_RESPONSES_BODY_BYTES, @@ -1312,7 +1379,10 @@ async function handlePOST(req: NextRequest) { // Per-org tier rate limit (skipped for anonymous users — they use the outer withRateLimit) // Shares the "completions" counter with /chat/completions — both are LLM inference endpoints if (user.organization_id) { - const orgRateLimited = await enforceOrgRateLimit(user.organization_id, "completions"); + const orgRateLimited = await enforceOrgRateLimit( + user.organization_id, + "completions", + ); if (orgRateLimited) return orgRateLimited; } @@ -1327,10 +1397,13 @@ async function handlePOST(req: NextRequest) { const bodyText = await req.text(); const actualBodyBytes = Buffer.byteLength(bodyText, "utf8"); if (actualBodyBytes > MAX_RESPONSES_BODY_BYTES) { - logger.warn("[Responses API] rejecting oversized request body (post-read)", { - actualBytes: actualBodyBytes, - limit: MAX_RESPONSES_BODY_BYTES, - }); + logger.warn( + "[Responses API] rejecting oversized request body (post-read)", + { + actualBytes: actualBodyBytes, + limit: MAX_RESPONSES_BODY_BYTES, + }, + ); return Response.json( { error: { @@ -1344,7 +1417,11 @@ async function handlePOST(req: NextRequest) { } try { const parsedBody: unknown = JSON.parse(bodyText); - if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) { + if ( + !parsedBody || + typeof parsedBody !== "object" || + Array.isArray(parsedBody) + ) { return Response.json( { error: { @@ -1457,7 +1534,8 @@ async function handlePOST(req: NextRequest) { request.messages = request.messages.filter((msg, i) => { if ( msg.role === "system" && - (!msg.content || (typeof msg.content === "string" && msg.content.trim() === "")) + (!msg.content || + (typeof msg.content === "string" && msg.content.trim() === "")) ) { logger.debug("[Responses API] Filtering out empty system message", { messageIndex: i, @@ -1502,7 +1580,8 @@ async function handlePOST(req: NextRequest) { return Response.json( { error: { - message: "Each message must have content, tool_calls, tool_call_id, or function_call", + message: + "Each message must have content, tool_calls, tool_call_id, or function_call", type: "invalid_request_error", param: `messages.${i}.content`, code: "invalid_value", @@ -1539,8 +1618,14 @@ async function handlePOST(req: NextRequest) { const hasValidTextContent = msg.content.some((part) => { if (typeof part === "object" && part !== null && "type" in part) { const typedPart = part as { type: string; text?: string }; - if (typedPart.type === "text" || typedPart.type === "input_text") { - return typeof typedPart.text === "string" && typedPart.text.trim() !== ""; + if ( + typedPart.type === "text" || + typedPart.type === "input_text" + ) { + return ( + typeof typedPart.text === "string" && + typedPart.text.trim() !== "" + ); } // Non-text parts (images) are valid return true; @@ -1549,17 +1634,26 @@ async function handlePOST(req: NextRequest) { }); // If we have a content array but no valid content, and no tool calls, reject - if (!hasValidTextContent && !hasToolCalls && !hasToolCallId && !hasFunctionCall) { - logger.warn("[Responses API] Content array has no valid text content", { - messageIndex: i, - role: msg.role, - contentLength: msg.content.length, - }); + if ( + !hasValidTextContent && + !hasToolCalls && + !hasToolCallId && + !hasFunctionCall + ) { + logger.warn( + "[Responses API] Content array has no valid text content", + { + messageIndex: i, + role: msg.role, + contentLength: msg.content.length, + }, + ); return Response.json( { error: { - message: "Message content array must contain at least one non-empty text block", + message: + "Message content array must contain at least one non-empty text block", type: "invalid_request_error", param: `messages.${i}.content`, code: "invalid_value", @@ -1591,7 +1685,9 @@ async function handlePOST(req: NextRequest) { } // Start async content moderation (runs in background, doesn't block) - const lastUserMessage = [...request.messages].reverse().find((m) => m.role === "user"); + const lastUserMessage = [...request.messages] + .reverse() + .find((m) => m.role === "user"); if (lastUserMessage?.content) { const messageText = typeof lastUserMessage.content === "string" @@ -1599,13 +1695,18 @@ async function handlePOST(req: NextRequest) { : lastUserMessage.content.find((c) => c.type === "text")?.text || ""; if (messageText) { - contentModerationService.moderateInBackground(messageText, user.id, undefined, (result) => { - logger.warn("[Responses API] Async moderation detected violation", { - userId: user.id, - categories: result.flaggedCategories, - action: result.action, - }); - }); + contentModerationService.moderateInBackground( + messageText, + user.id, + undefined, + (result) => { + logger.warn("[Responses API] Async moderation detected violation", { + userId: user.id, + categories: result.flaggedCategories, + action: result.action, + }); + }, + ); } } @@ -1692,7 +1793,8 @@ async function handlePOST(req: NextRequest) { let reservationSettled = false; settleReservation = async (actualCost: number) => { - if (reservationSettled || !user.organization_id || !reservedAmount) return; + if (reservationSettled || !user.organization_id || !reservedAmount) + return; reservationSettled = true; @@ -1794,11 +1896,19 @@ async function handlePOST(req: NextRequest) { error: { message: string; type?: string; code?: string }; } - if (error && typeof error === "object" && "error" in error && "status" in error) { + if ( + error && + typeof error === "object" && + "error" in error && + "status" in error + ) { const gatewayStatus = (error as { status: unknown }).status; if (typeof gatewayStatus === "number") { const gatewayError = error as GatewayError; - return Response.json({ error: gatewayError.error }, { status: gatewayError.status }); + return Response.json( + { error: gatewayError.error }, + { status: gatewayError.status }, + ); } } @@ -1819,7 +1929,8 @@ async function handlePOST(req: NextRequest) { logger.error("[Responses API] Error:", error); const status = getErrorStatusCode(error); - const clientMessage = status >= 500 ? "Internal server error" : getSafeErrorMessage(error); + const clientMessage = + status >= 500 ? "Internal server error" : getSafeErrorMessage(error); const code = status === 402 ? "insufficient_credits" @@ -2027,7 +2138,9 @@ function handleStreamingResponse( type: "message", id: itemId, role: "assistant", - content: [{ type: "output_text", text: fullContent, annotations: [] }], + content: [ + { type: "output_text", text: fullContent, annotations: [] }, + ], status: "completed", }, }); @@ -2137,7 +2250,10 @@ function handleStreamingResponse( // Log parsing failures as warnings - silent failures are hard to debug logger.warn("[Responses API] Failed to parse streaming chunk", { line: line.substring(0, 200), // Truncate to avoid log spam - error: parseError instanceof Error ? parseError.message : String(parseError), + error: + parseError instanceof Error + ? parseError.message + : String(parseError), }); } } @@ -2182,14 +2298,21 @@ function handleStreamingResponse( // After stream completes, record analytics if (totalTokens === 0) { - logger.warn("[Responses API] No usage data in stream, estimating tokens", { - model, - contentLength: fullContent.length, - }); + logger.warn( + "[Responses API] No usage data in stream, estimating tokens", + { + model, + contentLength: fullContent.length, + }, + ); // Estimate tokens from content const messageText = messages - .map((m) => (typeof m.content === "string" ? m.content : JSON.stringify(m.content))) + .map((m) => + typeof m.content === "string" + ? m.content + : JSON.stringify(m.content), + ) .join(" "); inputTokens = estimateTokens(messageText); outputTokens = estimateTokens(fullContent); diff --git a/app/api/v1/solana/methods/route.ts b/app/api/v1/solana/methods/route.ts index 0c3090f3e..d9be418eb 100644 --- a/app/api/v1/solana/methods/route.ts +++ b/app/api/v1/solana/methods/route.ts @@ -25,7 +25,8 @@ export async function OPTIONS() { export async function GET() { try { - const pricingRecords = await servicePricingRepository.listByService("solana-rpc"); + const pricingRecords = + await servicePricingRepository.listByService("solana-rpc"); // Only return active methods, excluding internal entries (prefixed with _) const activeMethods = pricingRecords diff --git a/app/api/v1/solana/rpc/route.ts b/app/api/v1/solana/rpc/route.ts index 5b85e6269..7d87377f9 100644 --- a/app/api/v1/solana/rpc/route.ts +++ b/app/api/v1/solana/rpc/route.ts @@ -12,7 +12,10 @@ import { NextRequest, NextResponse } from "next/server"; import { applyCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { createHandler } from "@/lib/services/proxy/engine"; -import { rpcConfigForChain, rpcHandlerForChain } from "@/lib/services/proxy/services/rpc"; +import { + rpcConfigForChain, + rpcHandlerForChain, +} from "@/lib/services/proxy/services/rpc"; export const maxDuration = 30; const CORS_METHODS = "POST, OPTIONS"; @@ -21,7 +24,10 @@ export async function OPTIONS() { return handleCorsOptions(CORS_METHODS); } -const basePostHandler = createHandler(rpcConfigForChain("solana"), rpcHandlerForChain("solana")); +const basePostHandler = createHandler( + rpcConfigForChain("solana"), + rpcHandlerForChain("solana"), +); export async function POST(request: NextRequest) { try { diff --git a/app/api/v1/solana/token-accounts/[address]/route.ts b/app/api/v1/solana/token-accounts/[address]/route.ts index f0e811bb1..68bbd3fea 100644 --- a/app/api/v1/solana/token-accounts/[address]/route.ts +++ b/app/api/v1/solana/token-accounts/[address]/route.ts @@ -11,7 +11,10 @@ import { NextRequest, NextResponse } from "next/server"; import { getCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { solanaRpcConfig, solanaRpcHandler } from "@/lib/services/proxy/services/solana-rpc"; +import { + solanaRpcConfig, + solanaRpcHandler, +} from "@/lib/services/proxy/services/solana-rpc"; import { isValidSolanaAddress } from "@/lib/services/proxy/services/solana-validation"; export const maxDuration = 30; @@ -50,7 +53,12 @@ export async function GET( const corsHeaders = getCorsHeaders("GET, OPTIONS"); try { - const response = await executeWithBody(solanaRpcConfig, solanaRpcHandler, request, body); + const response = await executeWithBody( + solanaRpcConfig, + solanaRpcHandler, + request, + body, + ); for (const [key, value] of Object.entries(corsHeaders)) { response.headers.set(key, value); diff --git a/app/api/v1/solana/transactions/[address]/route.ts b/app/api/v1/solana/transactions/[address]/route.ts index 5781996ad..602902e7c 100644 --- a/app/api/v1/solana/transactions/[address]/route.ts +++ b/app/api/v1/solana/transactions/[address]/route.ts @@ -11,7 +11,10 @@ import { NextRequest, NextResponse } from "next/server"; import { getCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; import { executeWithBody } from "@/lib/services/proxy/engine"; -import { solanaRpcConfig, solanaRpcHandler } from "@/lib/services/proxy/services/solana-rpc"; +import { + solanaRpcConfig, + solanaRpcHandler, +} from "@/lib/services/proxy/services/solana-rpc"; import { isValidSolanaAddress } from "@/lib/services/proxy/services/solana-validation"; export const maxDuration = 30; @@ -49,7 +52,12 @@ export async function GET( }; try { - const response = await executeWithBody(solanaRpcConfig, solanaRpcHandler, request, body); + const response = await executeWithBody( + solanaRpcConfig, + solanaRpcHandler, + request, + body, + ); const corsHeaders = getCorsHeaders("GET, OPTIONS"); for (const [key, value] of Object.entries(corsHeaders)) { diff --git a/app/api/v1/steward/tenants/credentials/route.ts b/app/api/v1/steward/tenants/credentials/route.ts index 0c921099c..8747d1c54 100644 --- a/app/api/v1/steward/tenants/credentials/route.ts +++ b/app/api/v1/steward/tenants/credentials/route.ts @@ -27,7 +27,10 @@ export async function GET(req: NextRequest) { .limit(1); if (!org) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } if (!org.stewardTenantId) { @@ -37,7 +40,8 @@ export async function GET(req: NextRequest) { ); } - const stewardApiUrl = process.env.STEWARD_API_URL ?? "http://localhost:3200"; + const stewardApiUrl = + process.env.STEWARD_API_URL ?? "http://localhost:3200"; return NextResponse.json({ tenantId: org.stewardTenantId, diff --git a/app/api/v1/steward/tenants/route.ts b/app/api/v1/steward/tenants/route.ts index ff0708970..5d8c985bf 100644 --- a/app/api/v1/steward/tenants/route.ts +++ b/app/api/v1/steward/tenants/route.ts @@ -32,9 +32,15 @@ export async function POST(req: NextRequest) { try { const { user } = await requireAuthOrApiKeyWithOrg(req); - const body = (await req.json()) as { organizationId?: string; tenantName?: string }; + const body = (await req.json()) as { + organizationId?: string; + tenantName?: string; + }; if (!body.organizationId) { - return NextResponse.json({ error: "organizationId is required" }, { status: 400 }); + return NextResponse.json( + { error: "organizationId is required" }, + { status: 400 }, + ); } if (body.organizationId !== user.organization_id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -51,7 +57,10 @@ export async function POST(req: NextRequest) { .limit(1); if (!org) { - return NextResponse.json({ error: "Organization not found" }, { status: 404 }); + return NextResponse.json( + { error: "Organization not found" }, + { status: 404 }, + ); } // Idempotent — already provisioned @@ -67,7 +76,10 @@ export async function POST(req: NextRequest) { platformKey = getPlatformKey(); } catch { logger.error("[steward-tenants] STEWARD_PLATFORM_KEYS not configured"); - return NextResponse.json({ error: "Steward not configured" }, { status: 503 }); + return NextResponse.json( + { error: "Steward not configured" }, + { status: 503 }, + ); } const stewardRes = await fetch(`${getStewardApiUrl()}/platform/tenants`, { @@ -88,7 +100,9 @@ export async function POST(req: NextRequest) { if (stewardRes.status === 409) { // Tenant already exists in Steward but not linked in our DB — re-link without API key - logger.warn(`[steward-tenants] Tenant ${tenantId} already exists in Steward, linking org`); + logger.warn( + `[steward-tenants] Tenant ${tenantId} already exists in Steward, linking org`, + ); await dbWrite .update(organizations) .set({ steward_tenant_id: tenantId }) @@ -100,7 +114,10 @@ export async function POST(req: NextRequest) { logger.error("[steward-tenants] Failed to create Steward tenant", { error: stewardData.error, }); - return NextResponse.json({ error: "Failed to provision Steward tenant" }, { status: 502 }); + return NextResponse.json( + { error: "Failed to provision Steward tenant" }, + { status: 502 }, + ); } const apiKey = stewardData.apiKey ?? stewardData.data?.apiKey ?? ""; @@ -110,7 +127,9 @@ export async function POST(req: NextRequest) { .set({ steward_tenant_id: tenantId, steward_tenant_api_key: apiKey }) .where(eq(organizations.id, org.id)); - logger.info(`[steward-tenants] Provisioned tenant ${tenantId} for org ${org.id}`); + logger.info( + `[steward-tenants] Provisioned tenant ${tenantId} for org ${org.id}`, + ); return NextResponse.json({ tenantId, isNew: true }, { status: 201 }); } catch (error) { const status = getErrorStatusCode(error); @@ -121,7 +140,9 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status }); } return NextResponse.json( - { error: error instanceof Error ? error.message : "Internal server error" }, + { + error: error instanceof Error ? error.message : "Internal server error", + }, { status }, ); } diff --git a/app/api/v1/telegram/chats/route.ts b/app/api/v1/telegram/chats/route.ts index 7b1f94cce..dfd68d1f0 100644 --- a/app/api/v1/telegram/chats/route.ts +++ b/app/api/v1/telegram/chats/route.ts @@ -7,7 +7,9 @@ export const maxDuration = 30; export async function GET(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); - const chats = await telegramChatsRepository.findByOrganization(user.organization_id); + const chats = await telegramChatsRepository.findByOrganization( + user.organization_id, + ); return NextResponse.json({ chats: chats.map((chat) => ({ diff --git a/app/api/v1/telegram/connect/route.ts b/app/api/v1/telegram/connect/route.ts index be9a31024..7f3e3edec 100644 --- a/app/api/v1/telegram/connect/route.ts +++ b/app/api/v1/telegram/connect/route.ts @@ -33,31 +33,48 @@ export async function POST(request: NextRequest): Promise { { status: 400 }, ); } - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); } - const validation = await telegramAutomationService.validateBotToken(body.botToken); + const validation = await telegramAutomationService.validateBotToken( + body.botToken, + ); if (!validation.valid || !validation.botInfo) { - return NextResponse.json({ error: validation.error || "Invalid bot token" }, { status: 400 }); + return NextResponse.json( + { error: validation.error || "Invalid bot token" }, + { status: 400 }, + ); } try { - await telegramAutomationService.storeCredentials(user.organization_id, user.id, { - botToken: body.botToken, - botUsername: validation.botInfo.botUsername, - botId: validation.botInfo.botId, - }); + await telegramAutomationService.storeCredentials( + user.organization_id, + user.id, + { + botToken: body.botToken, + botUsername: validation.botInfo.botUsername, + botId: validation.botInfo.botId, + }, + ); } catch (error) { logger.error("[Telegram Connect] Failed to store credentials", { organizationId: user.organization_id, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to store credentials" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to store credentials" }, + { status: 500 }, + ); } await invalidateOAuthState(user.organization_id, "telegram", user.id); - const webhookResult = await telegramAutomationService.setWebhook(user.organization_id); + const webhookResult = await telegramAutomationService.setWebhook( + user.organization_id, + ); if (!webhookResult.success) { logger.warn("[Telegram Connect] Webhook setup failed", { organizationId: user.organization_id, diff --git a/app/api/v1/telegram/disconnect/route.ts b/app/api/v1/telegram/disconnect/route.ts index 284eee8b8..147190758 100644 --- a/app/api/v1/telegram/disconnect/route.ts +++ b/app/api/v1/telegram/disconnect/route.ts @@ -16,7 +16,10 @@ export async function DELETE(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); try { - await telegramAutomationService.removeCredentials(user.organization_id, user.id); + await telegramAutomationService.removeCredentials( + user.organization_id, + user.id, + ); await invalidateOAuthState(user.organization_id, "telegram", user.id); @@ -30,6 +33,9 @@ export async function DELETE(request: NextRequest): Promise { organizationId: user.organization_id, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to disconnect" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to disconnect" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/telegram/scan-chats/route.ts b/app/api/v1/telegram/scan-chats/route.ts index f8f931bb4..c6ce6c3d6 100644 --- a/app/api/v1/telegram/scan-chats/route.ts +++ b/app/api/v1/telegram/scan-chats/route.ts @@ -29,7 +29,9 @@ export async function GET(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); try { - const allChats = await telegramChatsRepository.findByOrganization(user.organization_id); + const allChats = await telegramChatsRepository.findByOrganization( + user.organization_id, + ); return NextResponse.json({ success: true, @@ -48,7 +50,10 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Failed to fetch chats" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch chats" }, + { status: 500 }, + ); } } @@ -59,10 +64,15 @@ export async function GET(request: NextRequest): Promise { export async function POST(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); - const botToken = await telegramAutomationService.getBotToken(user.organization_id); + const botToken = await telegramAutomationService.getBotToken( + user.organization_id, + ); if (!botToken) { - return NextResponse.json({ error: "Telegram bot not connected" }, { status: 400 }); + return NextResponse.json( + { error: "Telegram bot not connected" }, + { status: 400 }, + ); } const bot = new Telegraf(botToken); @@ -143,7 +153,9 @@ export async function POST(request: NextRequest): Promise { if ( chat && !seenChatIds.has(chat.id) && - (chat.type === "group" || chat.type === "supergroup" || chat.type === "channel") + (chat.type === "group" || + chat.type === "supergroup" || + chat.type === "channel") ) { seenChatIds.add(chat.id); @@ -152,8 +164,10 @@ export async function POST(request: NextRequest): Promise { let canPost = false; try { const member = await bot.telegram.getChatMember(chat.id, botInfo.id); - isAdmin = member.status === "administrator" || member.status === "creator"; - canPost = isAdmin || (member.status === "member" && chat.type !== "channel"); + isAdmin = + member.status === "administrator" || member.status === "creator"; + canPost = + isAdmin || (member.status === "member" && chat.type !== "channel"); } catch { // If we can't check membership, assume basic permissions for groups canPost = chat.type !== "channel"; @@ -180,17 +194,28 @@ export async function POST(request: NextRequest): Promise { } // Also refresh existing chats' status - const existingChats = await telegramChatsRepository.findByOrganization(user.organization_id); + const existingChats = await telegramChatsRepository.findByOrganization( + user.organization_id, + ); for (const existingChat of existingChats) { if (!seenChatIds.has(existingChat.chat_id)) { try { - const member = await bot.telegram.getChatMember(existingChat.chat_id, botInfo.id); - const isAdmin = member.status === "administrator" || member.status === "creator"; + const member = await bot.telegram.getChatMember( + existingChat.chat_id, + botInfo.id, + ); + const isAdmin = + member.status === "administrator" || member.status === "creator"; const canPost = - isAdmin || (member.status === "member" && existingChat.chat_type !== "channel"); + isAdmin || + (member.status === "member" && + existingChat.chat_type !== "channel"); // Update if permissions changed - if (existingChat.is_admin !== isAdmin || existingChat.can_post_messages !== canPost) { + if ( + existingChat.is_admin !== isAdmin || + existingChat.can_post_messages !== canPost + ) { await telegramChatsRepository.upsert({ organization_id: user.organization_id, chat_id: existingChat.chat_id, @@ -208,7 +233,9 @@ export async function POST(request: NextRequest): Promise { } // Re-set the webhook using the centralized service (ensures secret_token is included) - const webhookResult = await telegramAutomationService.setWebhook(user.organization_id); + const webhookResult = await telegramAutomationService.setWebhook( + user.organization_id, + ); if (webhookResult.success) { logger.info("[Telegram Scan] Webhook set via service", { organizationId: user.organization_id, @@ -226,7 +253,9 @@ export async function POST(request: NextRequest): Promise { }); // Fetch all chats for this org - const allChats = await telegramChatsRepository.findByOrganization(user.organization_id); + const allChats = await telegramChatsRepository.findByOrganization( + user.organization_id, + ); return NextResponse.json({ success: true, @@ -248,7 +277,8 @@ export async function POST(request: NextRequest): Promise { return NextResponse.json( { - error: error instanceof Error ? error.message : "Failed to scan for chats", + error: + error instanceof Error ? error.message : "Failed to scan for chats", }, { status: 500 }, ); diff --git a/app/api/v1/telegram/status/route.ts b/app/api/v1/telegram/status/route.ts index 33b88609e..cd875ce71 100644 --- a/app/api/v1/telegram/status/route.ts +++ b/app/api/v1/telegram/status/route.ts @@ -16,10 +16,13 @@ export const maxDuration = 30; export async function GET(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); - const status = await telegramAutomationService.getConnectionStatus(user.organization_id); + const status = await telegramAutomationService.getConnectionStatus( + user.organization_id, + ); // Optionally include webhook info for debugging - const includeWebhookInfo = request.nextUrl.searchParams.get("webhook") === "true"; + const includeWebhookInfo = + request.nextUrl.searchParams.get("webhook") === "true"; let webhookInfo: { url?: string; @@ -33,7 +36,9 @@ export async function GET(request: NextRequest): Promise { } | null = null; if (includeWebhookInfo && status.connected) { - const botToken = await telegramAutomationService.getBotToken(user.organization_id); + const botToken = await telegramAutomationService.getBotToken( + user.organization_id, + ); if (botToken) { const bot = new Telegraf(botToken); const info = await bot.telegram.getWebhookInfo(); diff --git a/app/api/v1/telegram/webhook/[orgId]/route.ts b/app/api/v1/telegram/webhook/[orgId]/route.ts index cd4e175d2..a07eec5e7 100644 --- a/app/api/v1/telegram/webhook/[orgId]/route.ts +++ b/app/api/v1/telegram/webhook/[orgId]/route.ts @@ -48,7 +48,9 @@ async function handleTelegramWebhook( } else if (process.env.NODE_ENV === "production") { // No secret configured - org doesn't have telegram set up // Return 200 OK to stop Telegram from retrying (webhook was likely orphaned) - logger.warn("[Telegram Webhook] No webhook secret configured - ignoring", { orgId }); + logger.warn("[Telegram Webhook] No webhook secret configured - ignoring", { + orgId, + }); return NextResponse.json({ ok: true, status: "not_configured" }); } @@ -58,7 +60,9 @@ async function handleTelegramWebhook( if (!botToken && process.env.NODE_ENV === "development") { botToken = process.env.TELEGRAM_BOT_TOKEN || null; if (botToken) { - logger.info("[Telegram Webhook] Using fallback bot token from env", { orgId }); + logger.info("[Telegram Webhook] Using fallback bot token from env", { + orgId, + }); } } @@ -88,7 +92,8 @@ async function handleTelegramWebhook( } const bot = new Telegraf(botToken); - const activeApps = await telegramAppAutomationService.getAppsWithActiveAutomation(orgId); + const activeApps = + await telegramAppAutomationService.getAppsWithActiveAutomation(orgId); setupBotHandlers(bot, orgId, activeApps); @@ -107,7 +112,10 @@ async function handleTelegramWebhook( // Export POST handler with rate limiting (100 requests/min per IP) // Uses AGGRESSIVE preset for webhook endpoints -export const POST = withRateLimit(handleTelegramWebhook, RateLimitPresets.AGGRESSIVE); +export const POST = withRateLimit( + handleTelegramWebhook, + RateLimitPresets.AGGRESSIVE, +); /** * Track a chat from a regular message/post. @@ -119,7 +127,11 @@ async function trackChatFromMessage( botToken: string, ): Promise { // Only track groups, supergroups, and channels - if (chat.type !== "channel" && chat.type !== "group" && chat.type !== "supergroup") { + if ( + chat.type !== "channel" && + chat.type !== "group" && + chat.type !== "supergroup" + ) { return; } @@ -135,8 +147,10 @@ async function trackChatFromMessage( const botInfo = await bot.telegram.getMe(); const member = await bot.telegram.getChatMember(chat.id, botInfo.id); - const isAdmin = member.status === "administrator" || member.status === "creator"; - const canPost = isAdmin || (member.status === "member" && chat.type !== "channel"); + const isAdmin = + member.status === "administrator" || member.status === "creator"; + const canPost = + isAdmin || (member.status === "member" && chat.type !== "channel"); await telegramChatsRepository.upsert({ organization_id: orgId, @@ -166,18 +180,26 @@ async function trackChatFromMessage( } } -async function handleChatMemberUpdate(orgId: string, update: ChatMemberUpdated): Promise { +async function handleChatMemberUpdate( + orgId: string, + update: ChatMemberUpdated, +): Promise { const chat = update.chat; const newStatus = update.new_chat_member.status; // Only track channels, groups, and supergroups - if (chat.type !== "channel" && chat.type !== "group" && chat.type !== "supergroup") { + if ( + chat.type !== "channel" && + chat.type !== "group" && + chat.type !== "supergroup" + ) { return; } const isAdmin = newStatus === "administrator" || newStatus === "creator"; const isMember = isAdmin || newStatus === "member"; - const canPost = isAdmin || (newStatus === "member" && chat.type !== "channel"); + const canPost = + isAdmin || (newStatus === "member" && chat.type !== "channel"); if (isMember) { await telegramChatsRepository.upsert({ @@ -227,7 +249,9 @@ function setupBotHandlers(bot: Telegraf, orgId: string, activeApps: App[]) { `Welcome to ${matchingApp.name}! I'm here to help you.`; await ctx.reply(welcomeMessage); } else { - await ctx.reply(`Hello ${userName}! 👋 I'm an AI assistant. How can I help you today?`); + await ctx.reply( + `Hello ${userName}! 👋 I'm an AI assistant. How can I help you today?`, + ); } logger.info("[Telegram Webhook] Start command handled", { @@ -287,12 +311,16 @@ ${matchingApp.website_url ? `🌐 Website: ${matchingApp.website_url}` : ""}`; if (matchingApp?.telegram_automation?.autoReply) { try { - await telegramAppAutomationService.handleIncomingMessage(orgId, matchingApp.id, { - chatId, - messageId: message.message_id, - text, - userName, - }); + await telegramAppAutomationService.handleIncomingMessage( + orgId, + matchingApp.id, + { + chatId, + messageId: message.message_id, + text, + userName, + }, + ); } catch (error) { logger.error("[Telegram Webhook] Error handling message", { orgId, @@ -302,7 +330,9 @@ ${matchingApp.website_url ? `🌐 Website: ${matchingApp.website_url}` : ""}`; }); } } else if (!matchingApp) { - await ctx.reply("Thanks for your message! This bot is configured for specific applications."); + await ctx.reply( + "Thanks for your message! This bot is configured for specific applications.", + ); } }); diff --git a/app/api/v1/track/pageview/route.ts b/app/api/v1/track/pageview/route.ts index c9078eb07..4e57716d2 100644 --- a/app/api/v1/track/pageview/route.ts +++ b/app/api/v1/track/pageview/route.ts @@ -33,7 +33,11 @@ const CORS_HEADERS = { "Access-Control-Max-Age": "86400", }; -function detectSource(origin: string, referer: string, pageUrl: string): string { +function detectSource( + origin: string, + referer: string, + pageUrl: string, +): string { const combined = `${origin} ${referer} ${pageUrl}`.toLowerCase(); if ( @@ -138,7 +142,10 @@ export async function POST(req: NextRequest) { responseTimeMs: Date.now() - startTime, }); - return NextResponse.json({ success: true }, { status: 200, headers: CORS_HEADERS }); + return NextResponse.json( + { success: true }, + { status: 200, headers: CORS_HEADERS }, + ); } catch (error) { logger.error("[Track] Failed to record page view:", error); return NextResponse.json( diff --git a/app/api/v1/twilio/connect/route.ts b/app/api/v1/twilio/connect/route.ts index 0e969b95b..769a4b3f7 100644 --- a/app/api/v1/twilio/connect/route.ts +++ b/app/api/v1/twilio/connect/route.ts @@ -34,13 +34,18 @@ export async function POST(request: NextRequest): Promise { } if (!body || typeof body !== "object" || Array.isArray(body)) { - return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); + return NextResponse.json( + { error: "Request body must be a JSON object" }, + { status: 400 }, + ); } const parsedBody = twilioConnectSchema.safeParse(body); if (!parsedBody.success) { return NextResponse.json( - { error: parsedBody.error.issues[0]?.message || "Invalid request body" }, + { + error: parsedBody.error.issues[0]?.message || "Invalid request body", + }, { status: 400 }, ); } @@ -56,7 +61,10 @@ export async function POST(request: NextRequest): Promise { } // Validate the credentials - const validation = await twilioAutomationService.validateCredentials(accountSid, authToken); + const validation = await twilioAutomationService.validateCredentials( + accountSid, + authToken, + ); if (!validation.valid) { return NextResponse.json( @@ -66,14 +74,20 @@ export async function POST(request: NextRequest): Promise { } // Store credentials - await twilioAutomationService.storeCredentials(user.organization_id, user.id, { - accountSid, - authToken, - phoneNumber, - }); + await twilioAutomationService.storeCredentials( + user.organization_id, + user.id, + { + accountSid, + authToken, + phoneNumber, + }, + ); // Get the webhook URL to display to user - const webhookUrl = twilioAutomationService.getWebhookUrl(user.organization_id); + const webhookUrl = twilioAutomationService.getWebhookUrl( + user.organization_id, + ); await invalidateOAuthState(user.organization_id, "twilio", user.id); @@ -98,6 +112,9 @@ export async function POST(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to connect Twilio" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to connect Twilio" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/twilio/disconnect/route.ts b/app/api/v1/twilio/disconnect/route.ts index 9db05da3b..4d41a32f7 100644 --- a/app/api/v1/twilio/disconnect/route.ts +++ b/app/api/v1/twilio/disconnect/route.ts @@ -18,7 +18,10 @@ async function handleDisconnect(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); try { - await twilioAutomationService.removeCredentials(user.organization_id, user.id); + await twilioAutomationService.removeCredentials( + user.organization_id, + user.id, + ); await invalidateOAuthState(user.organization_id, "twilio", user.id); @@ -36,7 +39,10 @@ async function handleDisconnect(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to disconnect Twilio" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to disconnect Twilio" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/twilio/status/route.ts b/app/api/v1/twilio/status/route.ts index f873e77a2..1e219ee49 100644 --- a/app/api/v1/twilio/status/route.ts +++ b/app/api/v1/twilio/status/route.ts @@ -23,7 +23,9 @@ export async function GET(request: NextRequest): Promise { ]); // Include webhook URL for reference - const webhookUrl = twilioAutomationService.getWebhookUrl(user.organization_id); + const webhookUrl = twilioAutomationService.getWebhookUrl( + user.organization_id, + ); // Map properties for frontend compatibility: // - `configured` -> `webhookConfigured` @@ -40,6 +42,9 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to get Twilio status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get Twilio status" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/twilio/voice/inbound/route.ts b/app/api/v1/twilio/voice/inbound/route.ts index 40119eb4e..dd9834a50 100644 --- a/app/api/v1/twilio/voice/inbound/route.ts +++ b/app/api/v1/twilio/voice/inbound/route.ts @@ -43,7 +43,10 @@ export function verifyTwilioSignature( ): boolean { const sorted = Object.keys(params).sort(); const data = url + sorted.map((key) => `${key}${params[key]}`).join(""); - const computed = crypto.createHmac("sha1", authToken).update(data).digest("base64"); + const computed = crypto + .createHmac("sha1", authToken) + .update(data) + .digest("base64"); const a = Buffer.from(computed, "utf8"); const b = Buffer.from(signature, "utf8"); if (a.length !== b.length) return false; @@ -73,13 +76,20 @@ export async function POST(request: NextRequest): Promise { const authToken = process.env.TWILIO_AUTH_TOKEN?.trim(); if (!authToken) { - logger.warn("[twilio-voice-inbound] TWILIO_AUTH_TOKEN not configured — refusing call"); - return new NextResponse("Twilio auth token not configured", { status: 503 }); + logger.warn( + "[twilio-voice-inbound] TWILIO_AUTH_TOKEN not configured — refusing call", + ); + return new NextResponse("Twilio auth token not configured", { + status: 503, + }); } const signature = request.headers.get("x-twilio-signature") ?? ""; const fullUrl = resolveForwardedUrl(request); - if (!signature || !verifyTwilioSignature(authToken, signature, fullUrl, params)) { + if ( + !signature || + !verifyTwilioSignature(authToken, signature, fullUrl, params) + ) { logger.warn("[twilio-voice-inbound] signature verification failed", { url: fullUrl, }); diff --git a/app/api/v1/twitter/callback/route.ts b/app/api/v1/twitter/callback/route.ts index 7f7cbc0d2..90991e5e8 100644 --- a/app/api/v1/twitter/callback/route.ts +++ b/app/api/v1/twitter/callback/route.ts @@ -14,11 +14,19 @@ export async function GET(request: NextRequest): Promise { const oauthVerifier = searchParams.get("oauth_verifier"); const denied = searchParams.get("denied"); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const defaultRedirectPath = "/dashboard/settings?tab=connections"; - function buildRedirectUrl(redirectUrl: string | undefined, params: Record): URL { - const target = resolveSafeRedirectTarget(redirectUrl, baseUrl, defaultRedirectPath); + function buildRedirectUrl( + redirectUrl: string | undefined, + params: Record, + ): URL { + const target = resolveSafeRedirectTarget( + redirectUrl, + baseUrl, + defaultRedirectPath, + ); Object.entries(params).forEach(([key, value]) => { target.searchParams.set(key, value); @@ -63,7 +71,8 @@ export async function GET(request: NextRequest): Promise { }; try { - const parsed = typeof stateData === "string" ? JSON.parse(stateData) : stateData; + const parsed = + typeof stateData === "string" ? JSON.parse(stateData) : stateData; // Validate required fields exist if ( @@ -112,12 +121,16 @@ export async function GET(request: NextRequest): Promise { } try { - await twitterAutomationService.storeCredentials(state.organizationId, state.userId, { - accessToken: tokens.accessToken, - accessSecret: tokens.accessSecret, - screenName: tokens.screenName, - twitterUserId: tokens.userId, - }); + await twitterAutomationService.storeCredentials( + state.organizationId, + state.userId, + { + accessToken: tokens.accessToken, + accessSecret: tokens.accessSecret, + screenName: tokens.screenName, + twitterUserId: tokens.userId, + }, + ); } catch (error) { logger.error("[Twitter Callback] Failed to store credentials", { error: error instanceof Error ? error.message : String(error), diff --git a/app/api/v1/twitter/connect/route.ts b/app/api/v1/twitter/connect/route.ts index a04e40d6c..9d64c76d1 100644 --- a/app/api/v1/twitter/connect/route.ts +++ b/app/api/v1/twitter/connect/route.ts @@ -19,7 +19,8 @@ export async function POST(request: NextRequest): Promise { } const body = await request.json().catch(() => ({})); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const defaultRedirectPath = "/dashboard/settings?tab=connections"; const safeRedirectTarget = resolveSafeRedirectTarget( typeof body.redirectUrl === "string" ? body.redirectUrl : undefined, diff --git a/app/api/v1/twitter/disconnect/route.ts b/app/api/v1/twitter/disconnect/route.ts index 4f91faae0..f98a3fadd 100644 --- a/app/api/v1/twitter/disconnect/route.ts +++ b/app/api/v1/twitter/disconnect/route.ts @@ -8,7 +8,10 @@ export const dynamic = "force-dynamic"; export async function DELETE(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); - await twitterAutomationService.removeCredentials(user.organization_id, user.id); + await twitterAutomationService.removeCredentials( + user.organization_id, + user.id, + ); await invalidateOAuthState(user.organization_id, "twitter", user.id); diff --git a/app/api/v1/twitter/status/route.ts b/app/api/v1/twitter/status/route.ts index 66a621bcf..474c09672 100644 --- a/app/api/v1/twitter/status/route.ts +++ b/app/api/v1/twitter/status/route.ts @@ -16,7 +16,9 @@ export async function GET(request: NextRequest): Promise { }); } - const status = await twitterAutomationService.getConnectionStatus(user.organization_id); + const status = await twitterAutomationService.getConnectionStatus( + user.organization_id, + ); return NextResponse.json({ configured: true, diff --git a/app/api/v1/user/route.ts b/app/api/v1/user/route.ts index 1af16f889..9c84c7085 100644 --- a/app/api/v1/user/route.ts +++ b/app/api/v1/user/route.ts @@ -11,7 +11,15 @@ const updateUserSchema = z.object({ avatar: z.string().url().optional().or(z.literal("")), nickname: z.string().max(50).optional(), work_function: z - .enum(["developer", "designer", "product", "data", "marketing", "sales", "other"]) + .enum([ + "developer", + "designer", + "product", + "data", + "marketing", + "sales", + "other", + ]) .optional(), preferences: z.string().max(1000).optional(), response_notifications: z.boolean().optional(), @@ -63,7 +71,10 @@ async function handleGET(request: NextRequest) { return NextResponse.json( { success: false, - error: status === 500 ? "Failed to fetch user data" : getSafeErrorMessage(error), + error: + status === 500 + ? "Failed to fetch user data" + : getSafeErrorMessage(error), }, { status }, ); diff --git a/app/api/v1/user/wallets/provision/route.ts b/app/api/v1/user/wallets/provision/route.ts index 6438c7d39..0090c0921 100644 --- a/app/api/v1/user/wallets/provision/route.ts +++ b/app/api/v1/user/wallets/provision/route.ts @@ -26,7 +26,10 @@ const provisionWalletSchema = z path: ["clientAddress"], }); } - if (data.chainType === "solana" && !SOLANA_BASE58.test(data.clientAddress)) { + if ( + data.chainType === "solana" && + !SOLANA_BASE58.test(data.clientAddress) + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid Solana address (base58, 32–44 chars)", diff --git a/app/api/v1/user/wallets/rpc/route.ts b/app/api/v1/user/wallets/rpc/route.ts index 9dd83fb6d..9fd54e8b5 100644 --- a/app/api/v1/user/wallets/rpc/route.ts +++ b/app/api/v1/user/wallets/rpc/route.ts @@ -30,11 +30,15 @@ async function handlePOST(request: NextRequest) { } const authenticatedWallet = authenticatedUser.wallet_address?.toLowerCase(); - if (!authenticatedWallet || authenticatedWallet !== validated.clientAddress.toLowerCase()) { + if ( + !authenticatedWallet || + authenticatedWallet !== validated.clientAddress.toLowerCase() + ) { return NextResponse.json( { success: false, - error: "Unauthorized: clientAddress does not belong to the authenticated wallet", + error: + "Unauthorized: clientAddress does not belong to the authenticated wallet", }, { status: 403 }, ); @@ -72,23 +76,38 @@ async function handlePOST(request: NextRequest) { error.message.includes("Signature timestamp expired") || error.message.includes("Service temporarily unavailable")) ) { - return NextResponse.json({ success: false, error: error.message }, { status: 401 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 401 }, + ); } if (error instanceof Error && error.name === "RpcRequestExpiredError") { - return NextResponse.json({ success: false, error: error.message }, { status: 400 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 400 }, + ); } if (error instanceof Error && error.name === "RpcReplayError") { - return NextResponse.json({ success: false, error: error.message }, { status: 409 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 409 }, + ); } if (error instanceof Error && error.name === "InvalidRpcSignatureError") { - return NextResponse.json({ success: false, error: error.message }, { status: 401 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 401 }, + ); } if (error instanceof Error && error.name === "ServerWalletNotFoundError") { - return NextResponse.json({ success: false, error: error.message }, { status: 404 }); + return NextResponse.json( + { success: false, error: error.message }, + { status: 404 }, + ); } return NextResponse.json( diff --git a/app/api/v1/voice/[id]/route.ts b/app/api/v1/voice/[id]/route.ts index 71d35a840..8b18e4d7e 100644 --- a/app/api/v1/voice/[id]/route.ts +++ b/app/api/v1/voice/[id]/route.ts @@ -27,7 +27,8 @@ import { requireAuthOrApiKeyWithOrg } from "@/lib/auth"; import { voiceCloningService } from "@/lib/services/voice-cloning"; import { logger } from "@/lib/utils/logger"; -const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function isValidVoiceId(voiceId: string) { return uuidRegex.test(voiceId); @@ -37,14 +38,18 @@ function createInvalidVoiceIdResponse() { return NextResponse.json( { error: "Invalid voice ID format", - message: "Please use the internal voice ID (UUID format) from the 'List Voices' endpoint.", + message: + "Please use the internal voice ID (UUID format) from the 'List Voices' endpoint.", hint: "Call GET /api/v1/voice/list to get your voice IDs", }, { status: 400 }, ); } -function getInvalidVoiceIdResponseIfNeeded(voiceId: string, logMessage: string) { +function getInvalidVoiceIdResponseIfNeeded( + voiceId: string, + logMessage: string, +) { if (isValidVoiceId(voiceId)) { return null; } @@ -56,7 +61,8 @@ function getInvalidVoiceIdResponseIfNeeded(voiceId: string, logMessage: string) function isInvalidVoiceIdError(error: unknown) { return ( error instanceof Error && - (error.message.includes("invalid input syntax for type uuid") || error.message.includes("uuid")) + (error.message.includes("invalid input syntax for type uuid") || + error.message.includes("uuid")) ); } @@ -69,7 +75,10 @@ function isInvalidVoiceIdError(error: unknown) { * @param context - Route context containing the voice ID parameter. * @returns Voice details including provider voice ID and metadata. */ -export async function GET(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function GET( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -85,7 +94,10 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: return invalidVoiceIdResponse; } - const voice = await voiceCloningService.getVoiceById(voiceId, user.organization_id); + const voice = await voiceCloningService.getVoiceById( + voiceId, + user.organization_id, + ); if (!voice) { return NextResponse.json( @@ -122,7 +134,10 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id: * @param context - Route context containing the voice ID parameter. * @returns Success confirmation. */ -export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function DELETE( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -183,7 +198,10 @@ export async function DELETE(request: NextRequest, context: { params: Promise<{ * @param context - Route context containing the voice ID parameter. * @returns Updated voice details. */ -export async function PATCH(request: NextRequest, context: { params: Promise<{ id: string }> }) { +export async function PATCH( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { try { const { user } = await requireAuthOrApiKeyWithOrg(request); const params = await context.params; @@ -202,12 +220,16 @@ export async function PATCH(request: NextRequest, context: { params: Promise<{ i const body = await request.json(); const { name, description, settings, isActive } = body; - const updatedVoice = await voiceCloningService.updateVoice(voiceId, user.organization_id, { - name, - description, - settings, - isActive, - }); + const updatedVoice = await voiceCloningService.updateVoice( + voiceId, + user.organization_id, + { + name, + description, + settings, + isActive, + }, + ); return NextResponse.json({ success: true, diff --git a/app/api/v1/voice/clone/route.ts b/app/api/v1/voice/clone/route.ts index cf5a6be50..656e1e684 100644 --- a/app/api/v1/voice/clone/route.ts +++ b/app/api/v1/voice/clone/route.ts @@ -98,15 +98,24 @@ export async function POST(request: NextRequest) { } if (files.length === 0) { - return NextResponse.json({ error: "At least one audio file is required" }, { status: 400 }); + return NextResponse.json( + { error: "At least one audio file is required" }, + { status: 400 }, + ); } if (files.length > MAX_FILES) { - return NextResponse.json({ error: `Maximum ${MAX_FILES} files allowed` }, { status: 400 }); + return NextResponse.json( + { error: `Maximum ${MAX_FILES} files allowed` }, + { status: 400 }, + ); } if (totalSize > MAX_TOTAL_SIZE) { - return NextResponse.json({ error: "Total file size exceeds 100MB limit" }, { status: 400 }); + return NextResponse.json( + { error: "Total file size exceeds 100MB limit" }, + { status: 400 }, + ); } let settings: Record = {}; @@ -114,16 +123,22 @@ export async function POST(request: NextRequest) { try { settings = JSON.parse(settingsStr); } catch { - return NextResponse.json({ error: "Invalid settings JSON" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid settings JSON" }, + { status: 400 }, + ); } } - logger.info(`[Voice Clone API] Creating ${cloneType} voice clone: ${name}`, { - userId: user.id, - organizationId: user.organization_id, - fileCount: files.length, - totalSize, - }); + logger.info( + `[Voice Clone API] Creating ${cloneType} voice clone: ${name}`, + { + userId: user.id, + organizationId: user.organization_id, + fileCount: files.length, + totalSize, + }, + ); const cloneCost = await calculateVoiceCloneCostFromCatalog({ cloneType }); const cost = cloneCost.totalCost; @@ -244,7 +259,8 @@ export async function POST(request: NextRequest) { progress: result.job.progress, }, creditsDeducted: cost, - estimatedCompletionTime: cloneType === "professional" ? "30-60 minutes" : "30 seconds", + estimatedCompletionTime: + cloneType === "professional" ? "30-60 minutes" : "30 seconds", }, { status: 201 }, ); @@ -274,11 +290,15 @@ export async function POST(request: NextRequest) { input_cost: String(0), output_cost: String(0), is_successful: false, - error_message: error instanceof Error ? error.message : "Unknown error", + error_message: + error instanceof Error ? error.message : "Unknown error", }); } catch (usageError) { logger.error("[Voice Clone API] Failed to record usage", { - error: usageError instanceof Error ? usageError.message : "Unknown error", + error: + usageError instanceof Error + ? usageError.message + : "Unknown error", }); } })(); diff --git a/app/api/v1/voice/jobs/route.ts b/app/api/v1/voice/jobs/route.ts index f75e6219a..7b8763c8c 100644 --- a/app/api/v1/voice/jobs/route.ts +++ b/app/api/v1/voice/jobs/route.ts @@ -39,7 +39,10 @@ export async function GET(request: NextRequest) { logger.info(`[Voice Jobs API] Fetching jobs for user ${user.id}`); - const allJobs = await voiceCloningService.getUserJobs(user.organization_id, user.id); + const allJobs = await voiceCloningService.getUserJobs( + user.organization_id, + user.id, + ); const activeJobs = allJobs.filter( (job) => job.status === "processing" || job.status === "pending", diff --git a/app/api/v1/voice/list/route.ts b/app/api/v1/voice/list/route.ts index dd6992946..c81a196b1 100644 --- a/app/api/v1/voice/list/route.ts +++ b/app/api/v1/voice/list/route.ts @@ -45,11 +45,17 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const includeInactive = searchParams.get("includeInactive") === "true"; - const cloneType = searchParams.get("cloneType") as "instant" | "professional" | undefined; + const cloneType = searchParams.get("cloneType") as + | "instant" + | "professional" + | undefined; const MAX_LIMIT = 100; const rawLimit = Number.parseInt(searchParams.get("limit") || "50", 10); const rawOffset = Number.parseInt(searchParams.get("offset") || "0", 10); - const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 50 : rawLimit, 1), MAX_LIMIT); + const limit = Math.min( + Math.max(Number.isNaN(rawLimit) ? 50 : rawLimit, 1), + MAX_LIMIT, + ); const offset = Math.max(Number.isNaN(rawOffset) ? 0 : rawOffset, 0); logger.info(`[Voice List API] Fetching voices for user ${user.id}`, { diff --git a/app/api/v1/voice/stt/route.ts b/app/api/v1/voice/stt/route.ts index 851dcca3f..f769db248 100644 --- a/app/api/v1/voice/stt/route.ts +++ b/app/api/v1/voice/stt/route.ts @@ -61,7 +61,10 @@ const ALLOWED_AUDIO_SIGNATURES = new Set([ "video/webm", // Safari/macOS creates this for audio recordings ]); -function estimateAudioDurationMinutes(fileSizeBytes: number, mimeType: string): number { +function estimateAudioDurationMinutes( + fileSizeBytes: number, + mimeType: string, +): number { const bitratesKbps: Record = { "audio/mpeg": 128, "audio/mp3": 128, @@ -100,7 +103,10 @@ export async function POST(request: NextRequest) { const languageCode = formData.get("languageCode") as string | undefined; if (!audioFile) { - return NextResponse.json({ error: "No audio file provided" }, { status: 400 }); + return NextResponse.json( + { error: "No audio file provided" }, + { status: 400 }, + ); } if (audioFile.size > MAX_FILE_SIZE) { @@ -126,7 +132,9 @@ export async function POST(request: NextRequest) { const fileTypeResult = await fileTypeFromBuffer(buffer); if (!fileTypeResult) { - logger.warn(`[Voice STT API] Unable to detect file type for ${audioFile.name} - rejecting`); + logger.warn( + `[Voice STT API] Unable to detect file type for ${audioFile.name} - rejecting`, + ); return NextResponse.json( { error: @@ -164,7 +172,10 @@ export async function POST(request: NextRequest) { const parsedDurationSeconds = metadata.format.duration; const durationSeconds = Number.isFinite(parsedDurationSeconds) ? Math.max(parsedDurationSeconds ?? 0, 1) - : Math.max(estimateAudioDurationMinutes(audioFile.size, finalMimeType) * 60, 1); + : Math.max( + estimateAudioDurationMinutes(audioFile.size, finalMimeType) * 60, + 1, + ); const estimatedDurationMinutes = durationSeconds / 60; const sttCost = await calculateSTTCostFromCatalog({ model: "elevenlabs/scribe_v1", @@ -217,7 +228,9 @@ export async function POST(request: NextRequest) { reservation, ); - logger.info(`[Voice STT API] Completed in ${duration}ms: "${transcript.substring(0, 100)}..."`); + logger.info( + `[Voice STT API] Completed in ${duration}ms: "${transcript.substring(0, 100)}..."`, + ); (async () => { try { @@ -299,7 +312,8 @@ export async function POST(request: NextRequest) { ) { return NextResponse.json( { - error: "Speech-to-Text requires a paid plan. Please upgrade to continue.", + error: + "Speech-to-Text requires a paid plan. Please upgrade to continue.", }, { status: 402 }, ); @@ -316,7 +330,10 @@ export async function POST(request: NextRequest) { } if (errorMessage.includes("elevenlabs_api_key")) { - return NextResponse.json({ error: "Service not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Service not configured" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/voice/tts/route.ts b/app/api/v1/voice/tts/route.ts index 7ffb86e88..35cd52ba3 100644 --- a/app/api/v1/voice/tts/route.ts +++ b/app/api/v1/voice/tts/route.ts @@ -65,7 +65,10 @@ export async function POST(request: NextRequest) { } if (text.length === 0) { - return NextResponse.json({ error: "Text cannot be empty" }, { status: 400 }); + return NextResponse.json( + { error: "Text cannot be empty" }, + { status: 400 }, + ); } if (text.length > MAX_TEXT_LENGTH) { @@ -77,7 +80,9 @@ export async function POST(request: NextRequest) { ); } - logger.info(`[Voice TTS API] Generating speech for user ${user.id}: ${text.length} chars`); + logger.info( + `[Voice TTS API] Generating speech for user ${user.id}: ${text.length} chars`, + ); let userVoiceId: string | null = null; let voiceName: string | null = null; @@ -119,7 +124,8 @@ export async function POST(request: NextRequest) { characterCount: text.length, }); const estimatedCost = isCustomVoice - ? Math.round(ttsCost.totalCost * CUSTOM_VOICE_TTS_MARKUP * 1_000_000) / 1_000_000 + ? Math.round(ttsCost.totalCost * CUSTOM_VOICE_TTS_MARKUP * 1_000_000) / + 1_000_000 : ttsCost.totalCost; try { @@ -167,10 +173,14 @@ export async function POST(request: NextRequest) { { totalCost: estimatedCost, baseTotalCost: isCustomVoice - ? Math.round(ttsCost.baseTotalCost * CUSTOM_VOICE_TTS_MARKUP * 1_000_000) / 1_000_000 + ? Math.round( + ttsCost.baseTotalCost * CUSTOM_VOICE_TTS_MARKUP * 1_000_000, + ) / 1_000_000 : ttsCost.baseTotalCost, platformMarkup: isCustomVoice - ? Math.round(ttsCost.platformMarkup * CUSTOM_VOICE_TTS_MARKUP * 1_000_000) / 1_000_000 + ? Math.round( + ttsCost.platformMarkup * CUSTOM_VOICE_TTS_MARKUP * 1_000_000, + ) / 1_000_000 : ttsCost.platformMarkup, }, reservation, @@ -271,7 +281,10 @@ export async function POST(request: NextRequest) { } if (errorMessage.includes("elevenlabs_api_key")) { - return NextResponse.json({ error: "Service not configured" }, { status: 500 }); + return NextResponse.json( + { error: "Service not configured" }, + { status: 500 }, + ); } return NextResponse.json( diff --git a/app/api/v1/whatsapp/connect/route.ts b/app/api/v1/whatsapp/connect/route.ts index 47bf5370b..8ea176d75 100644 --- a/app/api/v1/whatsapp/connect/route.ts +++ b/app/api/v1/whatsapp/connect/route.ts @@ -23,15 +23,24 @@ export async function POST(request: NextRequest): Promise { const { accessToken, phoneNumberId, appSecret, businessPhone } = body; if (!accessToken) { - return NextResponse.json({ error: "Access token is required" }, { status: 400 }); + return NextResponse.json( + { error: "Access token is required" }, + { status: 400 }, + ); } if (!phoneNumberId) { - return NextResponse.json({ error: "Phone Number ID is required" }, { status: 400 }); + return NextResponse.json( + { error: "Phone Number ID is required" }, + { status: 400 }, + ); } if (!appSecret) { - return NextResponse.json({ error: "App Secret is required" }, { status: 400 }); + return NextResponse.json( + { error: "App Secret is required" }, + { status: 400 }, + ); } // Validate the access token by calling Meta Graph API @@ -51,16 +60,22 @@ export async function POST(request: NextRequest): Promise { const verifyToken = whatsappAutomationService.generateVerifyToken(); // Store credentials - await whatsappAutomationService.storeCredentials(user.organization_id, user.id, { - accessToken, - phoneNumberId, - appSecret, - verifyToken, - businessPhone: businessPhone || validation.phoneDisplay, - }); + await whatsappAutomationService.storeCredentials( + user.organization_id, + user.id, + { + accessToken, + phoneNumberId, + appSecret, + verifyToken, + businessPhone: businessPhone || validation.phoneDisplay, + }, + ); // Get the webhook URL to display to user - const webhookUrl = whatsappAutomationService.getWebhookUrl(user.organization_id); + const webhookUrl = whatsappAutomationService.getWebhookUrl( + user.organization_id, + ); logger.info("[WhatsApp Connect] Credentials stored", { organizationId: user.organization_id, @@ -82,6 +97,9 @@ export async function POST(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to connect WhatsApp" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to connect WhatsApp" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/whatsapp/disconnect/route.ts b/app/api/v1/whatsapp/disconnect/route.ts index 29139bd26..66b166712 100644 --- a/app/api/v1/whatsapp/disconnect/route.ts +++ b/app/api/v1/whatsapp/disconnect/route.ts @@ -17,7 +17,10 @@ async function handleDisconnect(request: NextRequest): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); try { - await whatsappAutomationService.removeCredentials(user.organization_id, user.id); + await whatsappAutomationService.removeCredentials( + user.organization_id, + user.id, + ); logger.info("[WhatsApp Disconnect] Credentials removed", { organizationId: user.organization_id, @@ -33,7 +36,10 @@ async function handleDisconnect(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), organizationId: user.organization_id, }); - return NextResponse.json({ error: "Failed to disconnect WhatsApp" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to disconnect WhatsApp" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/whatsapp/status/route.ts b/app/api/v1/whatsapp/status/route.ts index 748e1121d..204e0a247 100644 --- a/app/api/v1/whatsapp/status/route.ts +++ b/app/api/v1/whatsapp/status/route.ts @@ -37,6 +37,9 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : String(error), orgId, }); - return NextResponse.json({ error: "Failed to get WhatsApp status" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to get WhatsApp status" }, + { status: 500 }, + ); } } diff --git a/app/api/v1/x402/settle/route.ts b/app/api/v1/x402/settle/route.ts index 541ef31c8..3b3d2f512 100644 --- a/app/api/v1/x402/settle/route.ts +++ b/app/api/v1/x402/settle/route.ts @@ -48,7 +48,8 @@ async function settleHandler(request: NextRequest): Promise { success: false, transaction: "", network: "", - errorReason: "missing_fields: paymentPayload and paymentRequirements are required", + errorReason: + "missing_fields: paymentPayload and paymentRequirements are required", }, { status: 400 }, ); @@ -57,7 +58,9 @@ async function settleHandler(request: NextRequest): Promise { try { const result = await x402FacilitatorService.settle( paymentPayload as Parameters[0], - paymentRequirements as Parameters[1], + paymentRequirements as Parameters< + typeof x402FacilitatorService.settle + >[1], ); const status = result.success ? 200 : 400; diff --git a/app/api/v1/x402/verify/route.ts b/app/api/v1/x402/verify/route.ts index 2c41e7940..5afbb8507 100644 --- a/app/api/v1/x402/verify/route.ts +++ b/app/api/v1/x402/verify/route.ts @@ -42,7 +42,8 @@ async function verifyHandler(request: NextRequest): Promise { return NextResponse.json( { isValid: false, - invalidReason: "missing_fields: paymentPayload and paymentRequirements are required", + invalidReason: + "missing_fields: paymentPayload and paymentRequirements are required", }, { status: 400 }, ); @@ -51,7 +52,9 @@ async function verifyHandler(request: NextRequest): Promise { try { const result = await x402FacilitatorService.verify( paymentPayload as Parameters[0], - paymentRequirements as Parameters[1], + paymentRequirements as Parameters< + typeof x402FacilitatorService.verify + >[1], ); const status = result.isValid ? 200 : 400; diff --git a/app/api/webhooks/blooio/[orgId]/route.ts b/app/api/webhooks/blooio/[orgId]/route.ts index 7727ccac5..d0d484df4 100644 --- a/app/api/webhooks/blooio/[orgId]/route.ts +++ b/app/api/webhooks/blooio/[orgId]/route.ts @@ -26,11 +26,17 @@ interface RouteParams { params: Promise<{ orgId: string }>; } -async function handleBlooioWebhook(request: NextRequest, context: RouteParams): Promise { +async function handleBlooioWebhook( + request: NextRequest, + context: RouteParams, +): Promise { const { orgId } = context?.params ? await context.params : { orgId: "" }; if (!orgId) { - return NextResponse.json({ error: "Organization ID is required" }, { status: 400 }); + return NextResponse.json( + { error: "Organization ID is required" }, + { status: 400 }, + ); } try { @@ -39,25 +45,45 @@ async function handleBlooioWebhook(request: NextRequest, context: RouteParams): // Verify signature - only skip if explicitly disabled AND not in production const isProduction = process.env.NODE_ENV === "production"; - const skipVerification = process.env.SKIP_WEBHOOK_VERIFICATION === "true" && !isProduction; + const skipVerification = + process.env.SKIP_WEBHOOK_VERIFICATION === "true" && !isProduction; const webhookSecret = await blooioAutomationService.getWebhookSecret(orgId); if (process.env.SKIP_WEBHOOK_VERIFICATION === "true" && isProduction) { - logger.error("[BlooioWebhook] SKIP_WEBHOOK_VERIFICATION ignored in production", { orgId }); + logger.error( + "[BlooioWebhook] SKIP_WEBHOOK_VERIFICATION ignored in production", + { orgId }, + ); } if (skipVerification) { - logger.warn("[BlooioWebhook] Signature validation disabled (non-production)", { orgId }); + logger.warn( + "[BlooioWebhook] Signature validation disabled (non-production)", + { orgId }, + ); } else if (!webhookSecret) { - logger.error("[BlooioWebhook] No webhook secret configured - rejecting webhook", { orgId }); - return NextResponse.json({ error: "Webhook not configured" }, { status: 500 }); + logger.error( + "[BlooioWebhook] No webhook secret configured - rejecting webhook", + { orgId }, + ); + return NextResponse.json( + { error: "Webhook not configured" }, + { status: 500 }, + ); } else { const signatureHeader = request.headers.get("X-Blooio-Signature") || ""; - const isValid = await verifyBlooioSignature(webhookSecret, signatureHeader, rawBody); + const isValid = await verifyBlooioSignature( + webhookSecret, + signatureHeader, + rawBody, + ); if (!isValid) { logger.warn("[BlooioWebhook] Signature validation failed", { orgId }); - return NextResponse.json({ error: "Invalid webhook signature" }, { status: 401 }); + return NextResponse.json( + { error: "Invalid webhook signature" }, + { status: 401 }, + ); } } @@ -69,7 +95,10 @@ async function handleBlooioWebhook(request: NextRequest, context: RouteParams): } catch (parseError) { if (parseError instanceof SyntaxError) { logger.warn("[BlooioWebhook] Invalid JSON payload", { orgId }); - return NextResponse.json({ error: "Invalid JSON payload" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid JSON payload" }, + { status: 400 }, + ); } if (parseError instanceof ZodError) { logger.warn("[BlooioWebhook] Invalid webhook payload schema", { @@ -96,12 +125,18 @@ async function handleBlooioWebhook(request: NextRequest, context: RouteParams): orgId, messageId: payload.message_id, }); - return NextResponse.json({ success: true, status: "already_processed" }); + return NextResponse.json({ + success: true, + status: "already_processed", + }); } } else { - logger.warn("[BlooioWebhook] No message_id in payload, skipping idempotency check", { - orgId, - }); + logger.warn( + "[BlooioWebhook] No message_id in payload, skipping idempotency check", + { + orgId, + }, + ); } // Log the event @@ -164,22 +199,32 @@ async function handleBlooioWebhook(request: NextRequest, context: RouteParams): orgId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } // Export POST handler with rate limiting (100 requests/min per IP) // Uses AGGRESSIVE preset for webhook endpoints -export const POST = withRateLimit(handleBlooioWebhook, RateLimitPresets.AGGRESSIVE); +export const POST = withRateLimit( + handleBlooioWebhook, + RateLimitPresets.AGGRESSIVE, +); /** * Handle incoming message from Blooio */ -async function handleIncomingMessage(orgId: string, event: BlooioWebhookEvent): Promise { - const [{ messageRouterService }, { miladyGatewayRouterService }] = await Promise.all([ - import("@/lib/services/message-router"), - import("@/lib/services/milady-gateway-router"), - ]); +async function handleIncomingMessage( + orgId: string, + event: BlooioWebhookEvent, +): Promise { + const [{ messageRouterService }, { miladyGatewayRouterService }] = + await Promise.all([ + import("@/lib/services/message-router"), + import("@/lib/services/milady-gateway-router"), + ]); const chatId = event.external_id || event.sender; @@ -203,18 +248,21 @@ async function handleIncomingMessage(orgId: string, event: BlooioWebhookEvent): ]); if (!blooioFromNumber) { - logger.warn("[BlooioWebhook] No Blooio phone number configured for org", { orgId }); + logger.warn("[BlooioWebhook] No Blooio phone number configured for org", { + orgId, + }); } // Mark the chat as read immediately for better UX (sends read receipt) if (apiKey && event.sender) { - markChatAsRead(apiKey, event.sender, { fromNumber: blooioFromNumber || undefined }).catch( - (err) => - logger.warn("[BlooioWebhook] Failed to mark chat as read", { - orgId, - chatId, - error: err instanceof Error ? err.message : String(err), - }), + markChatAsRead(apiKey, event.sender, { + fromNumber: blooioFromNumber || undefined, + }).catch((err) => + logger.warn("[BlooioWebhook] Failed to mark chat as read", { + orgId, + chatId, + error: err instanceof Error ? err.message : String(err), + }), ); } @@ -238,7 +286,9 @@ async function handleIncomingMessage(orgId: string, event: BlooioWebhookEvent): // Fall back to external_id only if no from number is configured const recipient = blooioFromNumber || event.external_id || chatId; if (!recipient) { - logger.warn("[BlooioWebhook] Missing recipient in event payload", { orgId }); + logger.warn("[BlooioWebhook] Missing recipient in event payload", { + orgId, + }); return; } @@ -295,7 +345,10 @@ async function handleIncomingMessage(orgId: string, event: BlooioWebhookEvent): if (sent) { logger.info("[BlooioWebhook] Agent response sent", { orgId, chatId }); } else { - logger.error("[BlooioWebhook] Failed to send agent response", { orgId, chatId }); + logger.error("[BlooioWebhook] Failed to send agent response", { + orgId, + chatId, + }); } } } diff --git a/app/api/webhooks/twilio/[orgId]/route.ts b/app/api/webhooks/twilio/[orgId]/route.ts index 45469f9d7..29f5b1e0f 100644 --- a/app/api/webhooks/twilio/[orgId]/route.ts +++ b/app/api/webhooks/twilio/[orgId]/route.ts @@ -25,7 +25,10 @@ interface RouteParams { params: Promise<{ orgId: string }>; } -async function handleTwilioWebhook(request: NextRequest, context: RouteParams): Promise { +async function handleTwilioWebhook( + request: NextRequest, + context: RouteParams, +): Promise { const { orgId } = context?.params ? await context.params : { orgId: "" }; if (!orgId) { @@ -61,23 +64,38 @@ async function handleTwilioWebhook(request: NextRequest, context: RouteParams): // Verify signature - only skip if explicitly disabled AND not in production const isProduction = process.env.NODE_ENV === "production"; - const skipVerification = process.env.SKIP_WEBHOOK_VERIFICATION === "true" && !isProduction; + const skipVerification = + process.env.SKIP_WEBHOOK_VERIFICATION === "true" && !isProduction; const authToken = await twilioAutomationService.getAuthToken(orgId); if (process.env.SKIP_WEBHOOK_VERIFICATION === "true" && isProduction) { - logger.error("[TwilioWebhook] SKIP_WEBHOOK_VERIFICATION ignored in production", { orgId }); + logger.error( + "[TwilioWebhook] SKIP_WEBHOOK_VERIFICATION ignored in production", + { orgId }, + ); } if (skipVerification) { - logger.warn("[TwilioWebhook] Signature validation disabled (non-production)", { orgId }); + logger.warn( + "[TwilioWebhook] Signature validation disabled (non-production)", + { orgId }, + ); } else if (!authToken) { - logger.error("[TwilioWebhook] No auth token configured - rejecting webhook", { orgId }); + logger.error( + "[TwilioWebhook] No auth token configured - rejecting webhook", + { orgId }, + ); return new NextResponse("Webhook not configured", { status: 500 }); } else { const signature = request.headers.get("X-Twilio-Signature") || ""; const url = request.url; - const isValid = await verifyTwilioSignature(authToken, signature, url, webhookData); + const isValid = await verifyTwilioSignature( + authToken, + signature, + url, + webhookData, + ); if (!isValid) { logger.warn("[TwilioWebhook] Signature validation failed", { orgId }); @@ -92,12 +110,15 @@ async function handleTwilioWebhook(request: NextRequest, context: RouteParams): orgId, messageSid: event.MessageSid, }); - return new NextResponse('', { - status: 200, - headers: { - "Content-Type": "application/xml", + return new NextResponse( + '', + { + status: 200, + headers: { + "Content-Type": "application/xml", + }, }, - }); + ); } // Log the event @@ -117,12 +138,15 @@ async function handleTwilioWebhook(request: NextRequest, context: RouteParams): await markAsProcessed(idempotencyKey, "twilio"); // Return TwiML response (empty response acknowledges receipt) - return new NextResponse('', { - status: 200, - headers: { - "Content-Type": "application/xml", + return new NextResponse( + '', + { + status: 200, + headers: { + "Content-Type": "application/xml", + }, }, - }); + ); } catch (error) { logger.error("[TwilioWebhook] Error processing webhook", { orgId, @@ -134,16 +158,23 @@ async function handleTwilioWebhook(request: NextRequest, context: RouteParams): // Export POST handler with rate limiting (100 requests/min per IP) // Uses AGGRESSIVE preset for webhook endpoints -export const POST = withRateLimit(handleTwilioWebhook, RateLimitPresets.AGGRESSIVE); +export const POST = withRateLimit( + handleTwilioWebhook, + RateLimitPresets.AGGRESSIVE, +); /** * Handle incoming SMS message from Twilio */ -async function handleIncomingMessage(orgId: string, event: TwilioWebhookEvent): Promise { - const [{ messageRouterService }, { miladyGatewayRouterService }] = await Promise.all([ - import("@/lib/services/message-router"), - import("@/lib/services/milady-gateway-router"), - ]); +async function handleIncomingMessage( + orgId: string, + event: TwilioWebhookEvent, +): Promise { + const [{ messageRouterService }, { miladyGatewayRouterService }] = + await Promise.all([ + import("@/lib/services/message-router"), + import("@/lib/services/milady-gateway-router"), + ]); const from = event.From; const to = event.To; diff --git a/app/api/webhooks/whatsapp/[orgId]/route.ts b/app/api/webhooks/whatsapp/[orgId]/route.ts index 0e119dab6..09be27f0f 100644 --- a/app/api/webhooks/whatsapp/[orgId]/route.ts +++ b/app/api/webhooks/whatsapp/[orgId]/route.ts @@ -14,7 +14,10 @@ import { ZodError } from "zod"; import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit"; import { messageRouterService } from "@/lib/services/message-router"; import { whatsappAutomationService } from "@/lib/services/whatsapp-automation"; -import { releaseProcessingClaim, tryClaimForProcessing } from "@/lib/utils/idempotency"; +import { + releaseProcessingClaim, + tryClaimForProcessing, +} from "@/lib/utils/idempotency"; import { logger } from "@/lib/utils/logger"; import { createPerfTrace } from "@/lib/utils/perf-trace"; import { @@ -44,7 +47,10 @@ async function handleWhatsAppWebhook( const { orgId } = context?.params ? await context.params : { orgId: "" }; if (!orgId) { - return NextResponse.json({ error: "Organization ID is required" }, { status: 400 }); + return NextResponse.json( + { error: "Organization ID is required" }, + { status: 400 }, + ); } try { @@ -53,14 +59,21 @@ async function handleWhatsAppWebhook( // Verify signature - only skip if explicitly disabled AND not in production const isProduction = process.env.NODE_ENV === "production"; - const skipVerification = process.env.SKIP_WEBHOOK_VERIFICATION === "true" && !isProduction; + const skipVerification = + process.env.SKIP_WEBHOOK_VERIFICATION === "true" && !isProduction; if (process.env.SKIP_WEBHOOK_VERIFICATION === "true" && isProduction) { - logger.error("[WhatsAppWebhook] SKIP_WEBHOOK_VERIFICATION ignored in production", { orgId }); + logger.error( + "[WhatsAppWebhook] SKIP_WEBHOOK_VERIFICATION ignored in production", + { orgId }, + ); } if (skipVerification) { - logger.warn("[WhatsAppWebhook] Signature verification disabled (non-production)", { orgId }); + logger.warn( + "[WhatsAppWebhook] Signature verification disabled (non-production)", + { orgId }, + ); } else { const signatureHeader = request.headers.get("x-hub-signature-256") || ""; @@ -72,7 +85,10 @@ async function handleWhatsAppWebhook( if (!isValid) { logger.warn("[WhatsAppWebhook] Invalid signature", { orgId }); - return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); + return NextResponse.json( + { error: "Invalid signature" }, + { status: 401 }, + ); } } @@ -112,7 +128,10 @@ async function handleWhatsAppWebhook( const idempotencyKey = `whatsapp:org:${orgId}:${msg.messageId}`; // Atomic claim - prevents duplicate processing across concurrent deliveries - const claimed = await tryClaimForProcessing(idempotencyKey, "whatsapp-org"); + const claimed = await tryClaimForProcessing( + idempotencyKey, + "whatsapp-org", + ); if (!claimed) { logger.info("[WhatsAppWebhook] Skipping duplicate", { orgId, @@ -140,7 +159,10 @@ async function handleWhatsAppWebhook( orgId, error: error instanceof Error ? error.message : "Unknown error", }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } @@ -155,7 +177,10 @@ async function handleWhatsAppVerification( const { orgId } = context?.params ? await context.params : { orgId: "" }; if (!orgId) { - return NextResponse.json({ error: "Organization ID is required" }, { status: 400 }); + return NextResponse.json( + { error: "Organization ID is required" }, + { status: 400 }, + ); } const searchParams = request.nextUrl.searchParams; @@ -171,7 +196,9 @@ async function handleWhatsAppVerification( ); if (result) { - logger.info("[WhatsAppWebhook] Verification handshake successful", { orgId }); + logger.info("[WhatsAppWebhook] Verification handshake successful", { + orgId, + }); // Must return the challenge as plain text (not JSON) per Meta docs return new NextResponse(result, { status: 200, @@ -187,7 +214,10 @@ async function handleWhatsAppVerification( // Message Handling // ============================================================================ -async function handleIncomingMessage(orgId: string, msg: WhatsAppIncomingMessage): Promise { +async function handleIncomingMessage( + orgId: string, + msg: WhatsAppIncomingMessage, +): Promise { const text = msg.text?.trim(); if (!text) { logger.info("[WhatsAppWebhook] Skipping non-text message", { @@ -221,17 +251,24 @@ async function handleIncomingMessage(orgId: string, msg: WhatsAppIncomingMessage const markRead = async (retries = 2) => { for (let attempt = 0; attempt <= retries; attempt++) { try { - await markWhatsAppMessageAsRead(accessToken, phoneNumberId, msg.messageId); + await markWhatsAppMessageAsRead( + accessToken, + phoneNumberId, + msg.messageId, + ); return; } catch (err) { if (attempt < retries) { await new Promise((r) => setTimeout(r, 500 * (attempt + 1))); } else { - logger.warn("[WhatsAppWebhook] Failed to mark as read after retries", { - orgId, - messageId: msg.messageId, - error: err instanceof Error ? err.message : String(err), - }); + logger.warn( + "[WhatsAppWebhook] Failed to mark as read after retries", + { + orgId, + messageId: msg.messageId, + error: err instanceof Error ? err.message : String(err), + }, + ); } } } @@ -270,14 +307,22 @@ async function handleIncomingMessage(orgId: string, msg: WhatsAppIncomingMessage }, }; - const routeResult = await messageRouterService.routeIncomingMessage(messageContext); - - if (!routeResult.success || !routeResult.agentId || !routeResult.organizationId) { - logger.info("[WhatsAppWebhook] Message received (agent routing not configured)", { - orgId, - from: `***${msg.from.slice(-4)}`, - text: text.substring(0, 50), - }); + const routeResult = + await messageRouterService.routeIncomingMessage(messageContext); + + if ( + !routeResult.success || + !routeResult.agentId || + !routeResult.organizationId + ) { + logger.info( + "[WhatsAppWebhook] Message received (agent routing not configured)", + { + orgId, + from: `***${msg.from.slice(-4)}`, + text: text.substring(0, 50), + }, + ); return; } @@ -325,5 +370,11 @@ async function handleIncomingMessage(orgId: string, msg: WhatsAppIncomingMessage } // Export handlers with rate limiting -export const GET = withRateLimit(handleWhatsAppVerification, RateLimitPresets.STANDARD); -export const POST = withRateLimit(handleWhatsAppWebhook, RateLimitPresets.AGGRESSIVE); +export const GET = withRateLimit( + handleWhatsAppVerification, + RateLimitPresets.STANDARD, +); +export const POST = withRateLimit( + handleWhatsAppWebhook, + RateLimitPresets.AGGRESSIVE, +); diff --git a/app/auth/cli-login/page.tsx b/app/auth/cli-login/page.tsx index 83d113e0a..550561f65 100644 --- a/app/auth/cli-login/page.tsx +++ b/app/auth/cli-login/page.tsx @@ -2,7 +2,13 @@ import { Button } from "@elizaos/cloud-ui/components/button"; import { usePrivy } from "@privy-io/react-auth"; -import { AlertCircle, CheckCircle2, Key, Loader2, Terminal } from "lucide-react"; +import { + AlertCircle, + CheckCircle2, + Key, + Loader2, + Terminal, +} from "lucide-react"; import { useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; @@ -28,7 +34,9 @@ function CliLoginContent() { const [status, setStatus] = useState< "loading" | "waiting_auth" | "completing" | "success" | "error" >(initialStatus.status); - const [errorMessage, setErrorMessage] = useState(initialStatus.errorMessage); + const [errorMessage, setErrorMessage] = useState( + initialStatus.errorMessage, + ); const [apiKeyPrefix, setApiKeyPrefix] = useState(""); const [isLoggingIn, setIsLoggingIn] = useState(false); @@ -42,12 +50,15 @@ function CliLoginContent() { setStatus("completing"); try { - const response = await fetch(`/api/auth/cli-session/${sessionId}/complete`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `/api/auth/cli-session/${sessionId}/complete`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, }, - }); + ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -63,14 +74,21 @@ function CliLoginContent() { // Signal the opener window (Milady UI) that auth is complete try { - window.opener?.postMessage({ type: "eliza-cloud-auth-complete", sessionId }, "*"); + window.opener?.postMessage( + { type: "eliza-cloud-auth-complete", sessionId }, + "*", + ); } catch { // opener may not be accessible (cross-origin policy or popup blocker) } } catch (error) { console.error("CLI login error:", error); setStatus("error"); - setErrorMessage(error instanceof Error ? error.message : "Network error. Please try again."); + setErrorMessage( + error instanceof Error + ? error.message + : "Network error. Please try again.", + ); } }, [sessionId]); @@ -118,7 +136,9 @@ function CliLoginContent() {

Loading...

-

Preparing authentication

+

+ Preparing authentication +

@@ -136,7 +156,9 @@ function CliLoginContent() {
-

Authentication Error

+

+ Authentication Error +

{errorMessage}

@@ -286,7 +297,9 @@ export default function AdminPage() {

Access Denied

-

You don't have admin privileges.

+

+ You don't have admin privileges. +

Current wallet: {wallets[0]?.address?.slice(0, 10)}...

@@ -300,7 +313,9 @@ export default function AdminPage() {

Admin Panel

-

Moderation and user management • {adminRole}

+

+ Moderation and user management • {adminRole} +

@@ -409,7 +438,10 @@ export default function AdminPage() { ))} {violations.length === 0 && ( - + No violations found @@ -430,7 +462,9 @@ export default function AdminPage() { Flagged Users - Users with violations requiring review + + Users with violations requiring review +
@@ -471,7 +505,9 @@ export default function AdminPage() {
))} {flaggedUsers.length === 0 && ( -

No flagged users

+

+ No flagged users +

)}
@@ -484,7 +520,9 @@ export default function AdminPage() { Banned Users - Users currently banned from the platform + + Users currently banned from the platform +
@@ -502,14 +540,18 @@ export default function AdminPage() {
))} {bannedUsers.length === 0 && ( -

No banned users

+

+ No banned users +

)}
@@ -570,19 +612,20 @@ export default function AdminPage() { {admin.notes || "-"} - {adminRole === "super_admin" && admin.id !== "anvil-default" && ( - - )} + {adminRole === "super_admin" && + admin.id !== "anvil-default" && ( + + )} ))} @@ -598,7 +641,9 @@ export default function AdminPage() { Add Admin - Grant admin privileges to a wallet address + + Grant admin privileges to a wallet address +
@@ -643,7 +688,9 @@ export default function AdminPage() { }} disabled={actionLoading || !newAdminWallet} > - {actionLoading && } + {actionLoading && ( + + )} Add Admin @@ -655,7 +702,9 @@ export default function AdminPage() { User Details - Detailed information and moderation actions + + Detailed information and moderation actions + {userDetail ? (
@@ -673,7 +722,9 @@ export default function AdminPage() {
Wallet:{" "} - {userDetail.user?.wallet_address?.slice(0, 10)}... + + {userDetail.user?.wallet_address?.slice(0, 10)}... +
Generations:{" "} @@ -696,8 +747,12 @@ export default function AdminPage() { > {userDetail.moderationStatus.status} - Violations: {userDetail.moderationStatus.totalViolations} - Risk Score: {userDetail.moderationStatus.riskScore} + + Violations: {userDetail.moderationStatus.totalViolations} + + + Risk Score: {userDetail.moderationStatus.riskScore} +
)} @@ -712,12 +767,18 @@ export default function AdminPage() {
{v.categories.map((c) => ( - + {c} ))}
-

{v.messageText}

+

+ {v.messageText} +

))}
@@ -727,14 +788,18 @@ export default function AdminPage() {
)} @@ -290,9 +312,13 @@ export default function ApiExplorerPage() {
{getCategoryIcon(selectedEndpoint.category)} -

{selectedEndpoint.name}

+

+ {selectedEndpoint.name} +

-

{selectedEndpoint.description}

+

+ {selectedEndpoint.description} +

endpoint.category === category) - .length; + : endpointsWithLivePricing.filter( + (endpoint) => endpoint.category === category, + ).length; return ( - + {/* Timer & Session */} {timeRemaining && ( <> @@ -2951,56 +3169,65 @@ ANTHROPIC_API_KEY=your_key_here`} )} {/* GitHub Actions */} - {(status === "ready" || status === "recovering") && appData?.github_repo && ( - <> - - - {isSaving ? ( - - ) : ( - - )} - {isSaving ? "Saving..." : gitStatus?.hasChanges ? "Save to GitHub" : "Saved"} - - - {isDeploying ? ( - - ) : ( - - )} - {isDeploying - ? deployPhase === "saving" - ? "Saving to GitHub..." - : "Deploying..." - : "Deploy"} - - {productionUrl && ( - - - - View Live Site - + {(status === "ready" || status === "recovering") && + appData?.github_repo && ( + <> + + + {isSaving ? ( + + ) : ( + + )} + {isSaving + ? "Saving..." + : gitStatus?.hasChanges + ? "Save to GitHub" + : "Saved"} - )} - - )} + + {isDeploying ? ( + + ) : ( + + )} + {isDeploying + ? deployPhase === "saving" + ? "Saving to GitHub..." + : "Deploying..." + : "Deploy"} + + {productionUrl && ( + + + + View Live Site + + + )} + + )} {/* GitHub Repo Link */} {appData?.github_repo && ( @@ -3031,7 +3258,11 @@ ANTHROPIC_API_KEY=your_key_here`} {copied ? "Copied!" : "Copy Preview URL"} - + Open in New Tab @@ -3060,12 +3291,18 @@ ANTHROPIC_API_KEY=your_key_here`} {/* DESKTOP TOOLBAR - visible from xl (1280px) and up */}
- +
- + {appData?.name || appName} {isEditMode && ( @@ -3143,7 +3380,12 @@ ANTHROPIC_API_KEY=your_key_here`} Restore ) : status === "recovering" ? ( - @@ -3241,7 +3483,9 @@ ANTHROPIC_API_KEY=your_key_here`} onClick={stopSession} disabled={status === "recovering"} className="p-2 hover:bg-red-500/20 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - title={status === "recovering" ? "Reconnecting..." : "Stop session"} + title={ + status === "recovering" ? "Reconnecting..." : "Stop session" + } > @@ -3278,7 +3522,11 @@ ANTHROPIC_API_KEY=your_key_here`}
{/* Isolated ChatInput component - uses Zustand for zero re-renders on typing */} - +
{/* PREVIEW PANEL - visible on desktop (flex-1), toggled on mobile/tablet */} @@ -3483,7 +3731,9 @@ ANTHROPIC_API_KEY=your_key_here`} {/* Building overlay - Show until first generation completes */}
Starting sandbox...

-

This usually takes 10-20 seconds

+

+ This usually takes 10-20 seconds +

)} @@ -3669,7 +3921,10 @@ ANTHROPIC_API_KEY=your_key_here`} ) { colorClass = "text-red-400"; bgClass = "bg-red-500/10"; - } else if (log.includes("[warning]") || log.includes("Warning")) { + } else if ( + log.includes("[warning]") || + log.includes("Warning") + ) { colorClass = "text-yellow-400"; bgClass = "bg-yellow-500/5"; } else if (log.includes("Progress:")) { @@ -3682,12 +3937,18 @@ ANTHROPIC_API_KEY=your_key_here`} ) { if (log.includes(" 2")) { colorClass = "text-green-400/70"; - } else if (log.includes(" 4") || log.includes(" 5")) { + } else if ( + log.includes(" 4") || + log.includes(" 5") + ) { colorClass = "text-red-400/70"; } else { colorClass = "text-cyan-400/70"; } - } else if (log.includes("Next.js") || log.includes("Turbopack")) { + } else if ( + log.includes("Next.js") || + log.includes("Turbopack") + ) { colorClass = "text-white/80"; } @@ -3699,7 +3960,9 @@ ANTHROPIC_API_KEY=your_key_here`} {i + 1} -
+                                
                                   {log}
                                 
@@ -3730,7 +3993,8 @@ ANTHROPIC_API_KEY=your_key_here`}
- Type help for commands + Type help{" "} + for commands
diff --git a/app/dashboard/billing/success/page.tsx b/app/dashboard/billing/success/page.tsx index cb2f6c5e8..2fae77d65 100644 --- a/app/dashboard/billing/success/page.tsx +++ b/app/dashboard/billing/success/page.tsx @@ -59,7 +59,9 @@ async function verifyAndProcessSession( const session = await requireStripe().checkout.sessions.retrieve(sessionId); if (session.payment_status !== "paid") { - console.warn(`[BillingSuccess] Session ${sessionId} not paid: ${session.payment_status}`); + console.warn( + `[BillingSuccess] Session ${sessionId} not paid: ${session.payment_status}`, + ); return { success: false, error: `Payment not completed. Status: ${session.payment_status}`, @@ -73,7 +75,10 @@ async function verifyAndProcessSession( const purchaseType = session.metadata?.type || "checkout"; const paymentIntentId = session.payment_intent as string; - if (organizationId !== viewer.organization_id || (userId && userId !== viewer.id)) { + if ( + organizationId !== viewer.organization_id || + (userId && userId !== viewer.id) + ) { return { success: false, error: "You do not have access to this checkout session.", @@ -132,7 +137,9 @@ async function verifyAndProcessSession( // Create invoice record try { - const existingInvoice = await invoicesService.getByStripeInvoiceId(`cs_${sessionId}`); + const existingInvoice = await invoicesService.getByStripeInvoiceId( + `cs_${sessionId}`, + ); if (!existingInvoice) { const amountTotal = session.amount_total @@ -163,7 +170,10 @@ async function verifyAndProcessSession( } } catch (invoiceError) { // Non-critical - credits were added successfully - logger.error("[BillingSuccess] Invoice creation error (non-critical):", invoiceError); + logger.error( + "[BillingSuccess] Invoice creation error (non-critical):", + invoiceError, + ); } return { @@ -181,7 +191,9 @@ async function verifyAndProcessSession( * @param searchParams - Search parameters, including `from` (redirect source) and `session_id` (Stripe session ID). * @returns The rendered billing success page with payment status and credit balance. */ -export default async function BillingSuccessPage({ searchParams }: BillingSuccessPageProps) { +export default async function BillingSuccessPage({ + searchParams, +}: BillingSuccessPageProps) { const viewer = await requireAuthWithOrg(); const params = await searchParams; const fromSettings = params.from === "settings"; @@ -219,7 +231,8 @@ export default async function BillingSuccessPage({ searchParams }: BillingSucces

- If you believe this is an error, please contact support with your session ID. + If you believe this is an error, please contact support with your + session ID.

{sessionId && (

@@ -230,7 +243,13 @@ export default async function BillingSuccessPage({ searchParams }: BillingSucces @@ -256,10 +275,14 @@ export default async function BillingSuccessPage({ searchParams }: BillingSucces - +

- You can now use your credits for text generation, image creation, and video rendering. + You can now use your credits for text generation, image creation, + and video rendering.

@@ -267,7 +290,9 @@ export default async function BillingSuccessPage({ searchParams }: BillingSucces {fromSettings ? ( <> @@ -159,31 +169,46 @@ function InviteAcceptContent() { You're Invited! - You've been invited to join an organization + + You've been invited to join an organization +
-

Organization

-

{inviteDetails.organization_name}

+

+ Organization +

+

+ {inviteDetails.organization_name} +

-

Invited Email

-

{inviteDetails.invited_email}

+

+ Invited Email +

+

+ {inviteDetails.invited_email} +

{getRoleIcon(inviteDetails.role)}
-

Role

- +

+ Role +

+ {getRoleIcon(inviteDetails.role)} {inviteDetails.role} @@ -194,7 +219,9 @@ function InviteAcceptContent() {
-

Invited by

+

+ Invited by +

{inviteDetails.inviter_name}

@@ -205,7 +232,8 @@ function InviteAcceptContent() { - This invitation expires on {format(expiresAt, "MMM d, yyyy 'at' h:mm a")} + This invitation expires on{" "} + {format(expiresAt, "MMM d, yyyy 'at' h:mm a")} )} @@ -241,8 +269,8 @@ function InviteAcceptContent() {
- By accepting, you'll gain access to the organization's resources and - workspace. + By accepting, you'll gain access to the organization's + resources and workspace.
diff --git a/app/layout.tsx b/app/layout.tsx index 711851578..4e546aae0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,8 @@ import PrivyProvider from "@/lib/providers/PrivyProvider"; import { StewardAuthProvider } from "@/lib/providers/StewardProvider"; import { getRobotsMetadata } from "@/lib/seo"; -const stewardAuthEnabled = process.env.NEXT_PUBLIC_STEWARD_AUTH_ENABLED === "true"; +const stewardAuthEnabled = + process.env.NEXT_PUBLIC_STEWARD_AUTH_ENABLED === "true"; /** * Conditionally wraps children in StewardAuthProvider when enabled. @@ -75,7 +76,9 @@ const sfPro = localFont({ */ const baseUrl = process.env.NEXT_PUBLIC_APP_URL || - (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"); + (process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"); const shouldEnableVercelAnalytics = process.env.VERCEL === "1"; const robots = getRobotsMetadata(); @@ -107,7 +110,8 @@ export const metadata: Metadata = { }, openGraph: { title: "Eliza Cloud - Managed Hosting for AI Agents", - description: "Managed hosting, provisioning, billing, and deployment for AI agents", + description: + "Managed hosting, provisioning, billing, and deployment for AI agents", url: "/", siteName: "Eliza Cloud", type: "website", @@ -124,7 +128,8 @@ export const metadata: Metadata = { twitter: { card: "summary_large_image", title: "Eliza Cloud", - description: "Managed hosting, provisioning, billing, and deployment for AI agents", + description: + "Managed hosting, provisioning, billing, and deployment for AI agents", images: ["/cloudlogo.png"], }, robots, diff --git a/app/login/layout.tsx b/app/login/layout.tsx index 83b42f45a..8592beba0 100644 --- a/app/login/layout.tsx +++ b/app/login/layout.tsx @@ -3,7 +3,8 @@ import { generatePageMetadata } from "@/lib/seo"; export const metadata: Metadata = generatePageMetadata({ title: "Login", - description: "Sign in to Eliza Cloud to create, provision, and manage Milady agents.", + description: + "Sign in to Eliza Cloud to create, provision, and manage Milady agents.", path: "/login", keywords: ["login", "sign in", "authentication", "Milady", "Eliza Cloud"], noIndex: true, @@ -15,6 +16,10 @@ export const metadata: Metadata = generatePageMetadata({ * * @param children - The login page content. */ -export default function LoginLayout({ children }: { children: React.ReactNode }) { +export default function LoginLayout({ + children, +}: { + children: React.ReactNode; +}) { return children; } diff --git a/app/login/page.tsx b/app/login/page.tsx index 078ae896c..70d92fbf8 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -5,7 +5,8 @@ import dynamic from "next/dynamic"; import { Suspense } from "react"; import LandingHeader from "@/packages/ui/src/components/layout/landing-header"; -const STEWARD_AUTH_ENABLED = process.env.NEXT_PUBLIC_STEWARD_AUTH_ENABLED === "true"; +const STEWARD_AUTH_ENABLED = + process.env.NEXT_PUBLIC_STEWARD_AUTH_ENABLED === "true"; // Dynamic imports: only load the section that's needed. // ssr: false prevents Privy hooks from executing during SSR or when Privy is unconfigured. @@ -80,7 +81,9 @@ function LoginPageContent() {

Welcome back

-

Sign in to your Eliza Cloud account

+

+ Sign in to your Eliza Cloud account +

diff --git a/app/login/privy-login-section.tsx b/app/login/privy-login-section.tsx index 96677b51f..a996a2642 100644 --- a/app/login/privy-login-section.tsx +++ b/app/login/privy-login-section.tsx @@ -1,7 +1,12 @@ "use client"; import { BrandButton, Input } from "@elizaos/cloud-ui"; -import { useLogin, useLoginWithEmail, useLoginWithOAuth, usePrivy } from "@privy-io/react-auth"; +import { + useLogin, + useLoginWithEmail, + useLoginWithOAuth, + usePrivy, +} from "@privy-io/react-auth"; import { ArrowLeft, Chrome, Github, Loader2, Mail, Wallet } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; @@ -33,9 +38,11 @@ function getPendingSignupAttribution(searchParams: { get(name: string): string | null; has(name: string): boolean; }) { - const hasOAuthState = searchParams.has("state") || searchParams.has("privy_oauth_state"); + const hasOAuthState = + searchParams.has("state") || searchParams.has("privy_oauth_state"); const affiliateCode = searchParams.get("affiliate"); - const referralCode = searchParams.get("ref") || searchParams.get("referral_code"); + const referralCode = + searchParams.get("ref") || searchParams.get("referral_code"); const legacyCode = searchParams.get("code"); return { @@ -48,7 +55,9 @@ function getPendingSignupAttribution(searchParams: { }; } -function getSafeReturnTo(searchParams: { get(name: string): string | null }): string { +function getSafeReturnTo(searchParams: { + get(name: string): string | null; +}): string { const returnTo = searchParams.get("returnTo"); return returnTo && returnTo.startsWith("/") && !returnTo.startsWith("//") ? returnTo @@ -79,7 +88,8 @@ export default function PrivyLoginSection() { const hasOAuthParams = urlParams.has("privy_oauth_code") || urlParams.has("privy_oauth_state") || - (urlParams.has("code") && (urlParams.has("state") || urlParams.has("privy_oauth_state"))); + (urlParams.has("code") && + (urlParams.has("state") || urlParams.has("privy_oauth_state"))); const sessionFlag = sessionStorage.getItem("oauth_login_pending"); return hasOAuthParams || sessionFlag === "true"; }); @@ -93,14 +103,21 @@ export default function PrivyLoginSection() { const postLoginProcessingRef = useRef(false); useEffect(() => { - const { affiliateCode, referralCode } = getPendingSignupAttribution(searchParams); + const { affiliateCode, referralCode } = + getPendingSignupAttribution(searchParams); if (affiliateCode) { - sessionStorage.setItem(SIGNUP_ATTRIBUTION_STORAGE_KEYS.affiliate, affiliateCode); + sessionStorage.setItem( + SIGNUP_ATTRIBUTION_STORAGE_KEYS.affiliate, + affiliateCode, + ); } if (referralCode) { - sessionStorage.setItem(SIGNUP_ATTRIBUTION_STORAGE_KEYS.referral, referralCode); + sessionStorage.setItem( + SIGNUP_ATTRIBUTION_STORAGE_KEYS.referral, + referralCode, + ); } }, [searchParams]); @@ -155,10 +172,18 @@ export default function PrivyLoginSection() { }; const applyStoredSignupAttribution = async () => { - const affiliateCode = sessionStorage.getItem(SIGNUP_ATTRIBUTION_STORAGE_KEYS.affiliate); - const referralCode = sessionStorage.getItem(SIGNUP_ATTRIBUTION_STORAGE_KEYS.referral); - - const postAttribution = async (url: string, codeToApply: string, storageKey: string) => { + const affiliateCode = sessionStorage.getItem( + SIGNUP_ATTRIBUTION_STORAGE_KEYS.affiliate, + ); + const referralCode = sessionStorage.getItem( + SIGNUP_ATTRIBUTION_STORAGE_KEYS.referral, + ); + + const postAttribution = async ( + url: string, + codeToApply: string, + storageKey: string, + ) => { try { for (let attempt = 0; attempt < 3; attempt++) { const response = await fetch(url, { @@ -282,7 +307,9 @@ export default function PrivyLoginSection() { setLoadingButton(null); }; - const handleOAuthLogin = async (provider: "google" | "discord" | "github") => { + const handleOAuthLogin = async ( + provider: "google" | "discord" | "github", + ) => { setLoadingButton(provider); sessionStorage.setItem("oauth_login_pending", "true"); toast.loading(`Redirecting to ${provider}...`); @@ -335,7 +362,9 @@ export default function PrivyLoginSection() { {isProcessingOAuth ? "Completing sign in..." : "Loading..."}

- {isProcessingOAuth ? "Processing your authentication" : "Initializing..."} + {isProcessingOAuth + ? "Processing your authentication" + : "Initializing..."}

@@ -357,7 +386,9 @@ export default function PrivyLoginSection() {

Signing you in

-

Taking you to your dashboard...

+

+ Taking you to your dashboard... +

@@ -432,14 +463,17 @@ export default function PrivyLoginSection() { type="text" placeholder="000000" value={code} - onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))} + onChange={(e) => + setCode(e.target.value.replace(/\D/g, "").slice(0, 6)) + } disabled={loadingButton !== null} className="h-12 rounded-xl border-white/10 bg-black/40 text-white placeholder:text-neutral-600 focus:ring-1 focus:ring-[#FF5800] focus:border-[#FF5800] text-center text-xl tracking-[0.3em] font-mono" maxLength={6} autoFocus />

- Enter the 6-digit code sent to {email} + Enter the 6-digit code sent to{" "} + {email}

- Continue with Google + + Continue with Google + )} @@ -557,11 +593,17 @@ export default function PrivyLoginSection() { {/* Footer */}

By signing in, you agree to our{" "} - + Terms {" "} and{" "} - + Privacy Policy

diff --git a/app/login/steward-login-section.tsx b/app/login/steward-login-section.tsx index 32235cd5a..8e70e5a6a 100644 --- a/app/login/steward-login-section.tsx +++ b/app/login/steward-login-section.tsx @@ -5,14 +5,17 @@ import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -const STEWARD_API_URL = process.env.NEXT_PUBLIC_STEWARD_API_URL || "https://eliza.steward.fi"; +const STEWARD_API_URL = + process.env.NEXT_PUBLIC_STEWARD_API_URL || "https://eliza.steward.fi"; type AuthStep = "idle" | "loading" | "email-sent" | "success"; type Provider = "passkey" | "email" | "google" | "discord" | "twitter"; function getSafeReturnTo(sp: { get(n: string): string | null }): string { const r = sp.get("returnTo"); - return r && r.startsWith("/") && !r.startsWith("//") ? r : "/dashboard/milady"; + return r && r.startsWith("/") && !r.startsWith("//") + ? r + : "/dashboard/milady"; } export default function StewardLoginSection() { @@ -126,7 +129,9 @@ export default function StewardLoginSection() {

Magic link sent to {email}

-

Check your inbox and click the link to sign in.

+

+ Check your inbox and click the link to sign in. +

diff --git a/app/sandbox-proxy/page.tsx b/app/sandbox-proxy/page.tsx index 1a8cf6a87..92870c13f 100644 --- a/app/sandbox-proxy/page.tsx +++ b/app/sandbox-proxy/page.tsx @@ -83,7 +83,10 @@ export default function SandboxProxyPage() { const handleMessage = useCallback(async (event: MessageEvent) => { // Validate origin if (!isAllowedOrigin(event.origin)) { - console.warn("[SandboxProxy] Rejected message from origin:", event.origin); + console.warn( + "[SandboxProxy] Rejected message from origin:", + event.origin, + ); return; } @@ -174,7 +177,9 @@ export default function SandboxProxyPage() { if (process.env.NODE_ENV !== "development") { return (
-

Sandbox proxy is only available in development mode.

+

+ Sandbox proxy is only available in development mode. +

); } diff --git a/app/terms-of-service/page.tsx b/app/terms-of-service/page.tsx index c8964df4d..19488d697 100644 --- a/app/terms-of-service/page.tsx +++ b/app/terms-of-service/page.tsx @@ -10,7 +10,13 @@ export const metadata: Metadata = generatePageMetadata({ description: "Terms of Service for MILADY CLOUD - Read our terms and conditions for using our AI agent development platform.", path: "/terms-of-service", - keywords: ["terms of service", "terms and conditions", "legal", "agreement", "elizaOS"], + keywords: [ + "terms of service", + "terms and conditions", + "legal", + "agreement", + "elizaOS", + ], }); /** @@ -54,115 +60,159 @@ export default function TermsOfServicePage() { {/* Header */}
- -

Terms of Service

-

Last updated: November 4, 2025

+ +

+ Terms of Service +

+

+ Last updated: November 4, 2025 +

{/* Content */}
-

1. Acceptance of Terms

+

+ 1. Acceptance of Terms +

- By accessing and using the elizaOS platform ("Service"), you accept and - agree to be bound by the terms and provision of this agreement. If you do not - agree to abide by the above, please do not use this service. + By accessing and using the elizaOS platform + ("Service"), you accept and agree to be bound by the + terms and provision of this agreement. If you do not agree to + abide by the above, please do not use this service.

-

2. Use License

+

+ 2. Use License +

- Permission is granted to temporarily access the materials (information or - software) on elizaOS for personal, non-commercial transitory viewing only. This is - the grant of a license, not a transfer of title, and under this license you may - not: + Permission is granted to temporarily access the materials + (information or software) on elizaOS for personal, + non-commercial transitory viewing only. This is the grant of a + license, not a transfer of title, and under this license you + may not:

  • Modify or copy the materials
  • -
  • Use the materials for any commercial purpose or for any public display
  • -
  • Attempt to reverse engineer any software contained on elizaOS
  • -
  • Remove any copyright or other proprietary notations from the materials
  • - Transfer the materials to another person or "mirror" the materials on - any other server + Use the materials for any commercial purpose or for any + public display +
  • +
  • + Attempt to reverse engineer any software contained on + elizaOS +
  • +
  • + Remove any copyright or other proprietary notations from the + materials +
  • +
  • + Transfer the materials to another person or + "mirror" the materials on any other server
-

3. Account Terms

+

+ 3. Account Terms +

- You must provide a valid email address and any other information requested in - order to complete the signup process. You are responsible for maintaining the - security of your account and password. elizaOS cannot and will not be liable for - any loss or damage from your failure to comply with this security obligation. + You must provide a valid email address and any other + information requested in order to complete the signup process. + You are responsible for maintaining the security of your + account and password. elizaOS cannot and will not be liable + for any loss or damage from your failure to comply with this + security obligation.

-

4. API Usage and Limits

+

+ 4. API Usage and Limits +

- Your use of the elizaOS API is subject to rate limits and usage quotas. You agree - not to exceed these limits or attempt to circumvent them. We reserve the right to - modify, suspend, or discontinue the API at any time with or without notice. + Your use of the elizaOS API is subject to rate limits and + usage quotas. You agree not to exceed these limits or attempt + to circumvent them. We reserve the right to modify, suspend, + or discontinue the API at any time with or without notice.

-

5. Payment and Billing

+

+ 5. Payment and Billing +

- You agree to pay all fees associated with your use of the Service. All fees are - non-refundable unless otherwise stated. We reserve the right to change our pricing - structure at any time with reasonable notice to users. + You agree to pay all fees associated with your use of the + Service. All fees are non-refundable unless otherwise stated. + We reserve the right to change our pricing structure at any + time with reasonable notice to users.

-

6. Prohibited Uses

+

+ 6. Prohibited Uses +

- You may not use the Service for any illegal or unauthorized purpose. You must not, - in the use of the Service, violate any laws in your jurisdiction including but not - limited to copyright laws. + You may not use the Service for any illegal or unauthorized + purpose. You must not, in the use of the Service, violate any + laws in your jurisdiction including but not limited to + copyright laws.

7. Disclaimer

- The materials on elizaOS are provided on an 'as is' basis. elizaOS makes - no warranties, expressed or implied, and hereby disclaims and negates all other - warranties including, without limitation, implied warranties or conditions of - merchantability, fitness for a particular purpose, or non-infringement of - intellectual property or other violation of rights. + The materials on elizaOS are provided on an 'as is' + basis. elizaOS makes no warranties, expressed or implied, and + hereby disclaims and negates all other warranties including, + without limitation, implied warranties or conditions of + merchantability, fitness for a particular purpose, or + non-infringement of intellectual property or other violation + of rights.

-

8. Limitations

+

+ 8. Limitations +

- In no event shall elizaOS or its suppliers be liable for any damages (including, - without limitation, damages for loss of data or profit, or due to business - interruption) arising out of the use or inability to use the materials on elizaOS, - even if elizaOS or an authorized representative has been notified orally or in - writing of the possibility of such damage. + In no event shall elizaOS or its suppliers be liable for any + damages (including, without limitation, damages for loss of + data or profit, or due to business interruption) arising out + of the use or inability to use the materials on elizaOS, even + if elizaOS or an authorized representative has been notified + orally or in writing of the possibility of such damage.

-

9. Modifications

+

+ 9. Modifications +

- elizaOS may revise these terms of service at any time without notice. By using - this Service you are agreeing to be bound by the then current version of these - terms of service. + elizaOS may revise these terms of service at any time without + notice. By using this Service you are agreeing to be bound by + the then current version of these terms of service.

-

10. Contact Information

+

+ 10. Contact Information +

- If you have any questions about these Terms, please contact us through our support - channels. + If you have any questions about these Terms, please contact us + through our support channels.

diff --git a/biome.json b/biome.json index 9a8ff7802..022b0bbb2 100644 --- a/biome.json +++ b/biome.json @@ -1,4 +1,5 @@ { + "root": false, "$schema": "https://biomejs.dev/schemas/2.4.11/schema.json", "vcs": { "enabled": true, diff --git a/mdx-components.tsx b/mdx-components.tsx index 432164c68..a14311adb 100644 --- a/mdx-components.tsx +++ b/mdx-components.tsx @@ -3,7 +3,9 @@ import { useMDXComponents as getDocsMDXComponents } from "nextra-theme-docs"; const docsComponents = getDocsMDXComponents(); -export function useMDXComponents(components?: Record) { +export function useMDXComponents( + components?: Record, +) { return { ...docsComponents, Tweet, diff --git a/next.config.ts b/next.config.ts index 6ac14d9d1..9f89de03f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -43,17 +43,24 @@ function normalizeOrigin(value: string | undefined): string | null { } function uniqueCspValues(values: Array): string { - return [...new Set(values.filter((value): value is string => Boolean(value?.trim())))] + return [ + ...new Set( + values.filter((value): value is string => Boolean(value?.trim())), + ), + ] .map((value) => value.trim()) .join(" "); } const appOrigin = normalizeOrigin( process.env.NEXT_PUBLIC_APP_URL || - (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"), + (process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"), ); const posthogOrigin = - normalizeOrigin(process.env.NEXT_PUBLIC_POSTHOG_HOST) || "https://us.i.posthog.com"; + normalizeOrigin(process.env.NEXT_PUBLIC_POSTHOG_HOST) || + "https://us.i.posthog.com"; const localDevConnectOrigins = process.env.NODE_ENV === "development" ? [ @@ -225,14 +232,21 @@ const nextConfig: NextConfig = { config.plugins.push( new webpack.IgnorePlugin({ // Ignore missing highlight.js language files (c-like, htmlbars, sql_more removed in v11) - resourceRegExp: /^highlight\.js\/lib\/languages\/(c-like|htmlbars|sql_more)$/, + resourceRegExp: + /^highlight\.js\/lib\/languages\/(c-like|htmlbars|sql_more)$/, }), ); if (isServer) { // Resolve thread-stream to synchronous stub in production webpack builds // This prevents pino from creating dynamic worker modules like pino-28069d5257187539 - const stubPath = path.join(__dirname, "packages/lib/stubs/thread-stream.ts"); - const loggerStubPath = path.join(__dirname, "packages/lib/stubs/walletconnect-logger.ts"); + const stubPath = path.join( + __dirname, + "packages/lib/stubs/thread-stream.ts", + ); + const loggerStubPath = path.join( + __dirname, + "packages/lib/stubs/walletconnect-logger.ts", + ); config.resolve.alias = { ...config.resolve.alias, "thread-stream": stubPath, @@ -253,7 +267,9 @@ const nextConfig: NextConfig = { outputFileTracingIncludes: { "/api/v1/containers": ["./packages/scripts/cloudformation/**/*"], "/api/v1/containers/[id]": ["./packages/scripts/cloudformation/**/*"], - "/api/v1/cron/deployment-monitor": ["./packages/scripts/cloudformation/**/*"], + "/api/v1/cron/deployment-monitor": [ + "./packages/scripts/cloudformation/**/*", + ], }, serverExternalPackages: [ "pdfjs-dist", diff --git a/packages/db/client.ts b/packages/db/client.ts index 20f64bc08..8a1685a66 100644 --- a/packages/db/client.ts +++ b/packages/db/client.ts @@ -125,7 +125,11 @@ function detectRegion(): DatabaseRegion { // Check explicit override const explicitRegion = process.env.DATABASE_REGION?.toLowerCase(); - if (explicitRegion === "eu" || explicitRegion === "na" || explicitRegion === "apac") { + if ( + explicitRegion === "eu" || + explicitRegion === "na" || + explicitRegion === "apac" + ) { return explicitRegion; } @@ -401,14 +405,18 @@ export function getDbConnectionInfo() { /** * Execute a read query (uses read replica) */ -export async function withReadDb(fn: (db: Database) => Promise): Promise { +export async function withReadDb( + fn: (db: Database) => Promise, +): Promise { return fn(connectionManager.getReadConnection()); } /** * Execute a write query (uses primary) */ -export async function withWriteDb(fn: (db: Database) => Promise): Promise { +export async function withWriteDb( + fn: (db: Database) => Promise, +): Promise { return fn(connectionManager.getWriteConnection()); } diff --git a/packages/db/database-url.ts b/packages/db/database-url.ts index 9904f4402..5fd294a4c 100644 --- a/packages/db/database-url.ts +++ b/packages/db/database-url.ts @@ -30,7 +30,9 @@ function getFirstExternalIpv4Address(): string | null { export function getLocalDockerDatabaseUrl(env: EnvLike = process.env): string { const host = - env.LOCAL_DOCKER_DB_HOST || getFirstExternalIpv4Address() || DEFAULT_LOCAL_DOCKER_DB_HOST; + env.LOCAL_DOCKER_DB_HOST || + getFirstExternalIpv4Address() || + DEFAULT_LOCAL_DOCKER_DB_HOST; const port = env.LOCAL_DOCKER_DB_PORT || DEFAULT_LOCAL_DOCKER_DB_PORT; return `postgresql://${LOCAL_DOCKER_DB_USER}:${LOCAL_DOCKER_DB_PASSWORD}@${host}:${port}/${LOCAL_DOCKER_DB_NAME}`; @@ -42,7 +44,9 @@ export const LOCAL_DOCKER_DATABASE_URL = getLocalDockerDatabaseUrl({ }); function isLocalExecution(env: EnvLike): boolean { - return env.VERCEL !== "1" && env.NODE_ENV !== "production" && env.CI !== "true"; + return ( + env.VERCEL !== "1" && env.NODE_ENV !== "production" && env.CI !== "true" + ); } export function resolveDatabaseUrl(env: EnvLike = process.env): string | null { @@ -62,7 +66,9 @@ export function resolveDatabaseUrl(env: EnvLike = process.env): string | null { return null; } -export function applyDatabaseUrlFallback(env: EnvLike = process.env): string | null { +export function applyDatabaseUrlFallback( + env: EnvLike = process.env, +): string | null { const url = resolveDatabaseUrl(env); if (!url) { return null; diff --git a/packages/db/helpers.ts b/packages/db/helpers.ts index 8817d1940..d05214057 100644 --- a/packages/db/helpers.ts +++ b/packages/db/helpers.ts @@ -78,7 +78,9 @@ export function useWriteDb(fn: (db: Database) => T): T { * return db.query.users.findMany({ limit: 100 }); * }); */ -export async function readQuery(fn: (db: Database) => Promise): Promise { +export async function readQuery( + fn: (db: Database) => Promise, +): Promise { return fn(dbRead); } @@ -91,7 +93,9 @@ export async function readQuery(fn: (db: Database) => Promise): Promise * return created; * }); */ -export async function writeQuery(fn: (db: Database) => Promise): Promise { +export async function writeQuery( + fn: (db: Database) => Promise, +): Promise { return fn(dbWrite); } @@ -111,7 +115,9 @@ export async function writeQuery(fn: (db: Database) => Promise): Promise( fn: ( - tx: Parameters[0] extends (tx: infer U) => unknown ? U : never, + tx: Parameters[0] extends (tx: infer U) => unknown + ? U + : never, ) => Promise, ): Promise { return dbWrite.transaction(fn); diff --git a/packages/db/migrations/meta/0026_snapshot.json b/packages/db/migrations/meta/0026_snapshot.json index 70df83f66..8000a39d8 100644 --- a/packages/db/migrations/meta/0026_snapshot.json +++ b/packages/db/migrations/meta/0026_snapshot.json @@ -15,4 +15,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0033_snapshot.json b/packages/db/migrations/meta/0033_snapshot.json index 62302acd3..64cd8f9b4 100644 --- a/packages/db/migrations/meta/0033_snapshot.json +++ b/packages/db/migrations/meta/0033_snapshot.json @@ -15,4 +15,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0034_snapshot.json b/packages/db/migrations/meta/0034_snapshot.json index 8bcccb23b..6323bf41e 100644 --- a/packages/db/migrations/meta/0034_snapshot.json +++ b/packages/db/migrations/meta/0034_snapshot.json @@ -223,9 +223,7 @@ "organizations_slug_unique": { "name": "organizations_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -385,12 +383,8 @@ "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -398,12 +392,8 @@ "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "inviter_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -411,12 +401,8 @@ "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -426,9 +412,7 @@ "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -845,12 +829,8 @@ "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -860,51 +840,37 @@ "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "users_anonymous_session_id_unique": { "name": "users_anonymous_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "anonymous_session_id" - ] + "columns": ["anonymous_session_id"] }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "users_phone_number_unique": { "name": "users_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] }, "users_discord_id_unique": { "name": "users_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -1097,12 +1063,8 @@ "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1110,12 +1072,8 @@ "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1125,9 +1083,7 @@ "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -1337,12 +1293,8 @@ "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1352,9 +1304,7 @@ "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -1551,12 +1501,8 @@ "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1564,12 +1510,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1579,16 +1521,12 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", "nullsNotDistinct": false, - "columns": [ - "key_hash" - ] + "columns": ["key_hash"] } }, "policies": {}, @@ -1731,12 +1669,8 @@ "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1746,9 +1680,7 @@ "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -2034,12 +1966,8 @@ "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2047,12 +1975,8 @@ "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2060,12 +1984,8 @@ "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2232,12 +2152,8 @@ "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2392,12 +2308,8 @@ "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2405,12 +2317,8 @@ "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2557,9 +2465,7 @@ "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_price_id" - ] + "columns": ["stripe_price_id"] } }, "policies": {}, @@ -2743,9 +2649,7 @@ "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_invoice_id" - ] + "columns": ["stripe_invoice_id"] } }, "policies": {}, @@ -3096,12 +3000,8 @@ "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3109,12 +3009,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3122,12 +3018,8 @@ "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3135,12 +3027,8 @@ "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3350,12 +3238,8 @@ "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3363,12 +3247,8 @@ "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3376,12 +3256,8 @@ "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3389,12 +3265,8 @@ "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", "tableTo": "generations", - "columnsFrom": [ - "generation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3788,12 +3660,8 @@ "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3801,12 +3669,8 @@ "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3968,12 +3832,8 @@ "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3981,12 +3841,8 @@ "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4496,12 +4352,8 @@ "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4509,12 +4361,8 @@ "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4524,9 +4372,7 @@ "user_characters_username_unique": { "name": "user_characters_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -4739,12 +4585,8 @@ "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4752,12 +4594,8 @@ "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4767,9 +4605,7 @@ "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", "nullsNotDistinct": false, - "columns": [ - "elevenlabs_voice_id" - ] + "columns": ["elevenlabs_voice_id"] } }, "policies": {}, @@ -4896,12 +4732,8 @@ "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4909,12 +4741,8 @@ "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4922,12 +4750,8 @@ "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5048,12 +4872,8 @@ "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5061,12 +4881,8 @@ "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", "tableTo": "voice_cloning_jobs", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5074,12 +4890,8 @@ "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5087,12 +4899,8 @@ "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5238,12 +5046,8 @@ "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5251,12 +5055,8 @@ "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5264,12 +5064,8 @@ "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5718,12 +5514,8 @@ "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5731,12 +5523,8 @@ "name": "containers_user_id_users_id_fk", "tableFrom": "containers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5744,12 +5532,8 @@ "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -5757,12 +5541,8 @@ "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5846,9 +5626,7 @@ "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", "nullsNotDistinct": false, - "columns": [ - "priority" - ] + "columns": ["priority"] } }, "policies": {}, @@ -6081,12 +5859,8 @@ "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6321,12 +6095,8 @@ "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6334,12 +6104,8 @@ "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6506,12 +6272,8 @@ "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6519,12 +6281,8 @@ "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6971,12 +6729,8 @@ "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6984,12 +6738,8 @@ "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6999,23 +6749,17 @@ "apps_slug_unique": { "name": "apps_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", "nullsNotDistinct": false, - "columns": [ - "api_key_id" - ] + "columns": ["api_key_id"] }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", "nullsNotDistinct": false, - "columns": [ - "affiliate_code" - ] + "columns": ["affiliate_code"] } }, "policies": {}, @@ -7160,12 +6904,8 @@ "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7173,12 +6913,8 @@ "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7186,12 +6922,8 @@ "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7311,12 +7043,8 @@ "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7456,12 +7184,8 @@ "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7469,12 +7193,8 @@ "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7594,12 +7314,8 @@ "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7609,9 +7325,7 @@ "referral_codes_code_unique": { "name": "referral_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -7760,12 +7474,8 @@ "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7773,12 +7483,8 @@ "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referrer_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referrer_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7786,12 +7492,8 @@ "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referred_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referred_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7799,12 +7501,8 @@ "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "app_owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_owner_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7812,12 +7510,8 @@ "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7957,12 +7651,8 @@ "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8144,12 +7834,8 @@ "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8157,10 +7843,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -8191,12 +7874,8 @@ "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8204,10 +7883,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -8288,12 +7964,8 @@ "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", "tableTo": "message_servers", - "columnsFrom": [ - "message_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8372,12 +8044,8 @@ "name": "components_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8385,12 +8053,8 @@ "name": "components_agent_id_agents_id_fk", "tableFrom": "components", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8398,12 +8062,8 @@ "name": "components_room_id_rooms_id_fk", "tableFrom": "components", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8411,12 +8071,8 @@ "name": "components_world_id_worlds_id_fk", "tableFrom": "components", "tableTo": "worlds", - "columnsFrom": [ - "world_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["world_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8424,12 +8080,8 @@ "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8523,12 +8175,8 @@ "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8536,12 +8184,8 @@ "name": "fk_embedding_memory", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8601,12 +8245,8 @@ "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8616,10 +8256,7 @@ "id_agent_id_unique": { "name": "id_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "id", - "agent_id" - ] + "columns": ["id", "agent_id"] } }, "policies": {}, @@ -8675,12 +8312,8 @@ "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8688,12 +8321,8 @@ "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8701,12 +8330,8 @@ "name": "fk_room", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8714,12 +8339,8 @@ "name": "fk_user", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9162,12 +8783,8 @@ "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9175,12 +8792,8 @@ "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9188,12 +8801,8 @@ "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9201,12 +8810,8 @@ "name": "fk_room", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9214,12 +8819,8 @@ "name": "fk_user", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9227,12 +8828,8 @@ "name": "fk_agent", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9388,12 +8985,8 @@ "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9401,12 +8994,8 @@ "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", "tableTo": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9497,12 +9086,8 @@ "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9510,12 +9095,8 @@ "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9523,12 +9104,8 @@ "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9536,12 +9113,8 @@ "name": "fk_room", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9549,12 +9122,8 @@ "name": "fk_user", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9642,12 +9211,8 @@ "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9655,12 +9220,8 @@ "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9668,12 +9229,8 @@ "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9681,12 +9238,8 @@ "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9694,12 +9247,8 @@ "name": "fk_user_b", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9709,11 +9258,7 @@ "unique_relationship": { "name": "unique_relationship", "nullsNotDistinct": false, - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ] + "columns": ["source_entity_id", "target_entity_id", "agent_id"] } }, "policies": {}, @@ -9793,12 +9338,8 @@ "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10043,12 +9584,8 @@ "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10108,12 +9645,8 @@ "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10167,12 +9700,8 @@ "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10355,12 +9884,8 @@ "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10368,12 +9893,8 @@ "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10564,12 +10085,8 @@ "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10577,12 +10094,8 @@ "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10590,12 +10103,8 @@ "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -11078,12 +10587,8 @@ "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11091,12 +10596,8 @@ "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11104,12 +10605,8 @@ "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11117,12 +10614,8 @@ "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "verified_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["verified_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -11308,12 +10801,8 @@ "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11613,12 +11102,8 @@ "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11626,12 +11111,8 @@ "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11639,12 +11120,8 @@ "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -11798,12 +11275,8 @@ "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11813,9 +11286,7 @@ "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -12001,12 +11472,8 @@ "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12092,9 +11559,7 @@ "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", "nullsNotDistinct": false, - "columns": [ - "ledger_entry_id" - ] + "columns": ["ledger_entry_id"] } }, "policies": {}, @@ -12245,12 +11710,8 @@ "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12258,12 +11719,8 @@ "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "granted_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12273,9 +11730,7 @@ "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -12423,12 +11878,8 @@ "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12436,12 +11887,8 @@ "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12610,12 +12057,8 @@ "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12623,12 +12066,8 @@ "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "banned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12638,9 +12077,7 @@ "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -12812,12 +12249,8 @@ "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "agent_budgets", - "columnsFrom": [ - "budget_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["budget_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12825,12 +12258,8 @@ "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13036,12 +12465,8 @@ "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13049,12 +12474,8 @@ "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", "tableTo": "organizations", - "columnsFrom": [ - "owner_org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_org_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13064,9 +12485,7 @@ "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "agent_id" - ] + "columns": ["agent_id"] } }, "policies": {}, @@ -13332,12 +12751,8 @@ "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13345,12 +12760,8 @@ "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13473,12 +12884,8 @@ "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13752,12 +13159,8 @@ "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13765,12 +13168,8 @@ "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13778,12 +13177,8 @@ "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13793,9 +13188,7 @@ "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", "nullsNotDistinct": false, - "columns": [ - "sandbox_id" - ] + "columns": ["sandbox_id"] } }, "policies": {}, @@ -13980,9 +13373,7 @@ "app_templates_slug_unique": { "name": "app_templates_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -14144,9 +13535,7 @@ "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", "nullsNotDistinct": false, - "columns": [ - "snapshot_id" - ] + "columns": ["snapshot_id"] } }, "policies": {}, @@ -14268,12 +13657,8 @@ "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14375,12 +13760,8 @@ "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14520,9 +13901,7 @@ "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", "nullsNotDistinct": false, - "columns": [ - "event_id" - ] + "columns": ["event_id"] } }, "policies": {}, @@ -14651,12 +14030,8 @@ "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14664,12 +14039,8 @@ "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "users", - "columnsFrom": [ - "approved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14966,12 +14337,8 @@ "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14979,12 +14346,8 @@ "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15368,12 +14731,8 @@ "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15381,12 +14740,8 @@ "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", "tableTo": "secrets", - "columnsFrom": [ - "secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["secret_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15394,12 +14749,8 @@ "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -15707,12 +15058,8 @@ "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15720,12 +15067,8 @@ "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -15899,12 +15242,8 @@ "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16157,12 +15496,8 @@ "name": "org_feed_configs_organization_id_organizations_id_fk", "tableFrom": "org_feed_configs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16170,12 +15505,8 @@ "name": "org_feed_configs_created_by_users_id_fk", "tableFrom": "org_feed_configs", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -16457,12 +15788,8 @@ "name": "pending_reply_confirmations_organization_id_organizations_id_fk", "tableFrom": "pending_reply_confirmations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16470,12 +15797,8 @@ "name": "pending_reply_confirmations_engagement_event_id_social_engagement_events_id_fk", "tableFrom": "pending_reply_confirmations", "tableTo": "social_engagement_events", - "columnsFrom": [ - "engagement_event_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["engagement_event_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16757,12 +16080,8 @@ "name": "social_engagement_events_organization_id_organizations_id_fk", "tableFrom": "social_engagement_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16770,12 +16089,8 @@ "name": "social_engagement_events_feed_config_id_org_feed_configs_id_fk", "tableFrom": "social_engagement_events", "tableTo": "org_feed_configs", - "columnsFrom": [ - "feed_config_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["feed_config_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16944,12 +16259,8 @@ "name": "social_notification_messages_organization_id_organizations_id_fk", "tableFrom": "social_notification_messages", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16957,12 +16268,8 @@ "name": "social_notification_messages_engagement_event_id_social_engagement_events_id_fk", "tableFrom": "social_notification_messages", "tableTo": "social_engagement_events", - "columnsFrom": [ - "engagement_event_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["engagement_event_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17426,12 +16733,8 @@ "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17439,12 +16742,8 @@ "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17452,12 +16751,8 @@ "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17465,12 +16760,8 @@ "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17478,12 +16769,8 @@ "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17493,9 +16780,7 @@ "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", "nullsNotDistinct": false, - "columns": [ - "domain" - ] + "columns": ["domain"] } }, "policies": {}, @@ -17684,12 +16969,8 @@ "name": "domain_moderation_events_domain_id_managed_domains_id_fk", "tableFrom": "domain_moderation_events", "tableTo": "managed_domains", - "columnsFrom": [ - "domain_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["domain_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17697,12 +16978,8 @@ "name": "domain_moderation_events_admin_user_id_users_id_fk", "tableFrom": "domain_moderation_events", "tableTo": "users", - "columnsFrom": [ - "admin_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["admin_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17710,12 +16987,8 @@ "name": "domain_moderation_events_resolved_by_users_id_fk", "tableFrom": "domain_moderation_events", "tableTo": "users", - "columnsFrom": [ - "resolved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resolved_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17926,12 +17199,8 @@ "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17939,12 +17208,8 @@ "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17952,12 +17217,8 @@ "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "users", - "columnsFrom": [ - "requesting_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requesting_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17965,12 +17226,8 @@ "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "platform_credentials", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17980,9 +17237,7 @@ "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -18292,12 +17547,8 @@ "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18305,12 +17556,8 @@ "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18318,12 +17565,8 @@ "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18507,12 +17750,8 @@ "name": "org_checkin_responses_schedule_id_org_checkin_schedules_id_fk", "tableFrom": "org_checkin_responses", "tableTo": "org_checkin_schedules", - "columnsFrom": [ - "schedule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18520,12 +17759,8 @@ "name": "org_checkin_responses_organization_id_organizations_id_fk", "tableFrom": "org_checkin_responses", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18721,12 +17956,8 @@ "name": "org_checkin_schedules_organization_id_organizations_id_fk", "tableFrom": "org_checkin_schedules", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18734,12 +17965,8 @@ "name": "org_checkin_schedules_server_id_org_platform_servers_id_fk", "tableFrom": "org_checkin_schedules", "tableTo": "org_platform_servers", - "columnsFrom": [ - "server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18747,12 +17974,8 @@ "name": "org_checkin_schedules_created_by_users_id_fk", "tableFrom": "org_checkin_schedules", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18968,12 +18191,8 @@ "name": "org_platform_connections_organization_id_organizations_id_fk", "tableFrom": "org_platform_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18981,12 +18200,8 @@ "name": "org_platform_connections_connected_by_users_id_fk", "tableFrom": "org_platform_connections", "tableTo": "users", - "columnsFrom": [ - "connected_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -19179,12 +18394,8 @@ "name": "org_platform_servers_connection_id_org_platform_connections_id_fk", "tableFrom": "org_platform_servers", "tableTo": "org_platform_connections", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19192,12 +18403,8 @@ "name": "org_platform_servers_organization_id_organizations_id_fk", "tableFrom": "org_platform_servers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19414,12 +18621,8 @@ "name": "org_team_members_organization_id_organizations_id_fk", "tableFrom": "org_team_members", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19427,12 +18630,8 @@ "name": "org_team_members_server_id_org_platform_servers_id_fk", "tableFrom": "org_team_members", "tableTo": "org_platform_servers", - "columnsFrom": [ - "server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19671,12 +18870,8 @@ "name": "org_todos_organization_id_organizations_id_fk", "tableFrom": "org_todos", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19684,12 +18879,8 @@ "name": "org_todos_created_by_user_id_users_id_fk", "tableFrom": "org_todos", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -19876,12 +19067,8 @@ "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19889,12 +19076,8 @@ "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", "tableTo": "users", - "columnsFrom": [ - "connected_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19902,12 +19085,8 @@ "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "access_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["access_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19915,12 +19094,8 @@ "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "refresh_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refresh_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20227,12 +19402,8 @@ "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20240,12 +19411,8 @@ "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", "tableTo": "ad_accounts", - "columnsFrom": [ - "ad_account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ad_account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20253,12 +19420,8 @@ "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20452,12 +19615,8 @@ "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20652,12 +19811,8 @@ "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20665,12 +19820,8 @@ "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -20678,12 +19829,8 @@ "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20776,12 +19923,8 @@ "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20934,12 +20077,8 @@ "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21181,12 +20320,8 @@ "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21194,12 +20329,8 @@ "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -21207,12 +20338,8 @@ "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -21220,12 +20347,8 @@ "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21343,12 +20466,8 @@ "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21493,12 +20612,8 @@ "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21678,12 +20793,8 @@ "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21949,12 +21060,8 @@ "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21962,12 +21069,8 @@ "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -22232,12 +21335,8 @@ "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22463,12 +21562,8 @@ "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", "tableTo": "agent_phone_numbers", - "columnsFrom": [ - "phone_number_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["phone_number_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22554,9 +21649,7 @@ "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -22722,12 +21815,8 @@ "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22827,12 +21916,8 @@ "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22842,9 +21927,7 @@ "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -22924,12 +22007,8 @@ "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22937,12 +22016,8 @@ "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", "tableTo": "affiliate_codes", - "columnsFrom": [ - "affiliate_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["affiliate_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23118,12 +22193,8 @@ "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23131,12 +22202,8 @@ "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23144,12 +22211,8 @@ "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -23165,72 +22228,37 @@ "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -23260,84 +22288,42 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -23362,11 +22348,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.reply_confirmation_status": { "name": "reply_confirmation_status", @@ -23396,49 +22378,27 @@ "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.domain_event_detected_by": { "name": "domain_event_detected_by", @@ -23454,13 +22414,7 @@ "public.domain_event_severity": { "name": "domain_event_severity", "schema": "public", - "values": [ - "info", - "low", - "medium", - "high", - "critical" - ] + "values": ["info", "low", "medium", "high", "critical"] }, "public.domain_event_type": { "name": "domain_event_type", @@ -23484,13 +22438,7 @@ "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -23538,13 +22486,7 @@ "public.org_checkin_frequency": { "name": "org_checkin_frequency", "schema": "public", - "values": [ - "daily", - "weekdays", - "weekly", - "bi_weekly", - "monthly" - ] + "values": ["daily", "weekdays", "weekly", "bi_weekly", "monthly"] }, "public.org_checkin_type": { "name": "org_checkin_type", @@ -23560,42 +22502,22 @@ "public.org_platform_status": { "name": "org_platform_status", "schema": "public", - "values": [ - "active", - "disconnected", - "error", - "pending" - ] + "values": ["active", "disconnected", "error", "pending"] }, "public.org_platform_type": { "name": "org_platform_type", "schema": "public", - "values": [ - "discord", - "telegram", - "slack", - "twitter" - ] + "values": ["discord", "telegram", "slack", "twitter"] }, "public.org_todo_priority": { "name": "org_todo_priority", "schema": "public", - "values": [ - "low", - "medium", - "high", - "urgent" - ] + "values": ["low", "medium", "high", "urgent"] }, "public.org_todo_status": { "name": "org_todo_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "cancelled" - ] + "values": ["pending", "in_progress", "completed", "cancelled"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -23612,32 +22534,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -23655,22 +22562,12 @@ "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "other" - ] + "values": ["twilio", "blooio", "vonage", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage" - ] + "values": ["sms", "voice", "both", "imessage"] } }, "schemas": {}, @@ -23683,4 +22580,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0036_snapshot.json b/packages/db/migrations/meta/0036_snapshot.json index 537c1d2c0..9714644c3 100644 --- a/packages/db/migrations/meta/0036_snapshot.json +++ b/packages/db/migrations/meta/0036_snapshot.json @@ -15,4 +15,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0038_snapshot.json b/packages/db/migrations/meta/0038_snapshot.json index ca85d3809..7aae1236e 100644 --- a/packages/db/migrations/meta/0038_snapshot.json +++ b/packages/db/migrations/meta/0038_snapshot.json @@ -222,9 +222,7 @@ "uniqueConstraints": { "organizations_slug_unique": { "name": "organizations_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false } }, @@ -384,39 +382,27 @@ "organization_invites_organization_id_organizations_id_fk": { "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "organization_invites_inviter_user_id_users_id_fk": { "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "inviter_user_id" - ], + "columnsFrom": ["inviter_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "organization_invites_accepted_by_user_id_users_id_fk": { "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "accepted_by_user_id" - ], + "columnsFrom": ["accepted_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -425,9 +411,7 @@ "uniqueConstraints": { "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", - "columns": [ - "token_hash" - ], + "columns": ["token_hash"], "nullsNotDistinct": false } }, @@ -844,13 +828,9 @@ "users_organization_id_organizations_id_fk": { "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -859,51 +839,37 @@ "uniqueConstraints": { "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", - "columns": [ - "privy_user_id" - ], + "columns": ["privy_user_id"], "nullsNotDistinct": false }, "users_anonymous_session_id_unique": { "name": "users_anonymous_session_id_unique", - "columns": [ - "anonymous_session_id" - ], + "columns": ["anonymous_session_id"], "nullsNotDistinct": false }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", - "columns": [ - "telegram_id" - ], + "columns": ["telegram_id"], "nullsNotDistinct": false }, "users_phone_number_unique": { "name": "users_phone_number_unique", - "columns": [ - "phone_number" - ], + "columns": ["phone_number"], "nullsNotDistinct": false }, "users_discord_id_unique": { "name": "users_discord_id_unique", - "columns": [ - "discord_id" - ], + "columns": ["discord_id"], "nullsNotDistinct": false }, "users_email_unique": { "name": "users_email_unique", - "columns": [ - "email" - ], + "columns": ["email"], "nullsNotDistinct": false }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", - "columns": [ - "wallet_address" - ], + "columns": ["wallet_address"], "nullsNotDistinct": false } }, @@ -1096,26 +1062,18 @@ "user_sessions_user_id_users_id_fk": { "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_sessions_organization_id_organizations_id_fk": { "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1124,9 +1082,7 @@ "uniqueConstraints": { "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", - "columns": [ - "session_token" - ], + "columns": ["session_token"], "nullsNotDistinct": false } }, @@ -1336,13 +1292,9 @@ "anonymous_sessions_user_id_users_id_fk": { "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1351,9 +1303,7 @@ "uniqueConstraints": { "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", - "columns": [ - "session_token" - ], + "columns": ["session_token"], "nullsNotDistinct": false } }, @@ -1550,26 +1500,18 @@ "api_keys_organization_id_organizations_id_fk": { "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "api_keys_user_id_users_id_fk": { "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1578,16 +1520,12 @@ "uniqueConstraints": { "api_keys_key_unique": { "name": "api_keys_key_unique", - "columns": [ - "key" - ], + "columns": ["key"], "nullsNotDistinct": false }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", - "columns": [ - "key_hash" - ], + "columns": ["key_hash"], "nullsNotDistinct": false } }, @@ -1730,13 +1668,9 @@ "cli_auth_sessions_user_id_users_id_fk": { "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1745,9 +1679,7 @@ "uniqueConstraints": { "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nullsNotDistinct": false } }, @@ -2033,39 +1965,27 @@ "usage_records_organization_id_organizations_id_fk": { "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "usage_records_user_id_users_id_fk": { "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "usage_records_api_key_id_api_keys_id_fk": { "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2231,13 +2151,9 @@ "usage_quotas_organization_id_organizations_id_fk": { "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2391,26 +2307,18 @@ "credit_transactions_organization_id_organizations_id_fk": { "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "credit_transactions_user_id_users_id_fk": { "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2556,9 +2464,7 @@ "uniqueConstraints": { "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", - "columns": [ - "stripe_price_id" - ], + "columns": ["stripe_price_id"], "nullsNotDistinct": false } }, @@ -2742,9 +2648,7 @@ "uniqueConstraints": { "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", - "columns": [ - "stripe_invoice_id" - ], + "columns": ["stripe_invoice_id"], "nullsNotDistinct": false } }, @@ -3095,52 +2999,36 @@ "generations_organization_id_organizations_id_fk": { "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "generations_user_id_users_id_fk": { "name": "generations_user_id_users_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "generations_api_key_id_api_keys_id_fk": { "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "generations_usage_record_id_usage_records_id_fk": { "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "usage_record_id" - ], + "columnsFrom": ["usage_record_id"], "tableTo": "usage_records", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3349,52 +3237,36 @@ "jobs_organization_id_organizations_id_fk": { "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "jobs_user_id_users_id_fk": { "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" }, "jobs_api_key_id_api_keys_id_fk": { "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" }, "jobs_generation_id_generations_id_fk": { "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "generation_id" - ], + "columnsFrom": ["generation_id"], "tableTo": "generations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -3787,26 +3659,18 @@ "conversation_messages_conversation_id_conversations_id_fk": { "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", - "columnsFrom": [ - "conversation_id" - ], + "columnsFrom": ["conversation_id"], "tableTo": "conversations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "conversation_messages_usage_record_id_usage_records_id_fk": { "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", - "columnsFrom": [ - "usage_record_id" - ], + "columnsFrom": ["usage_record_id"], "tableTo": "usage_records", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3967,26 +3831,18 @@ "conversations_organization_id_organizations_id_fk": { "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "conversations_user_id_users_id_fk": { "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4495,26 +4351,18 @@ "user_characters_organization_id_organizations_id_fk": { "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_characters_user_id_users_id_fk": { "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4523,9 +4371,7 @@ "uniqueConstraints": { "user_characters_username_unique": { "name": "user_characters_username_unique", - "columns": [ - "username" - ], + "columns": ["username"], "nullsNotDistinct": false } }, @@ -4738,26 +4584,18 @@ "user_voices_organization_id_organizations_id_fk": { "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_voices_user_id_users_id_fk": { "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4766,9 +4604,7 @@ "uniqueConstraints": { "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", - "columns": [ - "elevenlabs_voice_id" - ], + "columns": ["elevenlabs_voice_id"], "nullsNotDistinct": false } }, @@ -4895,39 +4731,27 @@ "voice_cloning_jobs_organization_id_organizations_id_fk": { "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_cloning_jobs_user_id_users_id_fk": { "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_cloning_jobs_user_voice_id_user_voices_id_fk": { "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "user_voice_id" - ], + "columnsFrom": ["user_voice_id"], "tableTo": "user_voices", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -5047,52 +4871,36 @@ "voice_samples_user_voice_id_user_voices_id_fk": { "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "user_voice_id" - ], + "columnsFrom": ["user_voice_id"], "tableTo": "user_voices", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_job_id_voice_cloning_jobs_id_fk": { "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "job_id" - ], + "columnsFrom": ["job_id"], "tableTo": "voice_cloning_jobs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_organization_id_organizations_id_fk": { "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_user_id_users_id_fk": { "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5237,39 +5045,27 @@ "container_billing_records_container_id_containers_id_fk": { "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "container_billing_records_organization_id_organizations_id_fk": { "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "container_billing_records_credit_transaction_id_credit_transactions_id_fk": { "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "credit_transaction_id" - ], + "columnsFrom": ["credit_transaction_id"], "tableTo": "credit_transactions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -5717,52 +5513,36 @@ "containers_organization_id_organizations_id_fk": { "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "containers_user_id_users_id_fk": { "name": "containers_user_id_users_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "containers_api_key_id_api_keys_id_fk": { "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "containers_character_id_user_characters_id_fk": { "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -5845,9 +5625,7 @@ "uniqueConstraints": { "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", - "columns": [ - "priority" - ], + "columns": ["priority"], "nullsNotDistinct": false } }, @@ -6080,13 +5858,9 @@ "app_analytics_app_id_apps_id_fk": { "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6320,26 +6094,18 @@ "app_requests_app_id_apps_id_fk": { "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_requests_user_id_users_id_fk": { "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6505,26 +6271,18 @@ "app_users_app_id_apps_id_fk": { "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_users_user_id_users_id_fk": { "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6970,26 +6728,18 @@ "apps_organization_id_organizations_id_fk": { "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "apps_created_by_user_id_users_id_fk": { "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6998,23 +6748,17 @@ "uniqueConstraints": { "apps_slug_unique": { "name": "apps_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", - "columns": [ - "api_key_id" - ], + "columns": ["api_key_id"], "nullsNotDistinct": false }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", - "columns": [ - "affiliate_code" - ], + "columns": ["affiliate_code"], "nullsNotDistinct": false } }, @@ -7159,39 +6903,27 @@ "app_credit_balances_app_id_apps_id_fk": { "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_credit_balances_user_id_users_id_fk": { "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_credit_balances_organization_id_organizations_id_fk": { "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7310,13 +7042,9 @@ "app_earnings_app_id_apps_id_fk": { "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7455,26 +7183,18 @@ "app_earnings_transactions_app_id_apps_id_fk": { "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_earnings_transactions_user_id_users_id_fk": { "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7593,13 +7313,9 @@ "referral_codes_user_id_users_id_fk": { "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7608,9 +7324,7 @@ "uniqueConstraints": { "referral_codes_code_unique": { "name": "referral_codes_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "nullsNotDistinct": false } }, @@ -7759,65 +7473,45 @@ "referral_signups_referral_code_id_referral_codes_id_fk": { "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referral_code_id" - ], + "columnsFrom": ["referral_code_id"], "tableTo": "referral_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_referrer_user_id_users_id_fk": { "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referrer_user_id" - ], + "columnsFrom": ["referrer_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_referred_user_id_users_id_fk": { "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referred_user_id" - ], + "columnsFrom": ["referred_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_app_owner_id_users_id_fk": { "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "app_owner_id" - ], + "columnsFrom": ["app_owner_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "referral_signups_creator_id_users_id_fk": { "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "creator_id" - ], + "columnsFrom": ["creator_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7956,13 +7650,9 @@ "social_share_rewards_user_id_users_id_fk": { "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8143,13 +7833,9 @@ "cache_agent_id_agents_id_fk": { "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8157,10 +7843,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -8190,13 +7873,9 @@ "channel_participants_channel_id_channels_id_fk": { "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8204,10 +7883,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -8287,13 +7963,9 @@ "channels_message_server_id_message_servers_id_fk": { "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", - "columnsFrom": [ - "message_server_id" - ], + "columnsFrom": ["message_server_id"], "tableTo": "message_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8371,65 +8043,45 @@ "components_entity_id_entities_id_fk": { "name": "components_entity_id_entities_id_fk", "tableFrom": "components", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_agent_id_agents_id_fk": { "name": "components_agent_id_agents_id_fk", "tableFrom": "components", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_room_id_rooms_id_fk": { "name": "components_room_id_rooms_id_fk", "tableFrom": "components", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_world_id_worlds_id_fk": { "name": "components_world_id_worlds_id_fk", "tableFrom": "components", - "columnsFrom": [ - "world_id" - ], + "columnsFrom": ["world_id"], "tableTo": "worlds", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_source_entity_id_entities_id_fk": { "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8522,26 +8174,18 @@ "embeddings_memory_id_memories_id_fk": { "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", - "columnsFrom": [ - "memory_id" - ], + "columnsFrom": ["memory_id"], "tableTo": "memories", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_embedding_memory": { "name": "fk_embedding_memory", "tableFrom": "embeddings", - "columnsFrom": [ - "memory_id" - ], + "columnsFrom": ["memory_id"], "tableTo": "memories", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8600,13 +8244,9 @@ "entities_agent_id_agents_id_fk": { "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8615,10 +8255,7 @@ "uniqueConstraints": { "id_agent_id_unique": { "name": "id_agent_id_unique", - "columns": [ - "id", - "agent_id" - ], + "columns": ["id", "agent_id"], "nullsNotDistinct": false } }, @@ -8674,52 +8311,36 @@ "logs_entity_id_entities_id_fk": { "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "logs_room_id_rooms_id_fk": { "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "logs", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "logs", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9161,78 +8782,54 @@ "memories_entity_id_entities_id_fk": { "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memories_agent_id_agents_id_fk": { "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memories_room_id_rooms_id_fk": { "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "memories", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "memories", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_agent": { "name": "fk_agent", "tableFrom": "memories", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9387,26 +8984,18 @@ "central_messages_channel_id_channels_id_fk": { "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "central_messages_in_reply_to_root_message_id_central_messages_id_fk": { "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], "tableTo": "central_messages", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -9496,65 +9085,45 @@ "participants_entity_id_entities_id_fk": { "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "participants_room_id_rooms_id_fk": { "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "participants_agent_id_agents_id_fk": { "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "participants", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "participants", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9641,65 +9210,45 @@ "relationships_source_entity_id_entities_id_fk": { "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "relationships_target_entity_id_entities_id_fk": { "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "target_entity_id" - ], + "columnsFrom": ["target_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "relationships_agent_id_agents_id_fk": { "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user_a": { "name": "fk_user_a", "tableFrom": "relationships", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user_b": { "name": "fk_user_b", "tableFrom": "relationships", - "columnsFrom": [ - "target_entity_id" - ], + "columnsFrom": ["target_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9708,11 +9257,7 @@ "uniqueConstraints": { "unique_relationship": { "name": "unique_relationship", - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ], + "columns": ["source_entity_id", "target_entity_id", "agent_id"], "nullsNotDistinct": false } }, @@ -9792,13 +9337,9 @@ "rooms_agent_id_agents_id_fk": { "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10042,13 +9583,9 @@ "tasks_agent_id_agents_id_fk": { "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10107,13 +9644,9 @@ "worlds_agent_id_agents_id_fk": { "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10166,13 +9699,9 @@ "eliza_room_characters_character_id_user_characters_id_fk": { "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10354,26 +9883,18 @@ "agent_events_agent_id_user_characters_id_fk": { "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_events_organization_id_organizations_id_fk": { "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10563,39 +10084,27 @@ "mcp_usage_mcp_id_user_mcps_id_fk": { "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "mcp_id" - ], + "columnsFrom": ["mcp_id"], "tableTo": "user_mcps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "mcp_usage_organization_id_organizations_id_fk": { "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "mcp_usage_user_id_users_id_fk": { "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -11077,52 +10586,36 @@ "user_mcps_organization_id_organizations_id_fk": { "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_mcps_created_by_user_id_users_id_fk": { "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_mcps_container_id_containers_id_fk": { "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "user_mcps_verified_by_users_id_fk": { "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "verified_by" - ], + "columnsFrom": ["verified_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -11307,13 +10800,9 @@ "redemption_limits_user_id_users_id_fk": { "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11612,39 +11101,27 @@ "token_redemptions_user_id_users_id_fk": { "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "token_redemptions_app_id_apps_id_fk": { "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "token_redemptions_reviewed_by_users_id_fk": { "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "reviewed_by" - ], + "columnsFrom": ["reviewed_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -11797,13 +11274,9 @@ "redeemable_earnings_user_id_users_id_fk": { "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11812,9 +11285,7 @@ "uniqueConstraints": { "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -12000,13 +11471,9 @@ "redeemable_earnings_ledger_user_id_users_id_fk": { "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -12091,9 +11558,7 @@ "uniqueConstraints": { "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", - "columns": [ - "ledger_entry_id" - ], + "columns": ["ledger_entry_id"], "nullsNotDistinct": false } }, @@ -12244,26 +11709,18 @@ "admin_users_user_id_users_id_fk": { "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "admin_users_granted_by_users_id_fk": { "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", - "columnsFrom": [ - "granted_by" - ], + "columnsFrom": ["granted_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12272,9 +11729,7 @@ "uniqueConstraints": { "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", - "columns": [ - "wallet_address" - ], + "columns": ["wallet_address"], "nullsNotDistinct": false } }, @@ -12422,26 +11877,18 @@ "moderation_violations_user_id_users_id_fk": { "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "moderation_violations_reviewed_by_users_id_fk": { "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", - "columnsFrom": [ - "reviewed_by" - ], + "columnsFrom": ["reviewed_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12609,26 +12056,18 @@ "user_moderation_status_user_id_users_id_fk": { "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_moderation_status_banned_by_users_id_fk": { "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", - "columnsFrom": [ - "banned_by" - ], + "columnsFrom": ["banned_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12637,9 +12076,7 @@ "uniqueConstraints": { "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -12811,26 +12248,18 @@ "agent_budget_transactions_budget_id_agent_budgets_id_fk": { "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", - "columnsFrom": [ - "budget_id" - ], + "columnsFrom": ["budget_id"], "tableTo": "agent_budgets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_budget_transactions_agent_id_user_characters_id_fk": { "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13035,26 +12464,18 @@ "agent_budgets_agent_id_user_characters_id_fk": { "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_budgets_owner_org_id_organizations_id_fk": { "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", - "columnsFrom": [ - "owner_org_id" - ], + "columnsFrom": ["owner_org_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13063,9 +12484,7 @@ "uniqueConstraints": { "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", - "columns": [ - "agent_id" - ], + "columns": ["agent_id"], "nullsNotDistinct": false } }, @@ -13331,26 +12750,18 @@ "crypto_payments_organization_id_organizations_id_fk": { "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "crypto_payments_user_id_users_id_fk": { "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13472,13 +12883,9 @@ "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13751,39 +13158,27 @@ "app_sandbox_sessions_user_id_users_id_fk": { "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_sandbox_sessions_organization_id_organizations_id_fk": { "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_sandbox_sessions_app_id_apps_id_fk": { "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13792,9 +13187,7 @@ "uniqueConstraints": { "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", - "columns": [ - "sandbox_id" - ], + "columns": ["sandbox_id"], "nullsNotDistinct": false } }, @@ -13979,9 +13372,7 @@ "uniqueConstraints": { "app_templates_slug_unique": { "name": "app_templates_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false } }, @@ -14143,9 +13534,7 @@ "uniqueConstraints": { "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", - "columns": [ - "snapshot_id" - ], + "columns": ["snapshot_id"], "nullsNotDistinct": false } }, @@ -14267,13 +13656,9 @@ "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14374,13 +13759,9 @@ "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14519,9 +13900,7 @@ "uniqueConstraints": { "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", - "columns": [ - "event_id" - ], + "columns": ["event_id"], "nullsNotDistinct": false } }, @@ -14650,26 +14029,18 @@ "app_secret_requirements_app_id_apps_id_fk": { "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_secret_requirements_approved_by_users_id_fk": { "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", - "columnsFrom": [ - "approved_by" - ], + "columnsFrom": ["approved_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -14965,26 +14336,18 @@ "oauth_sessions_organization_id_organizations_id_fk": { "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "oauth_sessions_user_id_users_id_fk": { "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -15367,39 +14730,27 @@ "secret_bindings_organization_id_organizations_id_fk": { "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secret_bindings_secret_id_secrets_id_fk": { "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "secret_id" - ], + "columnsFrom": ["secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secret_bindings_created_by_users_id_fk": { "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -15706,26 +15057,18 @@ "secrets_organization_id_organizations_id_fk": { "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secrets_created_by_users_id_fk": { "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -15898,13 +15241,9 @@ "app_domains_app_id_apps_id_fk": { "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -16156,26 +15495,18 @@ "org_feed_configs_organization_id_organizations_id_fk": { "name": "org_feed_configs_organization_id_organizations_id_fk", "tableFrom": "org_feed_configs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_feed_configs_created_by_users_id_fk": { "name": "org_feed_configs_created_by_users_id_fk", "tableFrom": "org_feed_configs", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -16456,26 +15787,18 @@ "pending_reply_confirmations_organization_id_organizations_id_fk": { "name": "pending_reply_confirmations_organization_id_organizations_id_fk", "tableFrom": "pending_reply_confirmations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "pending_reply_confirmations_engagement_event_id_social_engagement_events_id_fk": { "name": "pending_reply_confirmations_engagement_event_id_social_engagement_events_id_fk", "tableFrom": "pending_reply_confirmations", - "columnsFrom": [ - "engagement_event_id" - ], + "columnsFrom": ["engagement_event_id"], "tableTo": "social_engagement_events", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -16756,26 +16079,18 @@ "social_engagement_events_organization_id_organizations_id_fk": { "name": "social_engagement_events_organization_id_organizations_id_fk", "tableFrom": "social_engagement_events", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "social_engagement_events_feed_config_id_org_feed_configs_id_fk": { "name": "social_engagement_events_feed_config_id_org_feed_configs_id_fk", "tableFrom": "social_engagement_events", - "columnsFrom": [ - "feed_config_id" - ], + "columnsFrom": ["feed_config_id"], "tableTo": "org_feed_configs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -16943,26 +16258,18 @@ "social_notification_messages_organization_id_organizations_id_fk": { "name": "social_notification_messages_organization_id_organizations_id_fk", "tableFrom": "social_notification_messages", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "social_notification_messages_engagement_event_id_social_engagement_events_id_fk": { "name": "social_notification_messages_engagement_event_id_social_engagement_events_id_fk", "tableFrom": "social_notification_messages", - "columnsFrom": [ - "engagement_event_id" - ], + "columnsFrom": ["engagement_event_id"], "tableTo": "social_engagement_events", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -17425,65 +16732,45 @@ "managed_domains_organization_id_organizations_id_fk": { "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "managed_domains_app_id_apps_id_fk": { "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_container_id_containers_id_fk": { "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_agent_id_user_characters_id_fk": { "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_mcp_id_user_mcps_id_fk": { "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "mcp_id" - ], + "columnsFrom": ["mcp_id"], "tableTo": "user_mcps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17492,9 +16779,7 @@ "uniqueConstraints": { "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", - "columns": [ - "domain" - ], + "columns": ["domain"], "nullsNotDistinct": false } }, @@ -17683,39 +16968,27 @@ "domain_moderation_events_domain_id_managed_domains_id_fk": { "name": "domain_moderation_events_domain_id_managed_domains_id_fk", "tableFrom": "domain_moderation_events", - "columnsFrom": [ - "domain_id" - ], + "columnsFrom": ["domain_id"], "tableTo": "managed_domains", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "domain_moderation_events_admin_user_id_users_id_fk": { "name": "domain_moderation_events_admin_user_id_users_id_fk", "tableFrom": "domain_moderation_events", - "columnsFrom": [ - "admin_user_id" - ], + "columnsFrom": ["admin_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "domain_moderation_events_resolved_by_users_id_fk": { "name": "domain_moderation_events_resolved_by_users_id_fk", "tableFrom": "domain_moderation_events", - "columnsFrom": [ - "resolved_by" - ], + "columnsFrom": ["resolved_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17925,52 +17198,36 @@ "platform_credential_sessions_organization_id_organizations_id_fk": { "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credential_sessions_app_id_apps_id_fk": { "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credential_sessions_requesting_user_id_users_id_fk": { "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "requesting_user_id" - ], + "columnsFrom": ["requesting_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "platform_credential_sessions_credential_id_platform_credentials_id_fk": { "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "credential_id" - ], + "columnsFrom": ["credential_id"], "tableTo": "platform_credentials", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17979,9 +17236,7 @@ "uniqueConstraints": { "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nullsNotDistinct": false } }, @@ -18291,39 +17546,27 @@ "platform_credentials_organization_id_organizations_id_fk": { "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credentials_user_id_users_id_fk": { "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credentials_app_id_apps_id_fk": { "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -18506,26 +17749,18 @@ "org_checkin_responses_schedule_id_org_checkin_schedules_id_fk": { "name": "org_checkin_responses_schedule_id_org_checkin_schedules_id_fk", "tableFrom": "org_checkin_responses", - "columnsFrom": [ - "schedule_id" - ], + "columnsFrom": ["schedule_id"], "tableTo": "org_checkin_schedules", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_checkin_responses_organization_id_organizations_id_fk": { "name": "org_checkin_responses_organization_id_organizations_id_fk", "tableFrom": "org_checkin_responses", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -18720,39 +17955,27 @@ "org_checkin_schedules_organization_id_organizations_id_fk": { "name": "org_checkin_schedules_organization_id_organizations_id_fk", "tableFrom": "org_checkin_schedules", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_checkin_schedules_server_id_org_platform_servers_id_fk": { "name": "org_checkin_schedules_server_id_org_platform_servers_id_fk", "tableFrom": "org_checkin_schedules", - "columnsFrom": [ - "server_id" - ], + "columnsFrom": ["server_id"], "tableTo": "org_platform_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_checkin_schedules_created_by_users_id_fk": { "name": "org_checkin_schedules_created_by_users_id_fk", "tableFrom": "org_checkin_schedules", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -18967,26 +18190,18 @@ "org_platform_connections_organization_id_organizations_id_fk": { "name": "org_platform_connections_organization_id_organizations_id_fk", "tableFrom": "org_platform_connections", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_platform_connections_connected_by_users_id_fk": { "name": "org_platform_connections_connected_by_users_id_fk", "tableFrom": "org_platform_connections", - "columnsFrom": [ - "connected_by" - ], + "columnsFrom": ["connected_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -19178,26 +18393,18 @@ "org_platform_servers_connection_id_org_platform_connections_id_fk": { "name": "org_platform_servers_connection_id_org_platform_connections_id_fk", "tableFrom": "org_platform_servers", - "columnsFrom": [ - "connection_id" - ], + "columnsFrom": ["connection_id"], "tableTo": "org_platform_connections", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_platform_servers_organization_id_organizations_id_fk": { "name": "org_platform_servers_organization_id_organizations_id_fk", "tableFrom": "org_platform_servers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19413,26 +18620,18 @@ "org_team_members_organization_id_organizations_id_fk": { "name": "org_team_members_organization_id_organizations_id_fk", "tableFrom": "org_team_members", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_team_members_server_id_org_platform_servers_id_fk": { "name": "org_team_members_server_id_org_platform_servers_id_fk", "tableFrom": "org_team_members", - "columnsFrom": [ - "server_id" - ], + "columnsFrom": ["server_id"], "tableTo": "org_platform_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19670,26 +18869,18 @@ "org_todos_organization_id_organizations_id_fk": { "name": "org_todos_organization_id_organizations_id_fk", "tableFrom": "org_todos", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_todos_created_by_user_id_users_id_fk": { "name": "org_todos_created_by_user_id_users_id_fk", "tableFrom": "org_todos", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -19875,52 +19066,36 @@ "ad_accounts_organization_id_organizations_id_fk": { "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_accounts_connected_by_user_id_users_id_fk": { "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "connected_by_user_id" - ], + "columnsFrom": ["connected_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_accounts_access_token_secret_id_secrets_id_fk": { "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "access_token_secret_id" - ], + "columnsFrom": ["access_token_secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "ad_accounts_refresh_token_secret_id_secrets_id_fk": { "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "refresh_token_secret_id" - ], + "columnsFrom": ["refresh_token_secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20226,39 +19401,27 @@ "ad_campaigns_organization_id_organizations_id_fk": { "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_campaigns_ad_account_id_ad_accounts_id_fk": { "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "ad_account_id" - ], + "columnsFrom": ["ad_account_id"], "tableTo": "ad_accounts", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_campaigns_app_id_apps_id_fk": { "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20451,13 +19614,9 @@ "ad_creatives_campaign_id_ad_campaigns_id_fk": { "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", - "columnsFrom": [ - "campaign_id" - ], + "columnsFrom": ["campaign_id"], "tableTo": "ad_campaigns", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20651,39 +19810,27 @@ "ad_transactions_organization_id_organizations_id_fk": { "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_transactions_campaign_id_ad_campaigns_id_fk": { "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "campaign_id" - ], + "columnsFrom": ["campaign_id"], "tableTo": "ad_campaigns", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "ad_transactions_credit_transaction_id_credit_transactions_id_fk": { "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], + "columnsFrom": ["credit_transaction_id"], "tableTo": "credit_transactions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20775,13 +19922,9 @@ "seo_artifacts_request_id_seo_requests_id_fk": { "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", - "columnsFrom": [ - "request_id" - ], + "columnsFrom": ["request_id"], "tableTo": "seo_requests", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20933,13 +20076,9 @@ "seo_provider_calls_request_id_seo_requests_id_fk": { "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", - "columnsFrom": [ - "request_id" - ], + "columnsFrom": ["request_id"], "tableTo": "seo_requests", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21180,52 +20319,36 @@ "seo_requests_organization_id_organizations_id_fk": { "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "seo_requests_app_id_apps_id_fk": { "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "seo_requests_user_id_users_id_fk": { "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "seo_requests_api_key_id_api_keys_id_fk": { "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -21342,13 +20465,9 @@ "telegram_chats_organization_id_organizations_id_fk": { "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21492,13 +20611,9 @@ "discord_guilds_organization_id_organizations_id_fk": { "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21677,13 +20792,9 @@ "discord_channels_organization_id_organizations_id_fk": { "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21948,26 +21059,18 @@ "discord_connections_organization_id_organizations_id_fk": { "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "discord_connections_character_id_user_characters_id_fk": { "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -22231,13 +21334,9 @@ "agent_phone_numbers_organization_id_organizations_id_fk": { "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22462,13 +21561,9 @@ "phone_message_log_phone_number_id_agent_phone_numbers_id_fk": { "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", - "columnsFrom": [ - "phone_number_id" - ], + "columnsFrom": ["phone_number_id"], "tableTo": "agent_phone_numbers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22553,9 +21648,7 @@ "uniqueConstraints": { "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", - "columns": [ - "key" - ], + "columns": ["key"], "nullsNotDistinct": false } }, @@ -22721,13 +21814,9 @@ "entity_settings_user_id_users_id_fk": { "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22832,26 +21921,18 @@ "affiliate_codes_user_id_users_id_fk": { "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "affiliate_codes_parent_referral_id_affiliate_codes_id_fk": { "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], + "columnsFrom": ["parent_referral_id"], "tableTo": "affiliate_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -22860,9 +21941,7 @@ "uniqueConstraints": { "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "nullsNotDistinct": false } }, @@ -22942,26 +22021,18 @@ "user_affiliates_user_id_users_id_fk": { "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_affiliates_affiliate_code_id_affiliate_codes_id_fk": { "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", - "columnsFrom": [ - "affiliate_code_id" - ], + "columnsFrom": ["affiliate_code_id"], "tableTo": "affiliate_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -23136,39 +22207,27 @@ "agent_server_wallets_organization_id_organizations_id_fk": { "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_server_wallets_user_id_users_id_fk": { "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_server_wallets_character_id_user_characters_id_fk": { "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -23177,9 +22236,7 @@ "uniqueConstraints": { "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", - "columns": [ - "client_address" - ], + "columns": ["client_address"], "nullsNotDistinct": false } }, @@ -23272,13 +22329,9 @@ "milaidy_sandbox_backups_sandbox_record_id_milaidy_sandboxes_id_fk": { "name": "milaidy_sandbox_backups_sandbox_record_id_milaidy_sandboxes_id_fk", "tableFrom": "milaidy_sandbox_backups", - "columnsFrom": [ - "sandbox_record_id" - ], + "columnsFrom": ["sandbox_record_id"], "tableTo": "milaidy_sandboxes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -23520,39 +22573,27 @@ "milaidy_sandboxes_organization_id_organizations_id_fk": { "name": "milaidy_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milaidy_sandboxes", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "milaidy_sandboxes_user_id_users_id_fk": { "name": "milaidy_sandboxes_user_id_users_id_fk", "tableFrom": "milaidy_sandboxes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "milaidy_sandboxes_character_id_user_characters_id_fk": { "name": "milaidy_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milaidy_sandboxes", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -23831,72 +22872,37 @@ "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -23926,84 +22932,42 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -24028,11 +22992,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.reply_confirmation_status": { "name": "reply_confirmation_status", @@ -24062,49 +23022,27 @@ "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.domain_event_detected_by": { "name": "domain_event_detected_by", @@ -24120,13 +23058,7 @@ "public.domain_event_severity": { "name": "domain_event_severity", "schema": "public", - "values": [ - "info", - "low", - "medium", - "high", - "critical" - ] + "values": ["info", "low", "medium", "high", "critical"] }, "public.domain_event_type": { "name": "domain_event_type", @@ -24150,13 +23082,7 @@ "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -24205,13 +23131,7 @@ "public.org_checkin_frequency": { "name": "org_checkin_frequency", "schema": "public", - "values": [ - "daily", - "weekdays", - "weekly", - "bi_weekly", - "monthly" - ] + "values": ["daily", "weekdays", "weekly", "bi_weekly", "monthly"] }, "public.org_checkin_type": { "name": "org_checkin_type", @@ -24227,42 +23147,22 @@ "public.org_platform_status": { "name": "org_platform_status", "schema": "public", - "values": [ - "active", - "disconnected", - "error", - "pending" - ] + "values": ["active", "disconnected", "error", "pending"] }, "public.org_platform_type": { "name": "org_platform_type", "schema": "public", - "values": [ - "discord", - "telegram", - "slack", - "twitter" - ] + "values": ["discord", "telegram", "slack", "twitter"] }, "public.org_todo_priority": { "name": "org_todo_priority", "schema": "public", - "values": [ - "low", - "medium", - "high", - "urgent" - ] + "values": ["low", "medium", "high", "urgent"] }, "public.org_todo_status": { "name": "org_todo_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "cancelled" - ] + "values": ["pending", "in_progress", "completed", "cancelled"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -24279,32 +23179,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -24322,22 +23207,12 @@ "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "other" - ] + "values": ["twilio", "blooio", "vonage", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage" - ] + "values": ["sms", "voice", "both", "imessage"] } }, "schemas": { @@ -24352,4 +23227,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0043_snapshot.json b/packages/db/migrations/meta/0043_snapshot.json index 2d566af37..b29ad8e00 100644 --- a/packages/db/migrations/meta/0043_snapshot.json +++ b/packages/db/migrations/meta/0043_snapshot.json @@ -222,9 +222,7 @@ "uniqueConstraints": { "organizations_slug_unique": { "name": "organizations_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false } }, @@ -384,39 +382,27 @@ "organization_invites_organization_id_organizations_id_fk": { "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "organization_invites_inviter_user_id_users_id_fk": { "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "inviter_user_id" - ], + "columnsFrom": ["inviter_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "organization_invites_accepted_by_user_id_users_id_fk": { "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "accepted_by_user_id" - ], + "columnsFrom": ["accepted_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -425,9 +411,7 @@ "uniqueConstraints": { "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", - "columns": [ - "token_hash" - ], + "columns": ["token_hash"], "nullsNotDistinct": false } }, @@ -844,13 +828,9 @@ "users_organization_id_organizations_id_fk": { "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -859,51 +839,37 @@ "uniqueConstraints": { "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", - "columns": [ - "privy_user_id" - ], + "columns": ["privy_user_id"], "nullsNotDistinct": false }, "users_anonymous_session_id_unique": { "name": "users_anonymous_session_id_unique", - "columns": [ - "anonymous_session_id" - ], + "columns": ["anonymous_session_id"], "nullsNotDistinct": false }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", - "columns": [ - "telegram_id" - ], + "columns": ["telegram_id"], "nullsNotDistinct": false }, "users_phone_number_unique": { "name": "users_phone_number_unique", - "columns": [ - "phone_number" - ], + "columns": ["phone_number"], "nullsNotDistinct": false }, "users_discord_id_unique": { "name": "users_discord_id_unique", - "columns": [ - "discord_id" - ], + "columns": ["discord_id"], "nullsNotDistinct": false }, "users_email_unique": { "name": "users_email_unique", - "columns": [ - "email" - ], + "columns": ["email"], "nullsNotDistinct": false }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", - "columns": [ - "wallet_address" - ], + "columns": ["wallet_address"], "nullsNotDistinct": false } }, @@ -1096,26 +1062,18 @@ "user_sessions_user_id_users_id_fk": { "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_sessions_organization_id_organizations_id_fk": { "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1124,9 +1082,7 @@ "uniqueConstraints": { "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", - "columns": [ - "session_token" - ], + "columns": ["session_token"], "nullsNotDistinct": false } }, @@ -1336,13 +1292,9 @@ "anonymous_sessions_user_id_users_id_fk": { "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1351,9 +1303,7 @@ "uniqueConstraints": { "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", - "columns": [ - "session_token" - ], + "columns": ["session_token"], "nullsNotDistinct": false } }, @@ -1550,26 +1500,18 @@ "api_keys_organization_id_organizations_id_fk": { "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "api_keys_user_id_users_id_fk": { "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1578,16 +1520,12 @@ "uniqueConstraints": { "api_keys_key_unique": { "name": "api_keys_key_unique", - "columns": [ - "key" - ], + "columns": ["key"], "nullsNotDistinct": false }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", - "columns": [ - "key_hash" - ], + "columns": ["key_hash"], "nullsNotDistinct": false } }, @@ -1730,13 +1668,9 @@ "cli_auth_sessions_user_id_users_id_fk": { "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1745,9 +1679,7 @@ "uniqueConstraints": { "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nullsNotDistinct": false } }, @@ -2033,39 +1965,27 @@ "usage_records_organization_id_organizations_id_fk": { "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "usage_records_user_id_users_id_fk": { "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "usage_records_api_key_id_api_keys_id_fk": { "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2231,13 +2151,9 @@ "usage_quotas_organization_id_organizations_id_fk": { "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2391,26 +2307,18 @@ "credit_transactions_organization_id_organizations_id_fk": { "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "credit_transactions_user_id_users_id_fk": { "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2556,9 +2464,7 @@ "uniqueConstraints": { "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", - "columns": [ - "stripe_price_id" - ], + "columns": ["stripe_price_id"], "nullsNotDistinct": false } }, @@ -2742,9 +2648,7 @@ "uniqueConstraints": { "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", - "columns": [ - "stripe_invoice_id" - ], + "columns": ["stripe_invoice_id"], "nullsNotDistinct": false } }, @@ -3095,52 +2999,36 @@ "generations_organization_id_organizations_id_fk": { "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "generations_user_id_users_id_fk": { "name": "generations_user_id_users_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "generations_api_key_id_api_keys_id_fk": { "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "generations_usage_record_id_usage_records_id_fk": { "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "usage_record_id" - ], + "columnsFrom": ["usage_record_id"], "tableTo": "usage_records", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3349,52 +3237,36 @@ "jobs_organization_id_organizations_id_fk": { "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "jobs_user_id_users_id_fk": { "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" }, "jobs_api_key_id_api_keys_id_fk": { "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" }, "jobs_generation_id_generations_id_fk": { "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "generation_id" - ], + "columnsFrom": ["generation_id"], "tableTo": "generations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -3787,26 +3659,18 @@ "conversation_messages_conversation_id_conversations_id_fk": { "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", - "columnsFrom": [ - "conversation_id" - ], + "columnsFrom": ["conversation_id"], "tableTo": "conversations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "conversation_messages_usage_record_id_usage_records_id_fk": { "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", - "columnsFrom": [ - "usage_record_id" - ], + "columnsFrom": ["usage_record_id"], "tableTo": "usage_records", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3967,26 +3831,18 @@ "conversations_organization_id_organizations_id_fk": { "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "conversations_user_id_users_id_fk": { "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4495,26 +4351,18 @@ "user_characters_organization_id_organizations_id_fk": { "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_characters_user_id_users_id_fk": { "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4523,9 +4371,7 @@ "uniqueConstraints": { "user_characters_username_unique": { "name": "user_characters_username_unique", - "columns": [ - "username" - ], + "columns": ["username"], "nullsNotDistinct": false } }, @@ -4738,26 +4584,18 @@ "user_voices_organization_id_organizations_id_fk": { "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_voices_user_id_users_id_fk": { "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4766,9 +4604,7 @@ "uniqueConstraints": { "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", - "columns": [ - "elevenlabs_voice_id" - ], + "columns": ["elevenlabs_voice_id"], "nullsNotDistinct": false } }, @@ -4895,39 +4731,27 @@ "voice_cloning_jobs_organization_id_organizations_id_fk": { "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_cloning_jobs_user_id_users_id_fk": { "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_cloning_jobs_user_voice_id_user_voices_id_fk": { "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "user_voice_id" - ], + "columnsFrom": ["user_voice_id"], "tableTo": "user_voices", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -5047,52 +4871,36 @@ "voice_samples_user_voice_id_user_voices_id_fk": { "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "user_voice_id" - ], + "columnsFrom": ["user_voice_id"], "tableTo": "user_voices", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_job_id_voice_cloning_jobs_id_fk": { "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "job_id" - ], + "columnsFrom": ["job_id"], "tableTo": "voice_cloning_jobs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_organization_id_organizations_id_fk": { "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_user_id_users_id_fk": { "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5237,39 +5045,27 @@ "container_billing_records_container_id_containers_id_fk": { "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "container_billing_records_organization_id_organizations_id_fk": { "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "container_billing_records_credit_transaction_id_credit_transactions_id_fk": { "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "credit_transaction_id" - ], + "columnsFrom": ["credit_transaction_id"], "tableTo": "credit_transactions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -5717,52 +5513,36 @@ "containers_organization_id_organizations_id_fk": { "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "containers_user_id_users_id_fk": { "name": "containers_user_id_users_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "containers_api_key_id_api_keys_id_fk": { "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "containers_character_id_user_characters_id_fk": { "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -5845,9 +5625,7 @@ "uniqueConstraints": { "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", - "columns": [ - "priority" - ], + "columns": ["priority"], "nullsNotDistinct": false } }, @@ -6080,13 +5858,9 @@ "app_analytics_app_id_apps_id_fk": { "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6320,26 +6094,18 @@ "app_requests_app_id_apps_id_fk": { "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_requests_user_id_users_id_fk": { "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6505,26 +6271,18 @@ "app_users_app_id_apps_id_fk": { "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_users_user_id_users_id_fk": { "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6970,26 +6728,18 @@ "apps_organization_id_organizations_id_fk": { "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "apps_created_by_user_id_users_id_fk": { "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6998,23 +6748,17 @@ "uniqueConstraints": { "apps_slug_unique": { "name": "apps_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", - "columns": [ - "api_key_id" - ], + "columns": ["api_key_id"], "nullsNotDistinct": false }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", - "columns": [ - "affiliate_code" - ], + "columns": ["affiliate_code"], "nullsNotDistinct": false } }, @@ -7159,39 +6903,27 @@ "app_credit_balances_app_id_apps_id_fk": { "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_credit_balances_user_id_users_id_fk": { "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_credit_balances_organization_id_organizations_id_fk": { "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7310,13 +7042,9 @@ "app_earnings_app_id_apps_id_fk": { "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7455,26 +7183,18 @@ "app_earnings_transactions_app_id_apps_id_fk": { "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_earnings_transactions_user_id_users_id_fk": { "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7593,13 +7313,9 @@ "referral_codes_user_id_users_id_fk": { "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7608,9 +7324,7 @@ "uniqueConstraints": { "referral_codes_code_unique": { "name": "referral_codes_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "nullsNotDistinct": false } }, @@ -7759,65 +7473,45 @@ "referral_signups_referral_code_id_referral_codes_id_fk": { "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referral_code_id" - ], + "columnsFrom": ["referral_code_id"], "tableTo": "referral_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_referrer_user_id_users_id_fk": { "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referrer_user_id" - ], + "columnsFrom": ["referrer_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_referred_user_id_users_id_fk": { "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referred_user_id" - ], + "columnsFrom": ["referred_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_app_owner_id_users_id_fk": { "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "app_owner_id" - ], + "columnsFrom": ["app_owner_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "referral_signups_creator_id_users_id_fk": { "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "creator_id" - ], + "columnsFrom": ["creator_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7956,13 +7650,9 @@ "social_share_rewards_user_id_users_id_fk": { "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8143,13 +7833,9 @@ "cache_agent_id_agents_id_fk": { "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8157,10 +7843,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -8190,13 +7873,9 @@ "channel_participants_channel_id_channels_id_fk": { "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8204,10 +7883,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -8287,13 +7963,9 @@ "channels_message_server_id_message_servers_id_fk": { "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", - "columnsFrom": [ - "message_server_id" - ], + "columnsFrom": ["message_server_id"], "tableTo": "message_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8371,65 +8043,45 @@ "components_entity_id_entities_id_fk": { "name": "components_entity_id_entities_id_fk", "tableFrom": "components", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_agent_id_agents_id_fk": { "name": "components_agent_id_agents_id_fk", "tableFrom": "components", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_room_id_rooms_id_fk": { "name": "components_room_id_rooms_id_fk", "tableFrom": "components", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_world_id_worlds_id_fk": { "name": "components_world_id_worlds_id_fk", "tableFrom": "components", - "columnsFrom": [ - "world_id" - ], + "columnsFrom": ["world_id"], "tableTo": "worlds", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_source_entity_id_entities_id_fk": { "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8522,26 +8174,18 @@ "embeddings_memory_id_memories_id_fk": { "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", - "columnsFrom": [ - "memory_id" - ], + "columnsFrom": ["memory_id"], "tableTo": "memories", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_embedding_memory": { "name": "fk_embedding_memory", "tableFrom": "embeddings", - "columnsFrom": [ - "memory_id" - ], + "columnsFrom": ["memory_id"], "tableTo": "memories", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8600,13 +8244,9 @@ "entities_agent_id_agents_id_fk": { "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8615,10 +8255,7 @@ "uniqueConstraints": { "id_agent_id_unique": { "name": "id_agent_id_unique", - "columns": [ - "id", - "agent_id" - ], + "columns": ["id", "agent_id"], "nullsNotDistinct": false } }, @@ -8674,52 +8311,36 @@ "logs_entity_id_entities_id_fk": { "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "logs_room_id_rooms_id_fk": { "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "logs", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "logs", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9161,78 +8782,54 @@ "memories_entity_id_entities_id_fk": { "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memories_agent_id_agents_id_fk": { "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memories_room_id_rooms_id_fk": { "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "memories", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "memories", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_agent": { "name": "fk_agent", "tableFrom": "memories", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9387,26 +8984,18 @@ "central_messages_channel_id_channels_id_fk": { "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "central_messages_in_reply_to_root_message_id_central_messages_id_fk": { "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], "tableTo": "central_messages", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -9496,65 +9085,45 @@ "participants_entity_id_entities_id_fk": { "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "participants_room_id_rooms_id_fk": { "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "participants_agent_id_agents_id_fk": { "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "participants", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "participants", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9641,65 +9210,45 @@ "relationships_source_entity_id_entities_id_fk": { "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "relationships_target_entity_id_entities_id_fk": { "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "target_entity_id" - ], + "columnsFrom": ["target_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "relationships_agent_id_agents_id_fk": { "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user_a": { "name": "fk_user_a", "tableFrom": "relationships", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user_b": { "name": "fk_user_b", "tableFrom": "relationships", - "columnsFrom": [ - "target_entity_id" - ], + "columnsFrom": ["target_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9708,11 +9257,7 @@ "uniqueConstraints": { "unique_relationship": { "name": "unique_relationship", - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ], + "columns": ["source_entity_id", "target_entity_id", "agent_id"], "nullsNotDistinct": false } }, @@ -9792,13 +9337,9 @@ "rooms_agent_id_agents_id_fk": { "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10042,13 +9583,9 @@ "tasks_agent_id_agents_id_fk": { "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10107,13 +9644,9 @@ "worlds_agent_id_agents_id_fk": { "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10166,13 +9699,9 @@ "eliza_room_characters_character_id_user_characters_id_fk": { "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10354,26 +9883,18 @@ "agent_events_agent_id_user_characters_id_fk": { "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_events_organization_id_organizations_id_fk": { "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10563,39 +10084,27 @@ "mcp_usage_mcp_id_user_mcps_id_fk": { "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "mcp_id" - ], + "columnsFrom": ["mcp_id"], "tableTo": "user_mcps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "mcp_usage_organization_id_organizations_id_fk": { "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "mcp_usage_user_id_users_id_fk": { "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -11077,52 +10586,36 @@ "user_mcps_organization_id_organizations_id_fk": { "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_mcps_created_by_user_id_users_id_fk": { "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_mcps_container_id_containers_id_fk": { "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "user_mcps_verified_by_users_id_fk": { "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "verified_by" - ], + "columnsFrom": ["verified_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -11307,13 +10800,9 @@ "redemption_limits_user_id_users_id_fk": { "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11612,39 +11101,27 @@ "token_redemptions_user_id_users_id_fk": { "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "token_redemptions_app_id_apps_id_fk": { "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "token_redemptions_reviewed_by_users_id_fk": { "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "reviewed_by" - ], + "columnsFrom": ["reviewed_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -11797,13 +11274,9 @@ "redeemable_earnings_user_id_users_id_fk": { "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11812,9 +11285,7 @@ "uniqueConstraints": { "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -12000,13 +11471,9 @@ "redeemable_earnings_ledger_user_id_users_id_fk": { "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -12091,9 +11558,7 @@ "uniqueConstraints": { "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", - "columns": [ - "ledger_entry_id" - ], + "columns": ["ledger_entry_id"], "nullsNotDistinct": false } }, @@ -12244,26 +11709,18 @@ "admin_users_user_id_users_id_fk": { "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "admin_users_granted_by_users_id_fk": { "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", - "columnsFrom": [ - "granted_by" - ], + "columnsFrom": ["granted_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12272,9 +11729,7 @@ "uniqueConstraints": { "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", - "columns": [ - "wallet_address" - ], + "columns": ["wallet_address"], "nullsNotDistinct": false } }, @@ -12422,26 +11877,18 @@ "moderation_violations_user_id_users_id_fk": { "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "moderation_violations_reviewed_by_users_id_fk": { "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", - "columnsFrom": [ - "reviewed_by" - ], + "columnsFrom": ["reviewed_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12609,26 +12056,18 @@ "user_moderation_status_user_id_users_id_fk": { "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_moderation_status_banned_by_users_id_fk": { "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", - "columnsFrom": [ - "banned_by" - ], + "columnsFrom": ["banned_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12637,9 +12076,7 @@ "uniqueConstraints": { "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -12811,26 +12248,18 @@ "agent_budget_transactions_budget_id_agent_budgets_id_fk": { "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", - "columnsFrom": [ - "budget_id" - ], + "columnsFrom": ["budget_id"], "tableTo": "agent_budgets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_budget_transactions_agent_id_user_characters_id_fk": { "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13035,26 +12464,18 @@ "agent_budgets_agent_id_user_characters_id_fk": { "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_budgets_owner_org_id_organizations_id_fk": { "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", - "columnsFrom": [ - "owner_org_id" - ], + "columnsFrom": ["owner_org_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13063,9 +12484,7 @@ "uniqueConstraints": { "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", - "columns": [ - "agent_id" - ], + "columns": ["agent_id"], "nullsNotDistinct": false } }, @@ -13331,26 +12750,18 @@ "crypto_payments_organization_id_organizations_id_fk": { "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "crypto_payments_user_id_users_id_fk": { "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13472,13 +12883,9 @@ "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13751,39 +13158,27 @@ "app_sandbox_sessions_user_id_users_id_fk": { "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_sandbox_sessions_organization_id_organizations_id_fk": { "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_sandbox_sessions_app_id_apps_id_fk": { "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13792,9 +13187,7 @@ "uniqueConstraints": { "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", - "columns": [ - "sandbox_id" - ], + "columns": ["sandbox_id"], "nullsNotDistinct": false } }, @@ -13979,9 +13372,7 @@ "uniqueConstraints": { "app_templates_slug_unique": { "name": "app_templates_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false } }, @@ -14143,9 +13534,7 @@ "uniqueConstraints": { "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", - "columns": [ - "snapshot_id" - ], + "columns": ["snapshot_id"], "nullsNotDistinct": false } }, @@ -14267,13 +13656,9 @@ "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14374,13 +13759,9 @@ "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14519,9 +13900,7 @@ "uniqueConstraints": { "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", - "columns": [ - "event_id" - ], + "columns": ["event_id"], "nullsNotDistinct": false } }, @@ -14650,26 +14029,18 @@ "app_secret_requirements_app_id_apps_id_fk": { "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_secret_requirements_approved_by_users_id_fk": { "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", - "columnsFrom": [ - "approved_by" - ], + "columnsFrom": ["approved_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -14965,26 +14336,18 @@ "oauth_sessions_organization_id_organizations_id_fk": { "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "oauth_sessions_user_id_users_id_fk": { "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -15367,39 +14730,27 @@ "secret_bindings_organization_id_organizations_id_fk": { "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secret_bindings_secret_id_secrets_id_fk": { "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "secret_id" - ], + "columnsFrom": ["secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secret_bindings_created_by_users_id_fk": { "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -15706,26 +15057,18 @@ "secrets_organization_id_organizations_id_fk": { "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secrets_created_by_users_id_fk": { "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -15898,13 +15241,9 @@ "app_domains_app_id_apps_id_fk": { "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -16156,26 +15495,18 @@ "org_feed_configs_organization_id_organizations_id_fk": { "name": "org_feed_configs_organization_id_organizations_id_fk", "tableFrom": "org_feed_configs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_feed_configs_created_by_users_id_fk": { "name": "org_feed_configs_created_by_users_id_fk", "tableFrom": "org_feed_configs", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -16456,26 +15787,18 @@ "pending_reply_confirmations_organization_id_organizations_id_fk": { "name": "pending_reply_confirmations_organization_id_organizations_id_fk", "tableFrom": "pending_reply_confirmations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "pending_reply_confirmations_engagement_event_id_social_engagement_events_id_fk": { "name": "pending_reply_confirmations_engagement_event_id_social_engagement_events_id_fk", "tableFrom": "pending_reply_confirmations", - "columnsFrom": [ - "engagement_event_id" - ], + "columnsFrom": ["engagement_event_id"], "tableTo": "social_engagement_events", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -16756,26 +16079,18 @@ "social_engagement_events_organization_id_organizations_id_fk": { "name": "social_engagement_events_organization_id_organizations_id_fk", "tableFrom": "social_engagement_events", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "social_engagement_events_feed_config_id_org_feed_configs_id_fk": { "name": "social_engagement_events_feed_config_id_org_feed_configs_id_fk", "tableFrom": "social_engagement_events", - "columnsFrom": [ - "feed_config_id" - ], + "columnsFrom": ["feed_config_id"], "tableTo": "org_feed_configs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -16943,26 +16258,18 @@ "social_notification_messages_organization_id_organizations_id_fk": { "name": "social_notification_messages_organization_id_organizations_id_fk", "tableFrom": "social_notification_messages", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "social_notification_messages_engagement_event_id_social_engagement_events_id_fk": { "name": "social_notification_messages_engagement_event_id_social_engagement_events_id_fk", "tableFrom": "social_notification_messages", - "columnsFrom": [ - "engagement_event_id" - ], + "columnsFrom": ["engagement_event_id"], "tableTo": "social_engagement_events", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -17425,65 +16732,45 @@ "managed_domains_organization_id_organizations_id_fk": { "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "managed_domains_app_id_apps_id_fk": { "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_container_id_containers_id_fk": { "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_agent_id_user_characters_id_fk": { "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_mcp_id_user_mcps_id_fk": { "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "mcp_id" - ], + "columnsFrom": ["mcp_id"], "tableTo": "user_mcps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17492,9 +16779,7 @@ "uniqueConstraints": { "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", - "columns": [ - "domain" - ], + "columns": ["domain"], "nullsNotDistinct": false } }, @@ -17683,39 +16968,27 @@ "domain_moderation_events_domain_id_managed_domains_id_fk": { "name": "domain_moderation_events_domain_id_managed_domains_id_fk", "tableFrom": "domain_moderation_events", - "columnsFrom": [ - "domain_id" - ], + "columnsFrom": ["domain_id"], "tableTo": "managed_domains", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "domain_moderation_events_admin_user_id_users_id_fk": { "name": "domain_moderation_events_admin_user_id_users_id_fk", "tableFrom": "domain_moderation_events", - "columnsFrom": [ - "admin_user_id" - ], + "columnsFrom": ["admin_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "domain_moderation_events_resolved_by_users_id_fk": { "name": "domain_moderation_events_resolved_by_users_id_fk", "tableFrom": "domain_moderation_events", - "columnsFrom": [ - "resolved_by" - ], + "columnsFrom": ["resolved_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17925,52 +17198,36 @@ "platform_credential_sessions_organization_id_organizations_id_fk": { "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credential_sessions_app_id_apps_id_fk": { "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credential_sessions_requesting_user_id_users_id_fk": { "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "requesting_user_id" - ], + "columnsFrom": ["requesting_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "platform_credential_sessions_credential_id_platform_credentials_id_fk": { "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "credential_id" - ], + "columnsFrom": ["credential_id"], "tableTo": "platform_credentials", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17979,9 +17236,7 @@ "uniqueConstraints": { "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nullsNotDistinct": false } }, @@ -18291,39 +17546,27 @@ "platform_credentials_organization_id_organizations_id_fk": { "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credentials_user_id_users_id_fk": { "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credentials_app_id_apps_id_fk": { "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -18506,26 +17749,18 @@ "org_checkin_responses_schedule_id_org_checkin_schedules_id_fk": { "name": "org_checkin_responses_schedule_id_org_checkin_schedules_id_fk", "tableFrom": "org_checkin_responses", - "columnsFrom": [ - "schedule_id" - ], + "columnsFrom": ["schedule_id"], "tableTo": "org_checkin_schedules", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_checkin_responses_organization_id_organizations_id_fk": { "name": "org_checkin_responses_organization_id_organizations_id_fk", "tableFrom": "org_checkin_responses", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -18720,39 +17955,27 @@ "org_checkin_schedules_organization_id_organizations_id_fk": { "name": "org_checkin_schedules_organization_id_organizations_id_fk", "tableFrom": "org_checkin_schedules", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_checkin_schedules_server_id_org_platform_servers_id_fk": { "name": "org_checkin_schedules_server_id_org_platform_servers_id_fk", "tableFrom": "org_checkin_schedules", - "columnsFrom": [ - "server_id" - ], + "columnsFrom": ["server_id"], "tableTo": "org_platform_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_checkin_schedules_created_by_users_id_fk": { "name": "org_checkin_schedules_created_by_users_id_fk", "tableFrom": "org_checkin_schedules", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -18967,26 +18190,18 @@ "org_platform_connections_organization_id_organizations_id_fk": { "name": "org_platform_connections_organization_id_organizations_id_fk", "tableFrom": "org_platform_connections", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_platform_connections_connected_by_users_id_fk": { "name": "org_platform_connections_connected_by_users_id_fk", "tableFrom": "org_platform_connections", - "columnsFrom": [ - "connected_by" - ], + "columnsFrom": ["connected_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -19178,26 +18393,18 @@ "org_platform_servers_connection_id_org_platform_connections_id_fk": { "name": "org_platform_servers_connection_id_org_platform_connections_id_fk", "tableFrom": "org_platform_servers", - "columnsFrom": [ - "connection_id" - ], + "columnsFrom": ["connection_id"], "tableTo": "org_platform_connections", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_platform_servers_organization_id_organizations_id_fk": { "name": "org_platform_servers_organization_id_organizations_id_fk", "tableFrom": "org_platform_servers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19413,26 +18620,18 @@ "org_team_members_organization_id_organizations_id_fk": { "name": "org_team_members_organization_id_organizations_id_fk", "tableFrom": "org_team_members", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_team_members_server_id_org_platform_servers_id_fk": { "name": "org_team_members_server_id_org_platform_servers_id_fk", "tableFrom": "org_team_members", - "columnsFrom": [ - "server_id" - ], + "columnsFrom": ["server_id"], "tableTo": "org_platform_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19670,26 +18869,18 @@ "org_todos_organization_id_organizations_id_fk": { "name": "org_todos_organization_id_organizations_id_fk", "tableFrom": "org_todos", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "org_todos_created_by_user_id_users_id_fk": { "name": "org_todos_created_by_user_id_users_id_fk", "tableFrom": "org_todos", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -19875,52 +19066,36 @@ "ad_accounts_organization_id_organizations_id_fk": { "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_accounts_connected_by_user_id_users_id_fk": { "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "connected_by_user_id" - ], + "columnsFrom": ["connected_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_accounts_access_token_secret_id_secrets_id_fk": { "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "access_token_secret_id" - ], + "columnsFrom": ["access_token_secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "ad_accounts_refresh_token_secret_id_secrets_id_fk": { "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "refresh_token_secret_id" - ], + "columnsFrom": ["refresh_token_secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20226,39 +19401,27 @@ "ad_campaigns_organization_id_organizations_id_fk": { "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_campaigns_ad_account_id_ad_accounts_id_fk": { "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "ad_account_id" - ], + "columnsFrom": ["ad_account_id"], "tableTo": "ad_accounts", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_campaigns_app_id_apps_id_fk": { "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20451,13 +19614,9 @@ "ad_creatives_campaign_id_ad_campaigns_id_fk": { "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", - "columnsFrom": [ - "campaign_id" - ], + "columnsFrom": ["campaign_id"], "tableTo": "ad_campaigns", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20651,39 +19810,27 @@ "ad_transactions_organization_id_organizations_id_fk": { "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_transactions_campaign_id_ad_campaigns_id_fk": { "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "campaign_id" - ], + "columnsFrom": ["campaign_id"], "tableTo": "ad_campaigns", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "ad_transactions_credit_transaction_id_credit_transactions_id_fk": { "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], + "columnsFrom": ["credit_transaction_id"], "tableTo": "credit_transactions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20775,13 +19922,9 @@ "seo_artifacts_request_id_seo_requests_id_fk": { "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", - "columnsFrom": [ - "request_id" - ], + "columnsFrom": ["request_id"], "tableTo": "seo_requests", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20933,13 +20076,9 @@ "seo_provider_calls_request_id_seo_requests_id_fk": { "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", - "columnsFrom": [ - "request_id" - ], + "columnsFrom": ["request_id"], "tableTo": "seo_requests", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21180,52 +20319,36 @@ "seo_requests_organization_id_organizations_id_fk": { "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "seo_requests_app_id_apps_id_fk": { "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "seo_requests_user_id_users_id_fk": { "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "seo_requests_api_key_id_api_keys_id_fk": { "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -21342,13 +20465,9 @@ "telegram_chats_organization_id_organizations_id_fk": { "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21492,13 +20611,9 @@ "discord_guilds_organization_id_organizations_id_fk": { "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21677,13 +20792,9 @@ "discord_channels_organization_id_organizations_id_fk": { "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21948,26 +21059,18 @@ "discord_connections_organization_id_organizations_id_fk": { "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "discord_connections_character_id_user_characters_id_fk": { "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -22231,13 +21334,9 @@ "agent_phone_numbers_organization_id_organizations_id_fk": { "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22462,13 +21561,9 @@ "phone_message_log_phone_number_id_agent_phone_numbers_id_fk": { "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", - "columnsFrom": [ - "phone_number_id" - ], + "columnsFrom": ["phone_number_id"], "tableTo": "agent_phone_numbers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22553,9 +21648,7 @@ "uniqueConstraints": { "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", - "columns": [ - "key" - ], + "columns": ["key"], "nullsNotDistinct": false } }, @@ -22721,13 +21814,9 @@ "entity_settings_user_id_users_id_fk": { "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22832,26 +21921,18 @@ "affiliate_codes_user_id_users_id_fk": { "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "affiliate_codes_parent_referral_id_affiliate_codes_id_fk": { "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], + "columnsFrom": ["parent_referral_id"], "tableTo": "affiliate_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -22860,9 +21941,7 @@ "uniqueConstraints": { "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "nullsNotDistinct": false } }, @@ -22942,26 +22021,18 @@ "user_affiliates_user_id_users_id_fk": { "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_affiliates_affiliate_code_id_affiliate_codes_id_fk": { "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", - "columnsFrom": [ - "affiliate_code_id" - ], + "columnsFrom": ["affiliate_code_id"], "tableTo": "affiliate_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -23136,39 +22207,27 @@ "agent_server_wallets_organization_id_organizations_id_fk": { "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_server_wallets_user_id_users_id_fk": { "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_server_wallets_character_id_user_characters_id_fk": { "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -23177,9 +22236,7 @@ "uniqueConstraints": { "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", - "columns": [ - "client_address" - ], + "columns": ["client_address"], "nullsNotDistinct": false } }, @@ -23272,13 +22329,9 @@ "milaidy_sandbox_backups_sandbox_record_id_milaidy_sandboxes_id_fk": { "name": "milaidy_sandbox_backups_sandbox_record_id_milaidy_sandboxes_id_fk", "tableFrom": "milaidy_sandbox_backups", - "columnsFrom": [ - "sandbox_record_id" - ], + "columnsFrom": ["sandbox_record_id"], "tableTo": "milaidy_sandboxes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -23520,39 +22573,27 @@ "milaidy_sandboxes_organization_id_organizations_id_fk": { "name": "milaidy_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milaidy_sandboxes", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "milaidy_sandboxes_user_id_users_id_fk": { "name": "milaidy_sandboxes_user_id_users_id_fk", "tableFrom": "milaidy_sandboxes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "milaidy_sandboxes_character_id_user_characters_id_fk": { "name": "milaidy_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milaidy_sandboxes", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -23831,72 +22872,37 @@ "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -23926,84 +22932,42 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -24028,11 +22992,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.reply_confirmation_status": { "name": "reply_confirmation_status", @@ -24062,49 +23022,27 @@ "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.domain_event_detected_by": { "name": "domain_event_detected_by", @@ -24120,13 +23058,7 @@ "public.domain_event_severity": { "name": "domain_event_severity", "schema": "public", - "values": [ - "info", - "low", - "medium", - "high", - "critical" - ] + "values": ["info", "low", "medium", "high", "critical"] }, "public.domain_event_type": { "name": "domain_event_type", @@ -24150,13 +23082,7 @@ "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -24205,13 +23131,7 @@ "public.org_checkin_frequency": { "name": "org_checkin_frequency", "schema": "public", - "values": [ - "daily", - "weekdays", - "weekly", - "bi_weekly", - "monthly" - ] + "values": ["daily", "weekdays", "weekly", "bi_weekly", "monthly"] }, "public.org_checkin_type": { "name": "org_checkin_type", @@ -24227,42 +23147,22 @@ "public.org_platform_status": { "name": "org_platform_status", "schema": "public", - "values": [ - "active", - "disconnected", - "error", - "pending" - ] + "values": ["active", "disconnected", "error", "pending"] }, "public.org_platform_type": { "name": "org_platform_type", "schema": "public", - "values": [ - "discord", - "telegram", - "slack", - "twitter" - ] + "values": ["discord", "telegram", "slack", "twitter"] }, "public.org_todo_priority": { "name": "org_todo_priority", "schema": "public", - "values": [ - "low", - "medium", - "high", - "urgent" - ] + "values": ["low", "medium", "high", "urgent"] }, "public.org_todo_status": { "name": "org_todo_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "cancelled" - ] + "values": ["pending", "in_progress", "completed", "cancelled"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -24279,32 +23179,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -24322,22 +23207,12 @@ "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "other" - ] + "values": ["twilio", "blooio", "vonage", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage" - ] + "values": ["sms", "voice", "both", "imessage"] } }, "schemas": { @@ -24352,4 +23227,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0048_snapshot.json b/packages/db/migrations/meta/0048_snapshot.json index 4cea564ca..cd91c4a3a 100644 --- a/packages/db/migrations/meta/0048_snapshot.json +++ b/packages/db/migrations/meta/0048_snapshot.json @@ -129,9 +129,7 @@ "organizations_slug_unique": { "name": "organizations_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -295,12 +293,8 @@ "name": "organization_billing_organization_id_organizations_id_fk", "tableFrom": "organization_billing", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -310,9 +304,7 @@ "organization_billing_organization_id_unique": { "name": "organization_billing_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -419,12 +411,8 @@ "name": "organization_config_organization_id_organizations_id_fk", "tableFrom": "organization_config", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -434,9 +422,7 @@ "organization_config_organization_id_unique": { "name": "organization_config_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -591,12 +577,8 @@ "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -604,12 +586,8 @@ "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "inviter_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -617,12 +595,8 @@ "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -632,9 +606,7 @@ "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -714,12 +686,8 @@ "name": "organization_encryption_keys_organization_id_organizations_id_fk", "tableFrom": "organization_encryption_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -729,9 +697,7 @@ "organization_encryption_keys_org_unique": { "name": "organization_encryption_keys_org_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -1115,12 +1081,8 @@ "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1130,51 +1092,37 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] }, "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "users_discord_id_unique": { "name": "users_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "users_whatsapp_id_unique": { "name": "users_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] }, "users_phone_number_unique": { "name": "users_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] } }, "policies": {}, @@ -1453,12 +1401,8 @@ "name": "user_identities_user_id_users_id_fk", "tableFrom": "user_identities", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1468,51 +1412,37 @@ "user_identities_user_id_unique": { "name": "user_identities_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] }, "user_identities_privy_user_id_unique": { "name": "user_identities_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "user_identities_anonymous_session_id_unique": { "name": "user_identities_anonymous_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "anonymous_session_id" - ] + "columns": ["anonymous_session_id"] }, "user_identities_telegram_id_unique": { "name": "user_identities_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "user_identities_phone_number_unique": { "name": "user_identities_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] }, "user_identities_discord_id_unique": { "name": "user_identities_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "user_identities_whatsapp_id_unique": { "name": "user_identities_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] } }, "policies": {}, @@ -1620,12 +1550,8 @@ "name": "user_preferences_user_id_users_id_fk", "tableFrom": "user_preferences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1635,9 +1561,7 @@ "user_preferences_user_id_unique": { "name": "user_preferences_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -1830,12 +1754,8 @@ "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1843,12 +1763,8 @@ "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1858,9 +1774,7 @@ "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -2070,12 +1984,8 @@ "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2085,9 +1995,7 @@ "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -2284,12 +2192,8 @@ "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2297,12 +2201,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2312,16 +2212,12 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", "nullsNotDistinct": false, - "columns": [ - "key_hash" - ] + "columns": ["key_hash"] } }, "policies": {}, @@ -2464,12 +2360,8 @@ "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2479,9 +2371,7 @@ "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -2767,12 +2657,8 @@ "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2780,12 +2666,8 @@ "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2793,12 +2675,8 @@ "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2965,12 +2843,8 @@ "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3125,12 +2999,8 @@ "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3138,12 +3008,8 @@ "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3290,9 +3156,7 @@ "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_price_id" - ] + "columns": ["stripe_price_id"] } }, "policies": {}, @@ -3476,9 +3340,7 @@ "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_invoice_id" - ] + "columns": ["stripe_invoice_id"] } }, "policies": {}, @@ -3829,12 +3691,8 @@ "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3842,12 +3700,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3855,12 +3709,8 @@ "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3868,12 +3718,8 @@ "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4083,12 +3929,8 @@ "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4096,12 +3938,8 @@ "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -4109,12 +3947,8 @@ "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -4122,12 +3956,8 @@ "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", "tableTo": "generations", - "columnsFrom": [ - "generation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4494,12 +4324,8 @@ "name": "service_pricing_audit_service_pricing_id_service_pricing_id_fk", "tableFrom": "service_pricing_audit", "tableTo": "service_pricing", - "columnsFrom": [ - "service_pricing_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_pricing_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4759,12 +4585,8 @@ "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4772,12 +4594,8 @@ "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4939,12 +4757,8 @@ "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4952,12 +4766,8 @@ "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5506,12 +5316,8 @@ "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5519,12 +5325,8 @@ "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5534,9 +5336,7 @@ "user_characters_username_unique": { "name": "user_characters_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -5749,12 +5549,8 @@ "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5762,12 +5558,8 @@ "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5777,9 +5569,7 @@ "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", "nullsNotDistinct": false, - "columns": [ - "elevenlabs_voice_id" - ] + "columns": ["elevenlabs_voice_id"] } }, "policies": {}, @@ -5906,12 +5696,8 @@ "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5919,12 +5705,8 @@ "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5932,12 +5714,8 @@ "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6058,12 +5836,8 @@ "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6071,12 +5845,8 @@ "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", "tableTo": "voice_cloning_jobs", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6084,12 +5854,8 @@ "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6097,12 +5863,8 @@ "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6248,12 +6010,8 @@ "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6261,12 +6019,8 @@ "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6274,12 +6028,8 @@ "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6728,12 +6478,8 @@ "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6741,12 +6487,8 @@ "name": "containers_user_id_users_id_fk", "tableFrom": "containers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6754,12 +6496,8 @@ "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -6767,12 +6505,8 @@ "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6856,9 +6590,7 @@ "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", "nullsNotDistinct": false, - "columns": [ - "priority" - ] + "columns": ["priority"] } }, "policies": {}, @@ -7091,12 +6823,8 @@ "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7331,12 +7059,8 @@ "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7344,12 +7068,8 @@ "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7516,12 +7236,8 @@ "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7529,12 +7245,8 @@ "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7958,12 +7670,8 @@ "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7971,12 +7679,8 @@ "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7986,23 +7690,17 @@ "apps_slug_unique": { "name": "apps_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", "nullsNotDistinct": false, - "columns": [ - "api_key_id" - ] + "columns": ["api_key_id"] }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", "nullsNotDistinct": false, - "columns": [ - "affiliate_code" - ] + "columns": ["affiliate_code"] } }, "policies": {}, @@ -8111,12 +7809,8 @@ "name": "app_config_app_id_apps_id_fk", "tableFrom": "app_config", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8126,9 +7820,7 @@ "app_config_app_id_unique": { "name": "app_config_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -8252,12 +7944,8 @@ "name": "app_billing_app_id_apps_id_fk", "tableFrom": "app_billing", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8267,9 +7955,7 @@ "app_billing_app_id_unique": { "name": "app_billing_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -8384,12 +8070,8 @@ "name": "app_databases_app_id_apps_id_fk", "tableFrom": "app_databases", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8399,9 +8081,7 @@ "app_databases_app_id_unique": { "name": "app_databases_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -8546,12 +8226,8 @@ "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8559,12 +8235,8 @@ "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8572,12 +8244,8 @@ "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8697,12 +8365,8 @@ "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8842,12 +8506,8 @@ "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8855,12 +8515,8 @@ "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8980,12 +8636,8 @@ "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8995,9 +8647,7 @@ "referral_codes_code_unique": { "name": "referral_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -9146,12 +8796,8 @@ "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9159,12 +8805,8 @@ "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referrer_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referrer_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9172,12 +8814,8 @@ "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referred_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referred_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9185,12 +8823,8 @@ "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "app_owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_owner_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -9198,12 +8832,8 @@ "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9343,12 +8973,8 @@ "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9530,12 +9156,8 @@ "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9543,10 +9165,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -9577,12 +9196,8 @@ "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9590,10 +9205,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -9674,12 +9286,8 @@ "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", "tableTo": "message_servers", - "columnsFrom": [ - "message_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9758,12 +9366,8 @@ "name": "components_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9771,12 +9375,8 @@ "name": "components_agent_id_agents_id_fk", "tableFrom": "components", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9784,12 +9384,8 @@ "name": "components_room_id_rooms_id_fk", "tableFrom": "components", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9797,12 +9393,8 @@ "name": "components_world_id_worlds_id_fk", "tableFrom": "components", "tableTo": "worlds", - "columnsFrom": [ - "world_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["world_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9810,12 +9402,8 @@ "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9909,12 +9497,8 @@ "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9922,12 +9506,8 @@ "name": "fk_embedding_memory", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9987,12 +9567,8 @@ "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10002,10 +9578,7 @@ "id_agent_id_unique": { "name": "id_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "id", - "agent_id" - ] + "columns": ["id", "agent_id"] } }, "policies": {}, @@ -10061,12 +9634,8 @@ "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10074,12 +9643,8 @@ "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10087,12 +9652,8 @@ "name": "fk_room", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10100,12 +9661,8 @@ "name": "fk_user", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10548,12 +10105,8 @@ "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10561,12 +10114,8 @@ "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10574,12 +10123,8 @@ "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10587,12 +10132,8 @@ "name": "fk_room", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10600,12 +10141,8 @@ "name": "fk_user", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10613,12 +10150,8 @@ "name": "fk_agent", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10774,12 +10307,8 @@ "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10787,12 +10316,8 @@ "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", "tableTo": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10883,12 +10408,8 @@ "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10896,12 +10417,8 @@ "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10909,12 +10426,8 @@ "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10922,12 +10435,8 @@ "name": "fk_room", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10935,12 +10444,8 @@ "name": "fk_user", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11028,12 +10533,8 @@ "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11041,12 +10542,8 @@ "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11054,12 +10551,8 @@ "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11067,12 +10560,8 @@ "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11080,12 +10569,8 @@ "name": "fk_user_b", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11095,11 +10580,7 @@ "unique_relationship": { "name": "unique_relationship", "nullsNotDistinct": false, - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ] + "columns": ["source_entity_id", "target_entity_id", "agent_id"] } }, "policies": {}, @@ -11179,12 +10660,8 @@ "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11429,12 +10906,8 @@ "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11494,12 +10967,8 @@ "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11553,12 +11022,8 @@ "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11741,12 +11206,8 @@ "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11754,12 +11215,8 @@ "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11950,12 +11407,8 @@ "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11963,12 +11416,8 @@ "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11976,12 +11425,8 @@ "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12464,12 +11909,8 @@ "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12477,12 +11918,8 @@ "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12490,12 +11927,8 @@ "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12503,12 +11936,8 @@ "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "verified_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["verified_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12694,12 +12123,8 @@ "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12999,12 +12424,8 @@ "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13012,12 +12433,8 @@ "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13025,12 +12442,8 @@ "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -13184,12 +12597,8 @@ "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13199,9 +12608,7 @@ "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -13387,12 +12794,8 @@ "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13478,9 +12881,7 @@ "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", "nullsNotDistinct": false, - "columns": [ - "ledger_entry_id" - ] + "columns": ["ledger_entry_id"] } }, "policies": {}, @@ -13631,12 +13032,8 @@ "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13644,12 +13041,8 @@ "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "granted_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -13659,9 +13052,7 @@ "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -13809,12 +13200,8 @@ "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13822,12 +13209,8 @@ "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -13996,12 +13379,8 @@ "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14009,12 +13388,8 @@ "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "banned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14024,9 +13399,7 @@ "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -14198,12 +13571,8 @@ "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "agent_budgets", - "columnsFrom": [ - "budget_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["budget_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14211,12 +13580,8 @@ "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14422,12 +13787,8 @@ "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14435,12 +13796,8 @@ "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", "tableTo": "organizations", - "columnsFrom": [ - "owner_org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_org_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14450,9 +13807,7 @@ "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "agent_id" - ] + "columns": ["agent_id"] } }, "policies": {}, @@ -14718,12 +14073,8 @@ "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14731,12 +14082,8 @@ "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14859,12 +14206,8 @@ "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15138,12 +14481,8 @@ "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15151,12 +14490,8 @@ "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15164,12 +14499,8 @@ "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15179,9 +14510,7 @@ "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", "nullsNotDistinct": false, - "columns": [ - "sandbox_id" - ] + "columns": ["sandbox_id"] } }, "policies": {}, @@ -15366,9 +14695,7 @@ "app_templates_slug_unique": { "name": "app_templates_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -15530,9 +14857,7 @@ "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", "nullsNotDistinct": false, - "columns": [ - "snapshot_id" - ] + "columns": ["snapshot_id"] } }, "policies": {}, @@ -15654,12 +14979,8 @@ "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15761,12 +15082,8 @@ "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15906,9 +15223,7 @@ "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", "nullsNotDistinct": false, - "columns": [ - "event_id" - ] + "columns": ["event_id"] } }, "policies": {}, @@ -16037,12 +15352,8 @@ "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16050,12 +15361,8 @@ "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "users", - "columnsFrom": [ - "approved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -16352,12 +15659,8 @@ "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16365,12 +15668,8 @@ "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16754,12 +16053,8 @@ "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16767,12 +16062,8 @@ "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", "tableTo": "secrets", - "columnsFrom": [ - "secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["secret_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16780,12 +16071,8 @@ "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -17093,12 +16380,8 @@ "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17106,12 +16389,8 @@ "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -17285,12 +16564,8 @@ "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17754,12 +17029,8 @@ "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17767,12 +17038,8 @@ "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17780,12 +17047,8 @@ "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17793,12 +17056,8 @@ "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17806,12 +17065,8 @@ "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17821,9 +17076,7 @@ "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", "nullsNotDistinct": false, - "columns": [ - "domain" - ] + "columns": ["domain"] } }, "policies": {}, @@ -18030,12 +17283,8 @@ "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18043,12 +17292,8 @@ "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18056,12 +17301,8 @@ "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "users", - "columnsFrom": [ - "requesting_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requesting_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18069,12 +17310,8 @@ "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "platform_credentials", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -18084,9 +17321,7 @@ "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -18396,12 +17631,8 @@ "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18409,12 +17640,8 @@ "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18422,12 +17649,8 @@ "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18614,12 +17837,8 @@ "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18627,12 +17846,8 @@ "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", "tableTo": "users", - "columnsFrom": [ - "connected_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18640,12 +17855,8 @@ "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "access_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["access_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18653,12 +17864,8 @@ "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "refresh_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refresh_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -18965,12 +18172,8 @@ "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18978,12 +18181,8 @@ "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", "tableTo": "ad_accounts", - "columnsFrom": [ - "ad_account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ad_account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18991,12 +18190,8 @@ "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19190,12 +18385,8 @@ "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19390,12 +18581,8 @@ "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19403,12 +18590,8 @@ "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19416,12 +18599,8 @@ "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19514,12 +18693,8 @@ "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19672,12 +18847,8 @@ "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19919,12 +19090,8 @@ "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19932,12 +19099,8 @@ "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19945,12 +19108,8 @@ "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19958,12 +19117,8 @@ "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20081,12 +19236,8 @@ "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20231,12 +19382,8 @@ "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20416,12 +19563,8 @@ "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20687,12 +19830,8 @@ "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20700,12 +19839,8 @@ "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20970,12 +20105,8 @@ "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21201,12 +20332,8 @@ "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", "tableTo": "agent_phone_numbers", - "columnsFrom": [ - "phone_number_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["phone_number_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21292,9 +20419,7 @@ "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -21460,12 +20585,8 @@ "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21571,12 +20692,8 @@ "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21584,12 +20701,8 @@ "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", "tableTo": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_referral_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21599,9 +20712,7 @@ "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -21681,12 +20792,8 @@ "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21694,12 +20801,8 @@ "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", "tableTo": "affiliate_codes", - "columnsFrom": [ - "affiliate_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["affiliate_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21875,12 +20978,8 @@ "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21888,12 +20987,8 @@ "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21901,12 +20996,8 @@ "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21916,9 +21007,7 @@ "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", "nullsNotDistinct": false, - "columns": [ - "client_address" - ] + "columns": ["client_address"] } }, "policies": {}, @@ -22011,12 +21100,8 @@ "name": "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk", "tableFrom": "milady_sandbox_backups", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "sandbox_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_record_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22295,12 +21380,8 @@ "name": "milady_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22308,12 +21389,8 @@ "name": "milady_sandboxes_user_id_users_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22321,12 +21398,8 @@ "name": "milady_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -22489,9 +21562,7 @@ "docker_nodes_node_id_unique": { "name": "docker_nodes_node_id_unique", "nullsNotDistinct": false, - "columns": [ - "node_id" - ] + "columns": ["node_id"] } }, "policies": {}, @@ -22692,72 +21763,37 @@ "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -22787,84 +21823,42 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -22889,69 +21883,37 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -23001,32 +21963,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -23044,24 +21991,12 @@ "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "whatsapp", - "other" - ] + "values": ["twilio", "blooio", "vonage", "whatsapp", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage", - "whatsapp" - ] + "values": ["sms", "voice", "both", "imessage", "whatsapp"] } }, "schemas": {}, @@ -23074,4 +22009,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0056_snapshot.json b/packages/db/migrations/meta/0056_snapshot.json index 7a0414041..34d0a7716 100644 --- a/packages/db/migrations/meta/0056_snapshot.json +++ b/packages/db/migrations/meta/0056_snapshot.json @@ -128,9 +128,7 @@ "uniqueConstraints": { "organizations_slug_unique": { "name": "organizations_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false } }, @@ -294,13 +292,9 @@ "organization_billing_organization_id_organizations_id_fk": { "name": "organization_billing_organization_id_organizations_id_fk", "tableFrom": "organization_billing", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -309,9 +303,7 @@ "uniqueConstraints": { "organization_billing_organization_id_unique": { "name": "organization_billing_organization_id_unique", - "columns": [ - "organization_id" - ], + "columns": ["organization_id"], "nullsNotDistinct": false } }, @@ -418,13 +410,9 @@ "organization_config_organization_id_organizations_id_fk": { "name": "organization_config_organization_id_organizations_id_fk", "tableFrom": "organization_config", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -433,9 +421,7 @@ "uniqueConstraints": { "organization_config_organization_id_unique": { "name": "organization_config_organization_id_unique", - "columns": [ - "organization_id" - ], + "columns": ["organization_id"], "nullsNotDistinct": false } }, @@ -590,39 +576,27 @@ "organization_invites_organization_id_organizations_id_fk": { "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "organization_invites_inviter_user_id_users_id_fk": { "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "inviter_user_id" - ], + "columnsFrom": ["inviter_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "organization_invites_accepted_by_user_id_users_id_fk": { "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", - "columnsFrom": [ - "accepted_by_user_id" - ], + "columnsFrom": ["accepted_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -631,9 +605,7 @@ "uniqueConstraints": { "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", - "columns": [ - "token_hash" - ], + "columns": ["token_hash"], "nullsNotDistinct": false } }, @@ -713,13 +685,9 @@ "organization_encryption_keys_organization_id_organizations_id_fk": { "name": "organization_encryption_keys_organization_id_organizations_id_fk", "tableFrom": "organization_encryption_keys", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -728,9 +696,7 @@ "uniqueConstraints": { "organization_encryption_keys_org_unique": { "name": "organization_encryption_keys_org_unique", - "columns": [ - "organization_id" - ], + "columns": ["organization_id"], "nullsNotDistinct": false } }, @@ -1114,13 +1080,9 @@ "users_organization_id_organizations_id_fk": { "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1129,51 +1091,37 @@ "uniqueConstraints": { "users_email_unique": { "name": "users_email_unique", - "columns": [ - "email" - ], + "columns": ["email"], "nullsNotDistinct": false }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", - "columns": [ - "wallet_address" - ], + "columns": ["wallet_address"], "nullsNotDistinct": false }, "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", - "columns": [ - "privy_user_id" - ], + "columns": ["privy_user_id"], "nullsNotDistinct": false }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", - "columns": [ - "telegram_id" - ], + "columns": ["telegram_id"], "nullsNotDistinct": false }, "users_discord_id_unique": { "name": "users_discord_id_unique", - "columns": [ - "discord_id" - ], + "columns": ["discord_id"], "nullsNotDistinct": false }, "users_whatsapp_id_unique": { "name": "users_whatsapp_id_unique", - "columns": [ - "whatsapp_id" - ], + "columns": ["whatsapp_id"], "nullsNotDistinct": false }, "users_phone_number_unique": { "name": "users_phone_number_unique", - "columns": [ - "phone_number" - ], + "columns": ["phone_number"], "nullsNotDistinct": false } }, @@ -1452,13 +1400,9 @@ "user_identities_user_id_users_id_fk": { "name": "user_identities_user_id_users_id_fk", "tableFrom": "user_identities", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1467,51 +1411,37 @@ "uniqueConstraints": { "user_identities_user_id_unique": { "name": "user_identities_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false }, "user_identities_privy_user_id_unique": { "name": "user_identities_privy_user_id_unique", - "columns": [ - "privy_user_id" - ], + "columns": ["privy_user_id"], "nullsNotDistinct": false }, "user_identities_anonymous_session_id_unique": { "name": "user_identities_anonymous_session_id_unique", - "columns": [ - "anonymous_session_id" - ], + "columns": ["anonymous_session_id"], "nullsNotDistinct": false }, "user_identities_telegram_id_unique": { "name": "user_identities_telegram_id_unique", - "columns": [ - "telegram_id" - ], + "columns": ["telegram_id"], "nullsNotDistinct": false }, "user_identities_phone_number_unique": { "name": "user_identities_phone_number_unique", - "columns": [ - "phone_number" - ], + "columns": ["phone_number"], "nullsNotDistinct": false }, "user_identities_discord_id_unique": { "name": "user_identities_discord_id_unique", - "columns": [ - "discord_id" - ], + "columns": ["discord_id"], "nullsNotDistinct": false }, "user_identities_whatsapp_id_unique": { "name": "user_identities_whatsapp_id_unique", - "columns": [ - "whatsapp_id" - ], + "columns": ["whatsapp_id"], "nullsNotDistinct": false } }, @@ -1619,13 +1549,9 @@ "user_preferences_user_id_users_id_fk": { "name": "user_preferences_user_id_users_id_fk", "tableFrom": "user_preferences", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1634,9 +1560,7 @@ "uniqueConstraints": { "user_preferences_user_id_unique": { "name": "user_preferences_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -1829,26 +1753,18 @@ "user_sessions_user_id_users_id_fk": { "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_sessions_organization_id_organizations_id_fk": { "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1857,9 +1773,7 @@ "uniqueConstraints": { "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", - "columns": [ - "session_token" - ], + "columns": ["session_token"], "nullsNotDistinct": false } }, @@ -2069,13 +1983,9 @@ "anonymous_sessions_user_id_users_id_fk": { "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2084,9 +1994,7 @@ "uniqueConstraints": { "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", - "columns": [ - "session_token" - ], + "columns": ["session_token"], "nullsNotDistinct": false } }, @@ -2283,26 +2191,18 @@ "api_keys_organization_id_organizations_id_fk": { "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "api_keys_user_id_users_id_fk": { "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2311,16 +2211,12 @@ "uniqueConstraints": { "api_keys_key_unique": { "name": "api_keys_key_unique", - "columns": [ - "key" - ], + "columns": ["key"], "nullsNotDistinct": false }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", - "columns": [ - "key_hash" - ], + "columns": ["key_hash"], "nullsNotDistinct": false } }, @@ -2463,13 +2359,9 @@ "cli_auth_sessions_user_id_users_id_fk": { "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2478,9 +2370,7 @@ "uniqueConstraints": { "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nullsNotDistinct": false } }, @@ -2766,39 +2656,27 @@ "usage_records_organization_id_organizations_id_fk": { "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "usage_records_user_id_users_id_fk": { "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "usage_records_api_key_id_api_keys_id_fk": { "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2964,13 +2842,9 @@ "usage_quotas_organization_id_organizations_id_fk": { "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -3124,26 +2998,18 @@ "credit_transactions_organization_id_organizations_id_fk": { "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "credit_transactions_user_id_users_id_fk": { "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3289,9 +3155,7 @@ "uniqueConstraints": { "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", - "columns": [ - "stripe_price_id" - ], + "columns": ["stripe_price_id"], "nullsNotDistinct": false } }, @@ -3475,9 +3339,7 @@ "uniqueConstraints": { "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", - "columns": [ - "stripe_invoice_id" - ], + "columns": ["stripe_invoice_id"], "nullsNotDistinct": false } }, @@ -3828,52 +3690,36 @@ "generations_organization_id_organizations_id_fk": { "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "generations_user_id_users_id_fk": { "name": "generations_user_id_users_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "generations_api_key_id_api_keys_id_fk": { "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "generations_usage_record_id_usage_records_id_fk": { "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", - "columnsFrom": [ - "usage_record_id" - ], + "columnsFrom": ["usage_record_id"], "tableTo": "usage_records", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -4082,52 +3928,36 @@ "jobs_organization_id_organizations_id_fk": { "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "jobs_user_id_users_id_fk": { "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" }, "jobs_api_key_id_api_keys_id_fk": { "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" }, "jobs_generation_id_generations_id_fk": { "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", - "columnsFrom": [ - "generation_id" - ], + "columnsFrom": ["generation_id"], "tableTo": "generations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -4493,13 +4323,9 @@ "service_pricing_audit_service_pricing_id_service_pricing_id_fk": { "name": "service_pricing_audit_service_pricing_id_service_pricing_id_fk", "tableFrom": "service_pricing_audit", - "columnsFrom": [ - "service_pricing_id" - ], + "columnsFrom": ["service_pricing_id"], "tableTo": "service_pricing", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -4758,26 +4584,18 @@ "conversation_messages_conversation_id_conversations_id_fk": { "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", - "columnsFrom": [ - "conversation_id" - ], + "columnsFrom": ["conversation_id"], "tableTo": "conversations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "conversation_messages_usage_record_id_usage_records_id_fk": { "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", - "columnsFrom": [ - "usage_record_id" - ], + "columnsFrom": ["usage_record_id"], "tableTo": "usage_records", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -4938,26 +4756,18 @@ "conversations_organization_id_organizations_id_fk": { "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "conversations_user_id_users_id_fk": { "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5505,26 +5315,18 @@ "user_characters_organization_id_organizations_id_fk": { "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_characters_user_id_users_id_fk": { "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5533,9 +5335,7 @@ "uniqueConstraints": { "user_characters_username_unique": { "name": "user_characters_username_unique", - "columns": [ - "username" - ], + "columns": ["username"], "nullsNotDistinct": false } }, @@ -5748,26 +5548,18 @@ "user_voices_organization_id_organizations_id_fk": { "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_voices_user_id_users_id_fk": { "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5776,9 +5568,7 @@ "uniqueConstraints": { "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", - "columns": [ - "elevenlabs_voice_id" - ], + "columns": ["elevenlabs_voice_id"], "nullsNotDistinct": false } }, @@ -5905,39 +5695,27 @@ "voice_cloning_jobs_organization_id_organizations_id_fk": { "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_cloning_jobs_user_id_users_id_fk": { "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_cloning_jobs_user_voice_id_user_voices_id_fk": { "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", - "columnsFrom": [ - "user_voice_id" - ], + "columnsFrom": ["user_voice_id"], "tableTo": "user_voices", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6057,52 +5835,36 @@ "voice_samples_user_voice_id_user_voices_id_fk": { "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "user_voice_id" - ], + "columnsFrom": ["user_voice_id"], "tableTo": "user_voices", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_job_id_voice_cloning_jobs_id_fk": { "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "job_id" - ], + "columnsFrom": ["job_id"], "tableTo": "voice_cloning_jobs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_organization_id_organizations_id_fk": { "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "voice_samples_user_id_users_id_fk": { "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6247,39 +6009,27 @@ "container_billing_records_container_id_containers_id_fk": { "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "container_billing_records_organization_id_organizations_id_fk": { "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "container_billing_records_credit_transaction_id_credit_transactions_id_fk": { "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", - "columnsFrom": [ - "credit_transaction_id" - ], + "columnsFrom": ["credit_transaction_id"], "tableTo": "credit_transactions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6727,52 +6477,36 @@ "containers_organization_id_organizations_id_fk": { "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "containers_user_id_users_id_fk": { "name": "containers_user_id_users_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "containers_api_key_id_api_keys_id_fk": { "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "containers_character_id_user_characters_id_fk": { "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6855,9 +6589,7 @@ "uniqueConstraints": { "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", - "columns": [ - "priority" - ], + "columns": ["priority"], "nullsNotDistinct": false } }, @@ -7090,13 +6822,9 @@ "app_analytics_app_id_apps_id_fk": { "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7330,26 +7058,18 @@ "app_requests_app_id_apps_id_fk": { "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_requests_user_id_users_id_fk": { "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7515,26 +7235,18 @@ "app_users_app_id_apps_id_fk": { "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_users_user_id_users_id_fk": { "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7957,26 +7669,18 @@ "apps_organization_id_organizations_id_fk": { "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "apps_created_by_user_id_users_id_fk": { "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7985,23 +7689,17 @@ "uniqueConstraints": { "apps_slug_unique": { "name": "apps_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", - "columns": [ - "api_key_id" - ], + "columns": ["api_key_id"], "nullsNotDistinct": false }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", - "columns": [ - "affiliate_code" - ], + "columns": ["affiliate_code"], "nullsNotDistinct": false } }, @@ -8110,13 +7808,9 @@ "app_config_app_id_apps_id_fk": { "name": "app_config_app_id_apps_id_fk", "tableFrom": "app_config", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8125,9 +7819,7 @@ "uniqueConstraints": { "app_config_app_id_unique": { "name": "app_config_app_id_unique", - "columns": [ - "app_id" - ], + "columns": ["app_id"], "nullsNotDistinct": false } }, @@ -8251,13 +7943,9 @@ "app_billing_app_id_apps_id_fk": { "name": "app_billing_app_id_apps_id_fk", "tableFrom": "app_billing", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8266,9 +7954,7 @@ "uniqueConstraints": { "app_billing_app_id_unique": { "name": "app_billing_app_id_unique", - "columns": [ - "app_id" - ], + "columns": ["app_id"], "nullsNotDistinct": false } }, @@ -8383,13 +8069,9 @@ "app_databases_app_id_apps_id_fk": { "name": "app_databases_app_id_apps_id_fk", "tableFrom": "app_databases", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8398,9 +8080,7 @@ "uniqueConstraints": { "app_databases_app_id_unique": { "name": "app_databases_app_id_unique", - "columns": [ - "app_id" - ], + "columns": ["app_id"], "nullsNotDistinct": false } }, @@ -8545,39 +8225,27 @@ "app_credit_balances_app_id_apps_id_fk": { "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_credit_balances_user_id_users_id_fk": { "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_credit_balances_organization_id_organizations_id_fk": { "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8696,13 +8364,9 @@ "app_earnings_app_id_apps_id_fk": { "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8841,26 +8505,18 @@ "app_earnings_transactions_app_id_apps_id_fk": { "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_earnings_transactions_user_id_users_id_fk": { "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -8979,13 +8635,9 @@ "referral_codes_user_id_users_id_fk": { "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -8994,9 +8646,7 @@ "uniqueConstraints": { "referral_codes_code_unique": { "name": "referral_codes_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "nullsNotDistinct": false } }, @@ -9145,65 +8795,45 @@ "referral_signups_referral_code_id_referral_codes_id_fk": { "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referral_code_id" - ], + "columnsFrom": ["referral_code_id"], "tableTo": "referral_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_referrer_user_id_users_id_fk": { "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referrer_user_id" - ], + "columnsFrom": ["referrer_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_referred_user_id_users_id_fk": { "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "referred_user_id" - ], + "columnsFrom": ["referred_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "referral_signups_app_owner_id_users_id_fk": { "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "app_owner_id" - ], + "columnsFrom": ["app_owner_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "referral_signups_creator_id_users_id_fk": { "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", - "columnsFrom": [ - "creator_id" - ], + "columnsFrom": ["creator_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -9342,13 +8972,9 @@ "social_share_rewards_user_id_users_id_fk": { "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9529,13 +9155,9 @@ "cache_agent_id_agents_id_fk": { "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9543,10 +9165,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -9576,13 +9195,9 @@ "channel_participants_channel_id_channels_id_fk": { "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9590,10 +9205,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -9673,13 +9285,9 @@ "channels_message_server_id_message_servers_id_fk": { "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", - "columnsFrom": [ - "message_server_id" - ], + "columnsFrom": ["message_server_id"], "tableTo": "message_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9757,65 +9365,45 @@ "components_entity_id_entities_id_fk": { "name": "components_entity_id_entities_id_fk", "tableFrom": "components", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_agent_id_agents_id_fk": { "name": "components_agent_id_agents_id_fk", "tableFrom": "components", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_room_id_rooms_id_fk": { "name": "components_room_id_rooms_id_fk", "tableFrom": "components", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_world_id_worlds_id_fk": { "name": "components_world_id_worlds_id_fk", "tableFrom": "components", - "columnsFrom": [ - "world_id" - ], + "columnsFrom": ["world_id"], "tableTo": "worlds", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "components_source_entity_id_entities_id_fk": { "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9908,26 +9496,18 @@ "embeddings_memory_id_memories_id_fk": { "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", - "columnsFrom": [ - "memory_id" - ], + "columnsFrom": ["memory_id"], "tableTo": "memories", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_embedding_memory": { "name": "fk_embedding_memory", "tableFrom": "embeddings", - "columnsFrom": [ - "memory_id" - ], + "columnsFrom": ["memory_id"], "tableTo": "memories", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -9986,13 +9566,9 @@ "entities_agent_id_agents_id_fk": { "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10001,10 +9577,7 @@ "uniqueConstraints": { "id_agent_id_unique": { "name": "id_agent_id_unique", - "columns": [ - "id", - "agent_id" - ], + "columns": ["id", "agent_id"], "nullsNotDistinct": false } }, @@ -10060,52 +9633,36 @@ "logs_entity_id_entities_id_fk": { "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "logs_room_id_rooms_id_fk": { "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "logs", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "logs", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10547,78 +10104,54 @@ "memories_entity_id_entities_id_fk": { "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memories_agent_id_agents_id_fk": { "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memories_room_id_rooms_id_fk": { "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "memories", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "memories", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_agent": { "name": "fk_agent", "tableFrom": "memories", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -10773,26 +10306,18 @@ "central_messages_channel_id_channels_id_fk": { "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "central_messages_in_reply_to_root_message_id_central_messages_id_fk": { "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], "tableTo": "central_messages", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -10882,65 +10407,45 @@ "participants_entity_id_entities_id_fk": { "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "participants_room_id_rooms_id_fk": { "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "participants_agent_id_agents_id_fk": { "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_room": { "name": "fk_room", "tableFrom": "participants", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user": { "name": "fk_user", "tableFrom": "participants", - "columnsFrom": [ - "entity_id" - ], + "columnsFrom": ["entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11027,65 +10532,45 @@ "relationships_source_entity_id_entities_id_fk": { "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "relationships_target_entity_id_entities_id_fk": { "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "target_entity_id" - ], + "columnsFrom": ["target_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "relationships_agent_id_agents_id_fk": { "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user_a": { "name": "fk_user_a", "tableFrom": "relationships", - "columnsFrom": [ - "source_entity_id" - ], + "columnsFrom": ["source_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "fk_user_b": { "name": "fk_user_b", "tableFrom": "relationships", - "columnsFrom": [ - "target_entity_id" - ], + "columnsFrom": ["target_entity_id"], "tableTo": "entities", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11094,11 +10579,7 @@ "uniqueConstraints": { "unique_relationship": { "name": "unique_relationship", - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ], + "columns": ["source_entity_id", "target_entity_id", "agent_id"], "nullsNotDistinct": false } }, @@ -11178,13 +10659,9 @@ "rooms_agent_id_agents_id_fk": { "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11428,13 +10905,9 @@ "tasks_agent_id_agents_id_fk": { "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11493,13 +10966,9 @@ "worlds_agent_id_agents_id_fk": { "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11552,13 +11021,9 @@ "eliza_room_characters_character_id_user_characters_id_fk": { "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11740,26 +11205,18 @@ "agent_events_agent_id_user_characters_id_fk": { "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_events_organization_id_organizations_id_fk": { "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -11949,39 +11406,27 @@ "mcp_usage_mcp_id_user_mcps_id_fk": { "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "mcp_id" - ], + "columnsFrom": ["mcp_id"], "tableTo": "user_mcps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "mcp_usage_organization_id_organizations_id_fk": { "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "mcp_usage_user_id_users_id_fk": { "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -12463,52 +11908,36 @@ "user_mcps_organization_id_organizations_id_fk": { "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_mcps_created_by_user_id_users_id_fk": { "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_mcps_container_id_containers_id_fk": { "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "user_mcps_verified_by_users_id_fk": { "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", - "columnsFrom": [ - "verified_by" - ], + "columnsFrom": ["verified_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -12693,13 +12122,9 @@ "redemption_limits_user_id_users_id_fk": { "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -12998,39 +12423,27 @@ "token_redemptions_user_id_users_id_fk": { "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "token_redemptions_app_id_apps_id_fk": { "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "token_redemptions_reviewed_by_users_id_fk": { "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", - "columnsFrom": [ - "reviewed_by" - ], + "columnsFrom": ["reviewed_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -13183,13 +12596,9 @@ "redeemable_earnings_user_id_users_id_fk": { "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13198,9 +12607,7 @@ "uniqueConstraints": { "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -13386,13 +12793,9 @@ "redeemable_earnings_ledger_user_id_users_id_fk": { "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -13477,9 +12880,7 @@ "uniqueConstraints": { "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", - "columns": [ - "ledger_entry_id" - ], + "columns": ["ledger_entry_id"], "nullsNotDistinct": false } }, @@ -13630,26 +13031,18 @@ "admin_users_user_id_users_id_fk": { "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "admin_users_granted_by_users_id_fk": { "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", - "columnsFrom": [ - "granted_by" - ], + "columnsFrom": ["granted_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -13658,9 +13051,7 @@ "uniqueConstraints": { "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", - "columns": [ - "wallet_address" - ], + "columns": ["wallet_address"], "nullsNotDistinct": false } }, @@ -13808,26 +13199,18 @@ "moderation_violations_user_id_users_id_fk": { "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "moderation_violations_reviewed_by_users_id_fk": { "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", - "columnsFrom": [ - "reviewed_by" - ], + "columnsFrom": ["reviewed_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -13995,26 +13378,18 @@ "user_moderation_status_user_id_users_id_fk": { "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_moderation_status_banned_by_users_id_fk": { "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", - "columnsFrom": [ - "banned_by" - ], + "columnsFrom": ["banned_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -14023,9 +13398,7 @@ "uniqueConstraints": { "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "nullsNotDistinct": false } }, @@ -14197,26 +13570,18 @@ "agent_budget_transactions_budget_id_agent_budgets_id_fk": { "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", - "columnsFrom": [ - "budget_id" - ], + "columnsFrom": ["budget_id"], "tableTo": "agent_budgets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_budget_transactions_agent_id_user_characters_id_fk": { "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14421,26 +13786,18 @@ "agent_budgets_agent_id_user_characters_id_fk": { "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_budgets_owner_org_id_organizations_id_fk": { "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", - "columnsFrom": [ - "owner_org_id" - ], + "columnsFrom": ["owner_org_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14449,9 +13806,7 @@ "uniqueConstraints": { "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", - "columns": [ - "agent_id" - ], + "columns": ["agent_id"], "nullsNotDistinct": false } }, @@ -14717,26 +14072,18 @@ "crypto_payments_organization_id_organizations_id_fk": { "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "crypto_payments_user_id_users_id_fk": { "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -14858,13 +14205,9 @@ "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -15137,39 +14480,27 @@ "app_sandbox_sessions_user_id_users_id_fk": { "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_sandbox_sessions_organization_id_organizations_id_fk": { "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_sandbox_sessions_app_id_apps_id_fk": { "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -15178,9 +14509,7 @@ "uniqueConstraints": { "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", - "columns": [ - "sandbox_id" - ], + "columns": ["sandbox_id"], "nullsNotDistinct": false } }, @@ -15365,9 +14694,7 @@ "uniqueConstraints": { "app_templates_slug_unique": { "name": "app_templates_slug_unique", - "columns": [ - "slug" - ], + "columns": ["slug"], "nullsNotDistinct": false } }, @@ -15529,9 +14856,7 @@ "uniqueConstraints": { "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", - "columns": [ - "snapshot_id" - ], + "columns": ["snapshot_id"], "nullsNotDistinct": false } }, @@ -15653,13 +14978,9 @@ "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -15760,13 +15081,9 @@ "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk": { "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", - "columnsFrom": [ - "sandbox_session_id" - ], + "columnsFrom": ["sandbox_session_id"], "tableTo": "app_sandbox_sessions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -15905,9 +15222,7 @@ "uniqueConstraints": { "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", - "columns": [ - "event_id" - ], + "columns": ["event_id"], "nullsNotDistinct": false } }, @@ -16036,26 +15351,18 @@ "app_secret_requirements_app_id_apps_id_fk": { "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "app_secret_requirements_approved_by_users_id_fk": { "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", - "columnsFrom": [ - "approved_by" - ], + "columnsFrom": ["approved_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -16351,26 +15658,18 @@ "oauth_sessions_organization_id_organizations_id_fk": { "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "oauth_sessions_user_id_users_id_fk": { "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -16753,39 +16052,27 @@ "secret_bindings_organization_id_organizations_id_fk": { "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secret_bindings_secret_id_secrets_id_fk": { "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "secret_id" - ], + "columnsFrom": ["secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secret_bindings_created_by_users_id_fk": { "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -17092,26 +16379,18 @@ "secrets_organization_id_organizations_id_fk": { "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "secrets_created_by_users_id_fk": { "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", - "columnsFrom": [ - "created_by" - ], + "columnsFrom": ["created_by"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "no action" } @@ -17284,13 +16563,9 @@ "app_domains_app_id_apps_id_fk": { "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -17753,65 +17028,45 @@ "managed_domains_organization_id_organizations_id_fk": { "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "managed_domains_app_id_apps_id_fk": { "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_container_id_containers_id_fk": { "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "container_id" - ], + "columnsFrom": ["container_id"], "tableTo": "containers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_agent_id_user_characters_id_fk": { "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "managed_domains_mcp_id_user_mcps_id_fk": { "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", - "columnsFrom": [ - "mcp_id" - ], + "columnsFrom": ["mcp_id"], "tableTo": "user_mcps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -17820,9 +17075,7 @@ "uniqueConstraints": { "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", - "columns": [ - "domain" - ], + "columns": ["domain"], "nullsNotDistinct": false } }, @@ -18029,52 +17282,36 @@ "platform_credential_sessions_organization_id_organizations_id_fk": { "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credential_sessions_app_id_apps_id_fk": { "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credential_sessions_requesting_user_id_users_id_fk": { "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "requesting_user_id" - ], + "columnsFrom": ["requesting_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "platform_credential_sessions_credential_id_platform_credentials_id_fk": { "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", - "columnsFrom": [ - "credential_id" - ], + "columnsFrom": ["credential_id"], "tableTo": "platform_credentials", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -18083,9 +17320,7 @@ "uniqueConstraints": { "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nullsNotDistinct": false } }, @@ -18395,39 +17630,27 @@ "platform_credentials_organization_id_organizations_id_fk": { "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credentials_user_id_users_id_fk": { "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "platform_credentials_app_id_apps_id_fk": { "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -18613,52 +17836,36 @@ "ad_accounts_organization_id_organizations_id_fk": { "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_accounts_connected_by_user_id_users_id_fk": { "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "connected_by_user_id" - ], + "columnsFrom": ["connected_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_accounts_access_token_secret_id_secrets_id_fk": { "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "access_token_secret_id" - ], + "columnsFrom": ["access_token_secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "ad_accounts_refresh_token_secret_id_secrets_id_fk": { "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", - "columnsFrom": [ - "refresh_token_secret_id" - ], + "columnsFrom": ["refresh_token_secret_id"], "tableTo": "secrets", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -18964,39 +18171,27 @@ "ad_campaigns_organization_id_organizations_id_fk": { "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_campaigns_ad_account_id_ad_accounts_id_fk": { "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "ad_account_id" - ], + "columnsFrom": ["ad_account_id"], "tableTo": "ad_accounts", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_campaigns_app_id_apps_id_fk": { "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -19189,13 +18384,9 @@ "ad_creatives_campaign_id_ad_campaigns_id_fk": { "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", - "columnsFrom": [ - "campaign_id" - ], + "columnsFrom": ["campaign_id"], "tableTo": "ad_campaigns", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19389,39 +18580,27 @@ "ad_transactions_organization_id_organizations_id_fk": { "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "ad_transactions_campaign_id_ad_campaigns_id_fk": { "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "campaign_id" - ], + "columnsFrom": ["campaign_id"], "tableTo": "ad_campaigns", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "ad_transactions_credit_transaction_id_credit_transactions_id_fk": { "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], + "columnsFrom": ["credit_transaction_id"], "tableTo": "credit_transactions", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -19513,13 +18692,9 @@ "seo_artifacts_request_id_seo_requests_id_fk": { "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", - "columnsFrom": [ - "request_id" - ], + "columnsFrom": ["request_id"], "tableTo": "seo_requests", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19671,13 +18846,9 @@ "seo_provider_calls_request_id_seo_requests_id_fk": { "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", - "columnsFrom": [ - "request_id" - ], + "columnsFrom": ["request_id"], "tableTo": "seo_requests", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -19918,52 +19089,36 @@ "seo_requests_organization_id_organizations_id_fk": { "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "seo_requests_app_id_apps_id_fk": { "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "app_id" - ], + "columnsFrom": ["app_id"], "tableTo": "apps", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "seo_requests_user_id_users_id_fk": { "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "seo_requests_api_key_id_api_keys_id_fk": { "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", - "columnsFrom": [ - "api_key_id" - ], + "columnsFrom": ["api_key_id"], "tableTo": "api_keys", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20080,13 +19235,9 @@ "telegram_chats_organization_id_organizations_id_fk": { "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20230,13 +19381,9 @@ "discord_guilds_organization_id_organizations_id_fk": { "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20415,13 +19562,9 @@ "discord_channels_organization_id_organizations_id_fk": { "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -20686,26 +19829,18 @@ "discord_connections_organization_id_organizations_id_fk": { "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "discord_connections_character_id_user_characters_id_fk": { "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -20969,13 +20104,9 @@ "agent_phone_numbers_organization_id_organizations_id_fk": { "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21200,13 +20331,9 @@ "phone_message_log_phone_number_id_agent_phone_numbers_id_fk": { "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", - "columnsFrom": [ - "phone_number_id" - ], + "columnsFrom": ["phone_number_id"], "tableTo": "agent_phone_numbers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21291,9 +20418,7 @@ "uniqueConstraints": { "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", - "columns": [ - "key" - ], + "columns": ["key"], "nullsNotDistinct": false } }, @@ -21459,13 +20584,9 @@ "entity_settings_user_id_users_id_fk": { "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21570,26 +20691,18 @@ "affiliate_codes_user_id_users_id_fk": { "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "affiliate_codes_parent_referral_id_affiliate_codes_id_fk": { "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], + "columnsFrom": ["parent_referral_id"], "tableTo": "affiliate_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -21598,9 +20711,7 @@ "uniqueConstraints": { "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "nullsNotDistinct": false } }, @@ -21680,26 +20791,18 @@ "user_affiliates_user_id_users_id_fk": { "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "user_affiliates_affiliate_code_id_affiliate_codes_id_fk": { "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", - "columnsFrom": [ - "affiliate_code_id" - ], + "columnsFrom": ["affiliate_code_id"], "tableTo": "affiliate_codes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -21874,39 +20977,27 @@ "agent_server_wallets_organization_id_organizations_id_fk": { "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_server_wallets_user_id_users_id_fk": { "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_server_wallets_character_id_user_characters_id_fk": { "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -21915,9 +21006,7 @@ "uniqueConstraints": { "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", - "columns": [ - "client_address" - ], + "columns": ["client_address"], "nullsNotDistinct": false } }, @@ -22010,13 +21099,9 @@ "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk": { "name": "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk", "tableFrom": "milady_sandbox_backups", - "columnsFrom": [ - "sandbox_record_id" - ], + "columnsFrom": ["sandbox_record_id"], "tableTo": "milady_sandboxes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -22294,39 +21379,27 @@ "milady_sandboxes_organization_id_organizations_id_fk": { "name": "milady_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milady_sandboxes", - "columnsFrom": [ - "organization_id" - ], + "columnsFrom": ["organization_id"], "tableTo": "organizations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "milady_sandboxes_user_id_users_id_fk": { "name": "milady_sandboxes_user_id_users_id_fk", "tableFrom": "milady_sandboxes", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "milady_sandboxes_character_id_user_characters_id_fk": { "name": "milady_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milady_sandboxes", - "columnsFrom": [ - "character_id" - ], + "columnsFrom": ["character_id"], "tableTo": "user_characters", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -22488,9 +21561,7 @@ "uniqueConstraints": { "docker_nodes_node_id_unique": { "name": "docker_nodes_node_id_unique", - "columns": [ - "node_id" - ], + "columns": ["node_id"], "nullsNotDistinct": false } }, @@ -22692,72 +21763,37 @@ "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -22787,84 +21823,42 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -22889,69 +21883,37 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -23001,32 +21963,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -23044,24 +21991,12 @@ "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "whatsapp", - "other" - ] + "values": ["twilio", "blooio", "vonage", "whatsapp", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage", - "whatsapp" - ] + "values": ["sms", "voice", "both", "imessage", "whatsapp"] } }, "schemas": {}, @@ -23074,4 +22009,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0059_snapshot.json b/packages/db/migrations/meta/0059_snapshot.json index 6b2a3b6ca..6de8a7d80 100644 --- a/packages/db/migrations/meta/0059_snapshot.json +++ b/packages/db/migrations/meta/0059_snapshot.json @@ -180,12 +180,8 @@ "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -193,12 +189,8 @@ "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", "tableTo": "users", - "columnsFrom": [ - "connected_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -206,12 +198,8 @@ "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "access_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["access_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -219,12 +207,8 @@ "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "refresh_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refresh_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -531,12 +515,8 @@ "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -544,12 +524,8 @@ "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", "tableTo": "ad_accounts", - "columnsFrom": [ - "ad_account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ad_account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -557,12 +533,8 @@ "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -756,12 +728,8 @@ "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -956,12 +924,8 @@ "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -969,12 +933,8 @@ "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -982,12 +942,8 @@ "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1142,12 +1098,8 @@ "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1155,12 +1107,8 @@ "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "granted_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1170,9 +1118,7 @@ "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -1274,12 +1220,8 @@ "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1287,12 +1229,8 @@ "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", "tableTo": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_referral_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1302,9 +1240,7 @@ "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -1384,12 +1320,8 @@ "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1397,12 +1329,8 @@ "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", "tableTo": "affiliate_codes", - "columnsFrom": [ - "affiliate_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["affiliate_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1578,12 +1506,8 @@ "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "agent_budgets", - "columnsFrom": [ - "budget_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["budget_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1591,12 +1515,8 @@ "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1802,12 +1722,8 @@ "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1815,12 +1731,8 @@ "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", "tableTo": "organizations", - "columnsFrom": [ - "owner_org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_org_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1830,9 +1742,7 @@ "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "agent_id" - ] + "columns": ["agent_id"] } }, "policies": {}, @@ -2011,12 +1921,8 @@ "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2024,12 +1930,8 @@ "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2294,12 +2196,8 @@ "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2525,12 +2423,8 @@ "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", "tableTo": "agent_phone_numbers", - "columnsFrom": [ - "phone_number_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["phone_number_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2755,12 +2649,8 @@ "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2768,12 +2658,8 @@ "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2781,12 +2667,8 @@ "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2796,9 +2678,7 @@ "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", "nullsNotDistinct": false, - "columns": [ - "client_address" - ] + "columns": ["client_address"] } }, "policies": {}, @@ -2878,9 +2758,7 @@ "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", "nullsNotDistinct": false, - "columns": [ - "priority" - ] + "columns": ["priority"] } }, "policies": {}, @@ -3090,12 +2968,8 @@ "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3105,9 +2979,7 @@ "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -3304,12 +3176,8 @@ "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3317,12 +3185,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3332,16 +3196,12 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", "nullsNotDistinct": false, - "columns": [ - "key_hash" - ] + "columns": ["key_hash"] } }, "policies": {}, @@ -3465,12 +3325,8 @@ "name": "app_billing_app_id_apps_id_fk", "tableFrom": "app_billing", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3480,9 +3336,7 @@ "app_billing_app_id_unique": { "name": "app_billing_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3591,12 +3445,8 @@ "name": "app_config_app_id_apps_id_fk", "tableFrom": "app_config", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3606,9 +3456,7 @@ "app_config_app_id_unique": { "name": "app_config_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3753,12 +3601,8 @@ "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3766,12 +3610,8 @@ "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3779,12 +3619,8 @@ "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3903,12 +3739,8 @@ "name": "app_databases_app_id_apps_id_fk", "tableFrom": "app_databases", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3918,9 +3750,7 @@ "app_databases_app_id_unique": { "name": "app_databases_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -4090,12 +3920,8 @@ "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4215,12 +4041,8 @@ "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4360,12 +4182,8 @@ "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4373,12 +4191,8 @@ "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4501,12 +4315,8 @@ "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4780,12 +4590,8 @@ "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4793,12 +4599,8 @@ "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4806,12 +4608,8 @@ "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4821,9 +4619,7 @@ "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", "nullsNotDistinct": false, - "columns": [ - "sandbox_id" - ] + "columns": ["sandbox_id"] } }, "policies": {}, @@ -5008,9 +4804,7 @@ "app_templates_slug_unique": { "name": "app_templates_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -5172,9 +4966,7 @@ "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", "nullsNotDistinct": false, - "columns": [ - "snapshot_id" - ] + "columns": ["snapshot_id"] } }, "policies": {}, @@ -5296,12 +5088,8 @@ "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5403,12 +5191,8 @@ "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5645,12 +5429,8 @@ "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5885,12 +5665,8 @@ "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5898,12 +5674,8 @@ "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6070,12 +5842,8 @@ "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6083,12 +5851,8 @@ "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6512,12 +6276,8 @@ "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6525,12 +6285,8 @@ "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6540,23 +6296,17 @@ "apps_slug_unique": { "name": "apps_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", "nullsNotDistinct": false, - "columns": [ - "api_key_id" - ] + "columns": ["api_key_id"] }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", "nullsNotDistinct": false, - "columns": [ - "affiliate_code" - ] + "columns": ["affiliate_code"] } }, "policies": {}, @@ -6699,12 +6449,8 @@ "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6714,9 +6460,7 @@ "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -6858,12 +6602,8 @@ "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6871,12 +6611,8 @@ "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6884,12 +6620,8 @@ "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7338,12 +7070,8 @@ "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7351,12 +7079,8 @@ "name": "containers_user_id_users_id_fk", "tableFrom": "containers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7364,12 +7088,8 @@ "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7377,12 +7097,8 @@ "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7537,12 +7253,8 @@ "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7550,12 +7262,8 @@ "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7717,12 +7425,8 @@ "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7730,12 +7434,8 @@ "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7882,9 +7582,7 @@ "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_price_id" - ] + "columns": ["stripe_price_id"] } }, "policies": {}, @@ -8035,12 +7733,8 @@ "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8048,12 +7742,8 @@ "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8323,12 +8013,8 @@ "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8336,12 +8022,8 @@ "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8625,12 +8307,8 @@ "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8896,12 +8574,8 @@ "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8909,12 +8583,8 @@ "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9059,12 +8729,8 @@ "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9227,9 +8893,7 @@ "docker_nodes_node_id_unique": { "name": "docker_nodes_node_id_unique", "nullsNotDistinct": false, - "columns": [ - "node_id" - ] + "columns": ["node_id"] } }, "policies": {}, @@ -9407,12 +9071,8 @@ "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9420,10 +9080,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -9454,12 +9111,8 @@ "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9467,10 +9120,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -9551,12 +9201,8 @@ "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", "tableTo": "message_servers", - "columnsFrom": [ - "message_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9635,12 +9281,8 @@ "name": "components_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9648,12 +9290,8 @@ "name": "components_agent_id_agents_id_fk", "tableFrom": "components", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9661,12 +9299,8 @@ "name": "components_room_id_rooms_id_fk", "tableFrom": "components", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9674,12 +9308,8 @@ "name": "components_world_id_worlds_id_fk", "tableFrom": "components", "tableTo": "worlds", - "columnsFrom": [ - "world_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["world_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9687,12 +9317,8 @@ "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9786,12 +9412,8 @@ "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9799,12 +9421,8 @@ "name": "fk_embedding_memory", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9864,12 +9482,8 @@ "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9879,10 +9493,7 @@ "id_agent_id_unique": { "name": "id_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "id", - "agent_id" - ] + "columns": ["id", "agent_id"] } }, "policies": {}, @@ -9938,12 +9549,8 @@ "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9951,12 +9558,8 @@ "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9964,12 +9567,8 @@ "name": "fk_room", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9977,12 +9576,8 @@ "name": "fk_user", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10425,12 +10020,8 @@ "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10438,12 +10029,8 @@ "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10451,12 +10038,8 @@ "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10464,12 +10047,8 @@ "name": "fk_room", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10477,12 +10056,8 @@ "name": "fk_user", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10490,12 +10065,8 @@ "name": "fk_agent", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10651,12 +10222,8 @@ "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10664,12 +10231,8 @@ "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", "tableTo": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10760,12 +10323,8 @@ "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10773,12 +10332,8 @@ "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10786,12 +10341,8 @@ "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10799,12 +10350,8 @@ "name": "fk_room", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10812,12 +10359,8 @@ "name": "fk_user", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10905,12 +10448,8 @@ "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10918,12 +10457,8 @@ "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10931,12 +10466,8 @@ "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10944,12 +10475,8 @@ "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10957,12 +10484,8 @@ "name": "fk_user_b", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10972,11 +10495,7 @@ "unique_relationship": { "name": "unique_relationship", "nullsNotDistinct": false, - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ] + "columns": ["source_entity_id", "target_entity_id", "agent_id"] } }, "policies": {}, @@ -11056,12 +10575,8 @@ "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11306,12 +10821,8 @@ "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11371,12 +10882,8 @@ "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11430,12 +10937,8 @@ "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11605,12 +11108,8 @@ "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11965,12 +11464,8 @@ "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11978,12 +11473,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11991,12 +11482,8 @@ "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12004,12 +11491,8 @@ "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12095,9 +11578,7 @@ "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -12281,9 +11762,7 @@ "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_invoice_id" - ] + "columns": ["stripe_invoice_id"] } }, "policies": {}, @@ -12489,12 +11968,8 @@ "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12502,12 +11977,8 @@ "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12515,12 +11986,8 @@ "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12528,12 +11995,8 @@ "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", "tableTo": "generations", - "columnsFrom": [ - "generation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12782,12 +12245,8 @@ "name": "llm_trajectories_organization_id_organizations_id_fk", "tableFrom": "llm_trajectories", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12795,12 +12254,8 @@ "name": "llm_trajectories_user_id_users_id_fk", "tableFrom": "llm_trajectories", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12808,12 +12263,8 @@ "name": "llm_trajectories_api_key_id_api_keys_id_fk", "tableFrom": "llm_trajectories", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13277,12 +12728,8 @@ "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13290,12 +12737,8 @@ "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13303,12 +12746,8 @@ "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13316,12 +12755,8 @@ "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13329,12 +12764,8 @@ "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13344,9 +12775,7 @@ "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", "nullsNotDistinct": false, - "columns": [ - "domain" - ] + "columns": ["domain"] } }, "policies": {}, @@ -13472,12 +12901,8 @@ "name": "milady_pairing_tokens_organization_id_organizations_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13485,12 +12910,8 @@ "name": "milady_pairing_tokens_user_id_users_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13498,12 +12919,8 @@ "name": "milady_pairing_tokens_agent_id_milady_sandboxes_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13513,9 +12930,7 @@ "milady_pairing_tokens_token_hash_unique": { "name": "milady_pairing_tokens_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -13608,12 +13023,8 @@ "name": "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk", "tableFrom": "milady_sandbox_backups", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "sandbox_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_record_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13946,12 +13357,8 @@ "name": "milady_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13959,12 +13366,8 @@ "name": "milady_sandboxes_user_id_users_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13972,12 +13375,8 @@ "name": "milady_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14263,12 +13662,8 @@ "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14276,12 +13671,8 @@ "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14450,12 +13841,8 @@ "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14463,12 +13850,8 @@ "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "banned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14478,9 +13861,7 @@ "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -14639,12 +14020,8 @@ "name": "organization_billing_organization_id_organizations_id_fk", "tableFrom": "organization_billing", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14654,9 +14031,7 @@ "organization_billing_organization_id_unique": { "name": "organization_billing_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14763,12 +14138,8 @@ "name": "organization_config_organization_id_organizations_id_fk", "tableFrom": "organization_config", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14778,9 +14149,7 @@ "organization_config_organization_id_unique": { "name": "organization_config_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14860,12 +14229,8 @@ "name": "organization_encryption_keys_organization_id_organizations_id_fk", "tableFrom": "organization_encryption_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14875,9 +14240,7 @@ "organization_encryption_keys_org_unique": { "name": "organization_encryption_keys_org_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15032,12 +14395,8 @@ "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15045,12 +14404,8 @@ "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "inviter_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15058,12 +14413,8 @@ "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15073,9 +14424,7 @@ "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -15207,9 +14556,7 @@ "organizations_slug_unique": { "name": "organizations_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -15421,12 +14768,8 @@ "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15434,12 +14777,8 @@ "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15447,12 +14786,8 @@ "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "users", - "columnsFrom": [ - "requesting_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requesting_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -15460,12 +14795,8 @@ "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "platform_credentials", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15475,9 +14806,7 @@ "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -15787,12 +15116,8 @@ "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15800,12 +15125,8 @@ "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15813,12 +15134,8 @@ "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16077,12 +15394,8 @@ "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16092,9 +15405,7 @@ "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -16280,12 +15591,8 @@ "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16371,9 +15678,7 @@ "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", "nullsNotDistinct": false, - "columns": [ - "ledger_entry_id" - ] + "columns": ["ledger_entry_id"] } }, "policies": {}, @@ -16489,12 +15794,8 @@ "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16504,9 +15805,7 @@ "referral_codes_code_unique": { "name": "referral_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -16655,12 +15954,8 @@ "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16668,12 +15963,8 @@ "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referrer_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referrer_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16681,12 +15972,8 @@ "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referred_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referred_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16694,12 +15981,8 @@ "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "app_owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_owner_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -16707,12 +15990,8 @@ "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16852,12 +16131,8 @@ "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17075,12 +16350,8 @@ "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17088,12 +16359,8 @@ "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "users", - "columnsFrom": [ - "approved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -17390,12 +16657,8 @@ "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17403,12 +16666,8 @@ "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17792,12 +17051,8 @@ "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17805,12 +17060,8 @@ "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", "tableTo": "secrets", - "columnsFrom": [ - "secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["secret_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17818,12 +17069,8 @@ "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18131,12 +17378,8 @@ "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18144,12 +17387,8 @@ "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18242,12 +17481,8 @@ "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18400,12 +17635,8 @@ "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18647,12 +17878,8 @@ "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18660,12 +17887,8 @@ "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18673,12 +17896,8 @@ "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18686,12 +17905,8 @@ "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -18924,12 +18139,8 @@ "name": "service_pricing_audit_service_pricing_id_service_pricing_id_fk", "tableFrom": "service_pricing_audit", "tableTo": "service_pricing", - "columnsFrom": [ - "service_pricing_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_pricing_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19047,12 +18258,8 @@ "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19238,12 +18445,8 @@ "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19543,12 +18746,8 @@ "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19556,12 +18755,8 @@ "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19569,12 +18764,8 @@ "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -19741,12 +18932,8 @@ "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20036,12 +19223,8 @@ "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20049,12 +19232,8 @@ "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -20062,12 +19241,8 @@ "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20616,12 +19791,8 @@ "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20629,12 +19800,8 @@ "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20644,9 +19811,7 @@ "user_characters_username_unique": { "name": "user_characters_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -20925,12 +20090,8 @@ "name": "user_identities_user_id_users_id_fk", "tableFrom": "user_identities", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20940,51 +20101,37 @@ "user_identities_user_id_unique": { "name": "user_identities_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] }, "user_identities_privy_user_id_unique": { "name": "user_identities_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "user_identities_anonymous_session_id_unique": { "name": "user_identities_anonymous_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "anonymous_session_id" - ] + "columns": ["anonymous_session_id"] }, "user_identities_telegram_id_unique": { "name": "user_identities_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "user_identities_phone_number_unique": { "name": "user_identities_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] }, "user_identities_discord_id_unique": { "name": "user_identities_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "user_identities_whatsapp_id_unique": { "name": "user_identities_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] } }, "policies": {}, @@ -21171,12 +20318,8 @@ "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21184,12 +20327,8 @@ "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21197,12 +20336,8 @@ "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21685,12 +20820,8 @@ "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21698,12 +20829,8 @@ "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21711,12 +20838,8 @@ "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -21724,12 +20847,8 @@ "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "verified_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["verified_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -21841,12 +20960,8 @@ "name": "user_preferences_user_id_users_id_fk", "tableFrom": "user_preferences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21856,9 +20971,7 @@ "user_preferences_user_id_unique": { "name": "user_preferences_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -22051,12 +21164,8 @@ "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22064,12 +21173,8 @@ "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22079,9 +21184,7 @@ "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -22294,12 +21397,8 @@ "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22307,12 +21406,8 @@ "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22322,9 +21417,7 @@ "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", "nullsNotDistinct": false, - "columns": [ - "elevenlabs_voice_id" - ] + "columns": ["elevenlabs_voice_id"] } }, "policies": {}, @@ -22451,12 +21544,8 @@ "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22464,12 +21553,8 @@ "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22477,12 +21562,8 @@ "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -22603,12 +21684,8 @@ "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22616,12 +21693,8 @@ "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", "tableTo": "voice_cloning_jobs", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22629,12 +21702,8 @@ "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22642,12 +21711,8 @@ "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23035,12 +22100,8 @@ "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23050,51 +22111,37 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] }, "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "users_discord_id_unique": { "name": "users_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "users_whatsapp_id_unique": { "name": "users_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] }, "users_phone_number_unique": { "name": "users_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] } }, "policies": {}, @@ -23349,12 +22396,8 @@ "name": "vertex_model_assignments_organization_id_organizations_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23362,12 +22405,8 @@ "name": "vertex_model_assignments_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23375,12 +22414,8 @@ "name": "vertex_model_assignments_tuned_model_id_vertex_tuned_models_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "vertex_tuned_models", - "columnsFrom": [ - "tuned_model_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuned_model_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23388,12 +22423,8 @@ "name": "vertex_model_assignments_assigned_by_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -23608,12 +22639,8 @@ "name": "vertex_tuned_models_tuning_job_id_vertex_tuning_jobs_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "vertex_tuning_jobs", - "columnsFrom": [ - "tuning_job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuning_job_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -23621,12 +22648,8 @@ "name": "vertex_tuned_models_organization_id_organizations_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23634,12 +22657,8 @@ "name": "vertex_tuned_models_user_id_users_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23959,12 +22978,8 @@ "name": "vertex_tuning_jobs_organization_id_organizations_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23972,12 +22987,8 @@ "name": "vertex_tuning_jobs_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23985,12 +22996,8 @@ "name": "vertex_tuning_jobs_created_by_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -24135,9 +23142,7 @@ "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", "nullsNotDistinct": false, - "columns": [ - "event_id" - ] + "columns": ["event_id"] } }, "policies": {}, @@ -24149,133 +23154,67 @@ "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "whatsapp", - "other" - ] + "values": ["twilio", "blooio", "vonage", "whatsapp", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage", - "whatsapp" - ] + "values": ["sms", "voice", "both", "imessage", "whatsapp"] }, "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -24325,73 +23264,37 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -24416,11 +23319,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -24437,32 +23336,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -24480,12 +23364,7 @@ "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -24503,22 +23382,12 @@ "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.vertex_tuning_job_state": { "name": "vertex_tuning_job_state", @@ -24534,11 +23403,7 @@ "public.vertex_tuning_scope": { "name": "vertex_tuning_scope", "schema": "public", - "values": [ - "global", - "organization", - "user" - ] + "values": ["global", "organization", "user"] }, "public.vertex_tuning_slot": { "name": "vertex_tuning_slot", @@ -24563,4 +23428,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0061_snapshot.json b/packages/db/migrations/meta/0061_snapshot.json index 3188f67c1..2963e7e6b 100644 --- a/packages/db/migrations/meta/0061_snapshot.json +++ b/packages/db/migrations/meta/0061_snapshot.json @@ -180,12 +180,8 @@ "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -193,12 +189,8 @@ "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", "tableTo": "users", - "columnsFrom": [ - "connected_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -206,12 +198,8 @@ "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "access_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["access_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -219,12 +207,8 @@ "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "refresh_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refresh_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -531,12 +515,8 @@ "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -544,12 +524,8 @@ "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", "tableTo": "ad_accounts", - "columnsFrom": [ - "ad_account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ad_account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -557,12 +533,8 @@ "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -756,12 +728,8 @@ "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -956,12 +924,8 @@ "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -969,12 +933,8 @@ "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -982,12 +942,8 @@ "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1142,12 +1098,8 @@ "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1155,12 +1107,8 @@ "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "granted_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1170,9 +1118,7 @@ "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -1274,12 +1220,8 @@ "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1287,12 +1229,8 @@ "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", "tableTo": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_referral_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1302,9 +1240,7 @@ "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -1384,12 +1320,8 @@ "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1397,12 +1329,8 @@ "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", "tableTo": "affiliate_codes", - "columnsFrom": [ - "affiliate_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["affiliate_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1578,12 +1506,8 @@ "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "agent_budgets", - "columnsFrom": [ - "budget_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["budget_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1591,12 +1515,8 @@ "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1802,12 +1722,8 @@ "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1815,12 +1731,8 @@ "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", "tableTo": "organizations", - "columnsFrom": [ - "owner_org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_org_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1830,9 +1742,7 @@ "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "agent_id" - ] + "columns": ["agent_id"] } }, "policies": {}, @@ -2011,12 +1921,8 @@ "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2024,12 +1930,8 @@ "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2294,12 +2196,8 @@ "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2525,12 +2423,8 @@ "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", "tableTo": "agent_phone_numbers", - "columnsFrom": [ - "phone_number_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["phone_number_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2755,12 +2649,8 @@ "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2768,12 +2658,8 @@ "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2781,12 +2667,8 @@ "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2796,9 +2678,7 @@ "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", "nullsNotDistinct": false, - "columns": [ - "client_address" - ] + "columns": ["client_address"] } }, "policies": {}, @@ -2878,9 +2758,7 @@ "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", "nullsNotDistinct": false, - "columns": [ - "priority" - ] + "columns": ["priority"] } }, "policies": {}, @@ -3090,12 +2968,8 @@ "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3105,9 +2979,7 @@ "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -3304,12 +3176,8 @@ "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3317,12 +3185,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3332,16 +3196,12 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", "nullsNotDistinct": false, - "columns": [ - "key_hash" - ] + "columns": ["key_hash"] } }, "policies": {}, @@ -3465,12 +3325,8 @@ "name": "app_billing_app_id_apps_id_fk", "tableFrom": "app_billing", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3480,9 +3336,7 @@ "app_billing_app_id_unique": { "name": "app_billing_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3591,12 +3445,8 @@ "name": "app_config_app_id_apps_id_fk", "tableFrom": "app_config", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3606,9 +3456,7 @@ "app_config_app_id_unique": { "name": "app_config_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3753,12 +3601,8 @@ "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3766,12 +3610,8 @@ "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3779,12 +3619,8 @@ "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3903,12 +3739,8 @@ "name": "app_databases_app_id_apps_id_fk", "tableFrom": "app_databases", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3918,9 +3750,7 @@ "app_databases_app_id_unique": { "name": "app_databases_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -4090,12 +3920,8 @@ "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4215,12 +4041,8 @@ "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4360,12 +4182,8 @@ "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4373,12 +4191,8 @@ "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4501,12 +4315,8 @@ "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4780,12 +4590,8 @@ "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4793,12 +4599,8 @@ "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4806,12 +4608,8 @@ "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4821,9 +4619,7 @@ "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", "nullsNotDistinct": false, - "columns": [ - "sandbox_id" - ] + "columns": ["sandbox_id"] } }, "policies": {}, @@ -5008,9 +4804,7 @@ "app_templates_slug_unique": { "name": "app_templates_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -5172,9 +4966,7 @@ "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", "nullsNotDistinct": false, - "columns": [ - "snapshot_id" - ] + "columns": ["snapshot_id"] } }, "policies": {}, @@ -5296,12 +5088,8 @@ "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5403,12 +5191,8 @@ "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5645,12 +5429,8 @@ "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5885,12 +5665,8 @@ "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5898,12 +5674,8 @@ "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6070,12 +5842,8 @@ "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6083,12 +5851,8 @@ "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6512,12 +6276,8 @@ "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6525,12 +6285,8 @@ "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6540,23 +6296,17 @@ "apps_slug_unique": { "name": "apps_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", "nullsNotDistinct": false, - "columns": [ - "api_key_id" - ] + "columns": ["api_key_id"] }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", "nullsNotDistinct": false, - "columns": [ - "affiliate_code" - ] + "columns": ["affiliate_code"] } }, "policies": {}, @@ -6699,12 +6449,8 @@ "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6714,9 +6460,7 @@ "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -6858,12 +6602,8 @@ "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6871,12 +6611,8 @@ "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6884,12 +6620,8 @@ "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7338,12 +7070,8 @@ "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7351,12 +7079,8 @@ "name": "containers_user_id_users_id_fk", "tableFrom": "containers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7364,12 +7088,8 @@ "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7377,12 +7097,8 @@ "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7537,12 +7253,8 @@ "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7550,12 +7262,8 @@ "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7717,12 +7425,8 @@ "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7730,12 +7434,8 @@ "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7882,9 +7582,7 @@ "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_price_id" - ] + "columns": ["stripe_price_id"] } }, "policies": {}, @@ -8035,12 +7733,8 @@ "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8048,12 +7742,8 @@ "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8323,12 +8013,8 @@ "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8336,12 +8022,8 @@ "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8625,12 +8307,8 @@ "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8896,12 +8574,8 @@ "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8909,12 +8583,8 @@ "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9059,12 +8729,8 @@ "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9227,9 +8893,7 @@ "docker_nodes_node_id_unique": { "name": "docker_nodes_node_id_unique", "nullsNotDistinct": false, - "columns": [ - "node_id" - ] + "columns": ["node_id"] } }, "policies": {}, @@ -9407,12 +9071,8 @@ "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9420,10 +9080,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -9454,12 +9111,8 @@ "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9467,10 +9120,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -9551,12 +9201,8 @@ "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", "tableTo": "message_servers", - "columnsFrom": [ - "message_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9635,12 +9281,8 @@ "name": "components_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9648,12 +9290,8 @@ "name": "components_agent_id_agents_id_fk", "tableFrom": "components", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9661,12 +9299,8 @@ "name": "components_room_id_rooms_id_fk", "tableFrom": "components", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9674,12 +9308,8 @@ "name": "components_world_id_worlds_id_fk", "tableFrom": "components", "tableTo": "worlds", - "columnsFrom": [ - "world_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["world_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9687,12 +9317,8 @@ "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9786,12 +9412,8 @@ "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9799,12 +9421,8 @@ "name": "fk_embedding_memory", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9864,12 +9482,8 @@ "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9879,10 +9493,7 @@ "id_agent_id_unique": { "name": "id_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "id", - "agent_id" - ] + "columns": ["id", "agent_id"] } }, "policies": {}, @@ -9938,12 +9549,8 @@ "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9951,12 +9558,8 @@ "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9964,12 +9567,8 @@ "name": "fk_room", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9977,12 +9576,8 @@ "name": "fk_user", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10425,12 +10020,8 @@ "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10438,12 +10029,8 @@ "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10451,12 +10038,8 @@ "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10464,12 +10047,8 @@ "name": "fk_room", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10477,12 +10056,8 @@ "name": "fk_user", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10490,12 +10065,8 @@ "name": "fk_agent", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10651,12 +10222,8 @@ "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10664,12 +10231,8 @@ "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", "tableTo": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10760,12 +10323,8 @@ "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10773,12 +10332,8 @@ "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10786,12 +10341,8 @@ "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10799,12 +10350,8 @@ "name": "fk_room", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10812,12 +10359,8 @@ "name": "fk_user", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10905,12 +10448,8 @@ "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10918,12 +10457,8 @@ "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10931,12 +10466,8 @@ "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10944,12 +10475,8 @@ "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10957,12 +10484,8 @@ "name": "fk_user_b", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10972,11 +10495,7 @@ "unique_relationship": { "name": "unique_relationship", "nullsNotDistinct": false, - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ] + "columns": ["source_entity_id", "target_entity_id", "agent_id"] } }, "policies": {}, @@ -11056,12 +10575,8 @@ "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11306,12 +10821,8 @@ "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11371,12 +10882,8 @@ "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11430,12 +10937,8 @@ "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11605,12 +11108,8 @@ "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11965,12 +11464,8 @@ "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11978,12 +11473,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11991,12 +11482,8 @@ "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12004,12 +11491,8 @@ "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12095,9 +11578,7 @@ "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -12281,9 +11762,7 @@ "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_invoice_id" - ] + "columns": ["stripe_invoice_id"] } }, "policies": {}, @@ -12489,12 +11968,8 @@ "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12502,12 +11977,8 @@ "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12515,12 +11986,8 @@ "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12528,12 +11995,8 @@ "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", "tableTo": "generations", - "columnsFrom": [ - "generation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12782,12 +12245,8 @@ "name": "llm_trajectories_organization_id_organizations_id_fk", "tableFrom": "llm_trajectories", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12795,12 +12254,8 @@ "name": "llm_trajectories_user_id_users_id_fk", "tableFrom": "llm_trajectories", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12808,12 +12263,8 @@ "name": "llm_trajectories_api_key_id_api_keys_id_fk", "tableFrom": "llm_trajectories", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13277,12 +12728,8 @@ "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13290,12 +12737,8 @@ "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13303,12 +12746,8 @@ "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13316,12 +12755,8 @@ "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13329,12 +12764,8 @@ "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13344,9 +12775,7 @@ "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", "nullsNotDistinct": false, - "columns": [ - "domain" - ] + "columns": ["domain"] } }, "policies": {}, @@ -13472,12 +12901,8 @@ "name": "milady_pairing_tokens_organization_id_organizations_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13485,12 +12910,8 @@ "name": "milady_pairing_tokens_user_id_users_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13498,12 +12919,8 @@ "name": "milady_pairing_tokens_agent_id_milady_sandboxes_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13513,9 +12930,7 @@ "milady_pairing_tokens_token_hash_unique": { "name": "milady_pairing_tokens_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -13608,12 +13023,8 @@ "name": "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk", "tableFrom": "milady_sandbox_backups", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "sandbox_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_record_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13946,12 +13357,8 @@ "name": "milady_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13959,12 +13366,8 @@ "name": "milady_sandboxes_user_id_users_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13972,12 +13375,8 @@ "name": "milady_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14263,12 +13662,8 @@ "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14276,12 +13671,8 @@ "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14450,12 +13841,8 @@ "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14463,12 +13850,8 @@ "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "banned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14478,9 +13861,7 @@ "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -14639,12 +14020,8 @@ "name": "organization_billing_organization_id_organizations_id_fk", "tableFrom": "organization_billing", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14654,9 +14031,7 @@ "organization_billing_organization_id_unique": { "name": "organization_billing_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14763,12 +14138,8 @@ "name": "organization_config_organization_id_organizations_id_fk", "tableFrom": "organization_config", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14778,9 +14149,7 @@ "organization_config_organization_id_unique": { "name": "organization_config_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14860,12 +14229,8 @@ "name": "organization_encryption_keys_organization_id_organizations_id_fk", "tableFrom": "organization_encryption_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14875,9 +14240,7 @@ "organization_encryption_keys_org_unique": { "name": "organization_encryption_keys_org_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15032,12 +14395,8 @@ "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15045,12 +14404,8 @@ "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "inviter_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15058,12 +14413,8 @@ "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15073,9 +14424,7 @@ "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -15219,16 +14568,12 @@ "organizations_slug_unique": { "name": "organizations_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "organizations_steward_tenant_id_unique": { "name": "organizations_steward_tenant_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_tenant_id" - ] + "columns": ["steward_tenant_id"] } }, "policies": {}, @@ -15440,12 +14785,8 @@ "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15453,12 +14794,8 @@ "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15466,12 +14803,8 @@ "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "users", - "columnsFrom": [ - "requesting_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requesting_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -15479,12 +14812,8 @@ "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "platform_credentials", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15494,9 +14823,7 @@ "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -15806,12 +15133,8 @@ "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15819,12 +15142,8 @@ "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15832,12 +15151,8 @@ "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16096,12 +15411,8 @@ "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16111,9 +15422,7 @@ "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -16299,12 +15608,8 @@ "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16390,9 +15695,7 @@ "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", "nullsNotDistinct": false, - "columns": [ - "ledger_entry_id" - ] + "columns": ["ledger_entry_id"] } }, "policies": {}, @@ -16508,12 +15811,8 @@ "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16523,9 +15822,7 @@ "referral_codes_code_unique": { "name": "referral_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -16674,12 +15971,8 @@ "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16687,12 +15980,8 @@ "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referrer_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referrer_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16700,12 +15989,8 @@ "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referred_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referred_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16713,12 +15998,8 @@ "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "app_owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_owner_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -16726,12 +16007,8 @@ "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16871,12 +16148,8 @@ "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17094,12 +16367,8 @@ "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17107,12 +16376,8 @@ "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "users", - "columnsFrom": [ - "approved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -17409,12 +16674,8 @@ "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17422,12 +16683,8 @@ "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17811,12 +17068,8 @@ "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17824,12 +17077,8 @@ "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", "tableTo": "secrets", - "columnsFrom": [ - "secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["secret_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17837,12 +17086,8 @@ "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18150,12 +17395,8 @@ "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18163,12 +17404,8 @@ "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18261,12 +17498,8 @@ "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18419,12 +17652,8 @@ "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18666,12 +17895,8 @@ "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18679,12 +17904,8 @@ "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18692,12 +17913,8 @@ "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18705,12 +17922,8 @@ "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -18943,12 +18156,8 @@ "name": "service_pricing_audit_service_pricing_id_service_pricing_id_fk", "tableFrom": "service_pricing_audit", "tableTo": "service_pricing", - "columnsFrom": [ - "service_pricing_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_pricing_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19066,12 +18275,8 @@ "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19257,12 +18462,8 @@ "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19562,12 +18763,8 @@ "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19575,12 +18772,8 @@ "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19588,12 +18781,8 @@ "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -19760,12 +18949,8 @@ "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20055,12 +19240,8 @@ "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20068,12 +19249,8 @@ "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -20081,12 +19258,8 @@ "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20635,12 +19808,8 @@ "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20648,12 +19817,8 @@ "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20663,9 +19828,7 @@ "user_characters_username_unique": { "name": "user_characters_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -20965,12 +20128,8 @@ "name": "user_identities_user_id_users_id_fk", "tableFrom": "user_identities", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20980,58 +20139,42 @@ "user_identities_user_id_unique": { "name": "user_identities_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] }, "user_identities_steward_user_id_unique": { "name": "user_identities_steward_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_user_id" - ] + "columns": ["steward_user_id"] }, "user_identities_privy_user_id_unique": { "name": "user_identities_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "user_identities_anonymous_session_id_unique": { "name": "user_identities_anonymous_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "anonymous_session_id" - ] + "columns": ["anonymous_session_id"] }, "user_identities_telegram_id_unique": { "name": "user_identities_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "user_identities_phone_number_unique": { "name": "user_identities_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] }, "user_identities_discord_id_unique": { "name": "user_identities_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "user_identities_whatsapp_id_unique": { "name": "user_identities_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] } }, "policies": {}, @@ -21218,12 +20361,8 @@ "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21231,12 +20370,8 @@ "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21244,12 +20379,8 @@ "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21732,12 +20863,8 @@ "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21745,12 +20872,8 @@ "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21758,12 +20881,8 @@ "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -21771,12 +20890,8 @@ "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "verified_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["verified_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -21888,12 +21003,8 @@ "name": "user_preferences_user_id_users_id_fk", "tableFrom": "user_preferences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21903,9 +21014,7 @@ "user_preferences_user_id_unique": { "name": "user_preferences_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -22098,12 +21207,8 @@ "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22111,12 +21216,8 @@ "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22126,9 +21227,7 @@ "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -22341,12 +21440,8 @@ "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22354,12 +21449,8 @@ "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22369,9 +21460,7 @@ "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", "nullsNotDistinct": false, - "columns": [ - "elevenlabs_voice_id" - ] + "columns": ["elevenlabs_voice_id"] } }, "policies": {}, @@ -22498,12 +21587,8 @@ "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22511,12 +21596,8 @@ "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22524,12 +21605,8 @@ "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -22650,12 +21727,8 @@ "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22663,12 +21736,8 @@ "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", "tableTo": "voice_cloning_jobs", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22676,12 +21745,8 @@ "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22689,12 +21754,8 @@ "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23103,12 +22164,8 @@ "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23118,58 +22175,42 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] }, "users_steward_user_id_unique": { "name": "users_steward_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_user_id" - ] + "columns": ["steward_user_id"] }, "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "users_discord_id_unique": { "name": "users_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "users_whatsapp_id_unique": { "name": "users_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] }, "users_phone_number_unique": { "name": "users_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] } }, "policies": {}, @@ -23424,12 +22465,8 @@ "name": "vertex_model_assignments_organization_id_organizations_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23437,12 +22474,8 @@ "name": "vertex_model_assignments_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23450,12 +22483,8 @@ "name": "vertex_model_assignments_tuned_model_id_vertex_tuned_models_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "vertex_tuned_models", - "columnsFrom": [ - "tuned_model_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuned_model_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23463,12 +22492,8 @@ "name": "vertex_model_assignments_assigned_by_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -23683,12 +22708,8 @@ "name": "vertex_tuned_models_tuning_job_id_vertex_tuning_jobs_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "vertex_tuning_jobs", - "columnsFrom": [ - "tuning_job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuning_job_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -23696,12 +22717,8 @@ "name": "vertex_tuned_models_organization_id_organizations_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23709,12 +22726,8 @@ "name": "vertex_tuned_models_user_id_users_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -24034,12 +23047,8 @@ "name": "vertex_tuning_jobs_organization_id_organizations_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24047,12 +23056,8 @@ "name": "vertex_tuning_jobs_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24060,12 +23065,8 @@ "name": "vertex_tuning_jobs_created_by_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -24210,9 +23211,7 @@ "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", "nullsNotDistinct": false, - "columns": [ - "event_id" - ] + "columns": ["event_id"] } }, "policies": {}, @@ -24224,133 +23223,67 @@ "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "whatsapp", - "other" - ] + "values": ["twilio", "blooio", "vonage", "whatsapp", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage", - "whatsapp" - ] + "values": ["sms", "voice", "both", "imessage", "whatsapp"] }, "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -24400,73 +23333,37 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -24491,11 +23388,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -24512,32 +23405,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -24555,12 +23433,7 @@ "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -24578,22 +23451,12 @@ "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.vertex_tuning_job_state": { "name": "vertex_tuning_job_state", @@ -24609,11 +23472,7 @@ "public.vertex_tuning_scope": { "name": "vertex_tuning_scope", "schema": "public", - "values": [ - "global", - "organization", - "user" - ] + "values": ["global", "organization", "user"] }, "public.vertex_tuning_slot": { "name": "vertex_tuning_slot", @@ -24638,4 +23497,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0063_snapshot.json b/packages/db/migrations/meta/0063_snapshot.json index cea14b0b6..dff8cd46a 100644 --- a/packages/db/migrations/meta/0063_snapshot.json +++ b/packages/db/migrations/meta/0063_snapshot.json @@ -180,12 +180,8 @@ "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -193,12 +189,8 @@ "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", "tableTo": "users", - "columnsFrom": [ - "connected_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -206,12 +198,8 @@ "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "access_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["access_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -219,12 +207,8 @@ "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "refresh_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refresh_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -531,12 +515,8 @@ "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -544,12 +524,8 @@ "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", "tableTo": "ad_accounts", - "columnsFrom": [ - "ad_account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ad_account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -557,12 +533,8 @@ "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -756,12 +728,8 @@ "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -956,12 +924,8 @@ "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -969,12 +933,8 @@ "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -982,12 +942,8 @@ "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1142,12 +1098,8 @@ "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1155,12 +1107,8 @@ "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "granted_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1170,9 +1118,7 @@ "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -1274,12 +1220,8 @@ "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1287,12 +1229,8 @@ "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", "tableTo": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_referral_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1302,9 +1240,7 @@ "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -1384,12 +1320,8 @@ "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1397,12 +1329,8 @@ "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", "tableTo": "affiliate_codes", - "columnsFrom": [ - "affiliate_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["affiliate_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1578,12 +1506,8 @@ "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "agent_budgets", - "columnsFrom": [ - "budget_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["budget_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1591,12 +1515,8 @@ "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1802,12 +1722,8 @@ "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1815,12 +1731,8 @@ "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", "tableTo": "organizations", - "columnsFrom": [ - "owner_org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_org_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1830,9 +1742,7 @@ "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "agent_id" - ] + "columns": ["agent_id"] } }, "policies": {}, @@ -2011,12 +1921,8 @@ "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2024,12 +1930,8 @@ "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2294,12 +2196,8 @@ "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2525,12 +2423,8 @@ "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", "tableTo": "agent_phone_numbers", - "columnsFrom": [ - "phone_number_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["phone_number_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2755,12 +2649,8 @@ "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2768,12 +2658,8 @@ "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2781,12 +2667,8 @@ "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2796,9 +2678,7 @@ "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", "nullsNotDistinct": false, - "columns": [ - "client_address" - ] + "columns": ["client_address"] } }, "policies": {}, @@ -2878,9 +2758,7 @@ "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", "nullsNotDistinct": false, - "columns": [ - "priority" - ] + "columns": ["priority"] } }, "policies": {}, @@ -3090,12 +2968,8 @@ "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3105,9 +2979,7 @@ "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -3304,12 +3176,8 @@ "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3317,12 +3185,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3332,16 +3196,12 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", "nullsNotDistinct": false, - "columns": [ - "key_hash" - ] + "columns": ["key_hash"] } }, "policies": {}, @@ -3465,12 +3325,8 @@ "name": "app_billing_app_id_apps_id_fk", "tableFrom": "app_billing", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3480,9 +3336,7 @@ "app_billing_app_id_unique": { "name": "app_billing_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3591,12 +3445,8 @@ "name": "app_config_app_id_apps_id_fk", "tableFrom": "app_config", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3606,9 +3456,7 @@ "app_config_app_id_unique": { "name": "app_config_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3753,12 +3601,8 @@ "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3766,12 +3610,8 @@ "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3779,12 +3619,8 @@ "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3903,12 +3739,8 @@ "name": "app_databases_app_id_apps_id_fk", "tableFrom": "app_databases", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3918,9 +3750,7 @@ "app_databases_app_id_unique": { "name": "app_databases_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -4090,12 +3920,8 @@ "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4215,12 +4041,8 @@ "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4360,12 +4182,8 @@ "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4373,12 +4191,8 @@ "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4501,12 +4315,8 @@ "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4780,12 +4590,8 @@ "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4793,12 +4599,8 @@ "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4806,12 +4608,8 @@ "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4821,9 +4619,7 @@ "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", "nullsNotDistinct": false, - "columns": [ - "sandbox_id" - ] + "columns": ["sandbox_id"] } }, "policies": {}, @@ -5008,9 +4804,7 @@ "app_templates_slug_unique": { "name": "app_templates_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -5172,9 +4966,7 @@ "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", "nullsNotDistinct": false, - "columns": [ - "snapshot_id" - ] + "columns": ["snapshot_id"] } }, "policies": {}, @@ -5296,12 +5088,8 @@ "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5403,12 +5191,8 @@ "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5645,12 +5429,8 @@ "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5885,12 +5665,8 @@ "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5898,12 +5674,8 @@ "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6070,12 +5842,8 @@ "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6083,12 +5851,8 @@ "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6512,12 +6276,8 @@ "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6525,12 +6285,8 @@ "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6540,23 +6296,17 @@ "apps_slug_unique": { "name": "apps_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", "nullsNotDistinct": false, - "columns": [ - "api_key_id" - ] + "columns": ["api_key_id"] }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", "nullsNotDistinct": false, - "columns": [ - "affiliate_code" - ] + "columns": ["affiliate_code"] } }, "policies": {}, @@ -6699,12 +6449,8 @@ "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6714,9 +6460,7 @@ "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -6858,12 +6602,8 @@ "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6871,12 +6611,8 @@ "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6884,12 +6620,8 @@ "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7338,12 +7070,8 @@ "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7351,12 +7079,8 @@ "name": "containers_user_id_users_id_fk", "tableFrom": "containers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7364,12 +7088,8 @@ "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7377,12 +7097,8 @@ "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7537,12 +7253,8 @@ "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7550,12 +7262,8 @@ "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7717,12 +7425,8 @@ "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7730,12 +7434,8 @@ "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7882,9 +7582,7 @@ "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_price_id" - ] + "columns": ["stripe_price_id"] } }, "policies": {}, @@ -8035,12 +7733,8 @@ "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8048,12 +7742,8 @@ "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8323,12 +8013,8 @@ "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8336,12 +8022,8 @@ "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8625,12 +8307,8 @@ "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8896,12 +8574,8 @@ "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8909,12 +8583,8 @@ "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9059,12 +8729,8 @@ "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9227,9 +8893,7 @@ "docker_nodes_node_id_unique": { "name": "docker_nodes_node_id_unique", "nullsNotDistinct": false, - "columns": [ - "node_id" - ] + "columns": ["node_id"] } }, "policies": {}, @@ -9407,12 +9071,8 @@ "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9420,10 +9080,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -9454,12 +9111,8 @@ "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9467,10 +9120,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -9551,12 +9201,8 @@ "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", "tableTo": "message_servers", - "columnsFrom": [ - "message_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9635,12 +9281,8 @@ "name": "components_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9648,12 +9290,8 @@ "name": "components_agent_id_agents_id_fk", "tableFrom": "components", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9661,12 +9299,8 @@ "name": "components_room_id_rooms_id_fk", "tableFrom": "components", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9674,12 +9308,8 @@ "name": "components_world_id_worlds_id_fk", "tableFrom": "components", "tableTo": "worlds", - "columnsFrom": [ - "world_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["world_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9687,12 +9317,8 @@ "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9786,12 +9412,8 @@ "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9799,12 +9421,8 @@ "name": "fk_embedding_memory", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9864,12 +9482,8 @@ "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9879,10 +9493,7 @@ "id_agent_id_unique": { "name": "id_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "id", - "agent_id" - ] + "columns": ["id", "agent_id"] } }, "policies": {}, @@ -9938,12 +9549,8 @@ "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9951,12 +9558,8 @@ "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9964,12 +9567,8 @@ "name": "fk_room", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9977,12 +9576,8 @@ "name": "fk_user", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10425,12 +10020,8 @@ "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10438,12 +10029,8 @@ "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10451,12 +10038,8 @@ "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10464,12 +10047,8 @@ "name": "fk_room", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10477,12 +10056,8 @@ "name": "fk_user", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10490,12 +10065,8 @@ "name": "fk_agent", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10651,12 +10222,8 @@ "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10664,12 +10231,8 @@ "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", "tableTo": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10760,12 +10323,8 @@ "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10773,12 +10332,8 @@ "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10786,12 +10341,8 @@ "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10799,12 +10350,8 @@ "name": "fk_room", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10812,12 +10359,8 @@ "name": "fk_user", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10905,12 +10448,8 @@ "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10918,12 +10457,8 @@ "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10931,12 +10466,8 @@ "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10944,12 +10475,8 @@ "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10957,12 +10484,8 @@ "name": "fk_user_b", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10972,11 +10495,7 @@ "unique_relationship": { "name": "unique_relationship", "nullsNotDistinct": false, - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ] + "columns": ["source_entity_id", "target_entity_id", "agent_id"] } }, "policies": {}, @@ -11056,12 +10575,8 @@ "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11306,12 +10821,8 @@ "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11371,12 +10882,8 @@ "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11430,12 +10937,8 @@ "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11605,12 +11108,8 @@ "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11965,12 +11464,8 @@ "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11978,12 +11473,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11991,12 +11482,8 @@ "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12004,12 +11491,8 @@ "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12095,9 +11578,7 @@ "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -12281,9 +11762,7 @@ "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_invoice_id" - ] + "columns": ["stripe_invoice_id"] } }, "policies": {}, @@ -12489,12 +11968,8 @@ "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12502,12 +11977,8 @@ "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12515,12 +11986,8 @@ "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12528,12 +11995,8 @@ "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", "tableTo": "generations", - "columnsFrom": [ - "generation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -12782,12 +12245,8 @@ "name": "llm_trajectories_organization_id_organizations_id_fk", "tableFrom": "llm_trajectories", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12795,12 +12254,8 @@ "name": "llm_trajectories_user_id_users_id_fk", "tableFrom": "llm_trajectories", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12808,12 +12263,8 @@ "name": "llm_trajectories_api_key_id_api_keys_id_fk", "tableFrom": "llm_trajectories", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13277,12 +12728,8 @@ "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13290,12 +12737,8 @@ "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13303,12 +12746,8 @@ "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13316,12 +12755,8 @@ "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13329,12 +12764,8 @@ "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13344,9 +12775,7 @@ "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", "nullsNotDistinct": false, - "columns": [ - "domain" - ] + "columns": ["domain"] } }, "policies": {}, @@ -13472,12 +12901,8 @@ "name": "milady_pairing_tokens_organization_id_organizations_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13485,12 +12910,8 @@ "name": "milady_pairing_tokens_user_id_users_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13498,12 +12919,8 @@ "name": "milady_pairing_tokens_agent_id_milady_sandboxes_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13513,9 +12930,7 @@ "milady_pairing_tokens_token_hash_unique": { "name": "milady_pairing_tokens_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -13608,12 +13023,8 @@ "name": "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk", "tableFrom": "milady_sandbox_backups", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "sandbox_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_record_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13946,12 +13357,8 @@ "name": "milady_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13959,12 +13366,8 @@ "name": "milady_sandboxes_user_id_users_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13972,12 +13375,8 @@ "name": "milady_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14263,12 +13662,8 @@ "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14276,12 +13671,8 @@ "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14450,12 +13841,8 @@ "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14463,12 +13850,8 @@ "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "banned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14478,9 +13861,7 @@ "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -14555,12 +13936,8 @@ "name": "org_rate_limit_overrides_organization_id_organizations_id_fk", "tableFrom": "org_rate_limit_overrides", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14570,9 +13947,7 @@ "org_rate_limit_overrides_organization_id_unique": { "name": "org_rate_limit_overrides_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14731,12 +14106,8 @@ "name": "organization_billing_organization_id_organizations_id_fk", "tableFrom": "organization_billing", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14746,9 +14117,7 @@ "organization_billing_organization_id_unique": { "name": "organization_billing_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14855,12 +14224,8 @@ "name": "organization_config_organization_id_organizations_id_fk", "tableFrom": "organization_config", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14870,9 +14235,7 @@ "organization_config_organization_id_unique": { "name": "organization_config_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -14952,12 +14315,8 @@ "name": "organization_encryption_keys_organization_id_organizations_id_fk", "tableFrom": "organization_encryption_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14967,9 +14326,7 @@ "organization_encryption_keys_org_unique": { "name": "organization_encryption_keys_org_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15124,12 +14481,8 @@ "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15137,12 +14490,8 @@ "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "inviter_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15150,12 +14499,8 @@ "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15165,9 +14510,7 @@ "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -15311,16 +14654,12 @@ "organizations_slug_unique": { "name": "organizations_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "organizations_steward_tenant_id_unique": { "name": "organizations_steward_tenant_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_tenant_id" - ] + "columns": ["steward_tenant_id"] } }, "policies": {}, @@ -15532,12 +14871,8 @@ "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15545,12 +14880,8 @@ "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15558,12 +14889,8 @@ "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "users", - "columnsFrom": [ - "requesting_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requesting_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -15571,12 +14898,8 @@ "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "platform_credentials", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15586,9 +14909,7 @@ "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -15898,12 +15219,8 @@ "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15911,12 +15228,8 @@ "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15924,12 +15237,8 @@ "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16188,12 +15497,8 @@ "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16203,9 +15508,7 @@ "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -16391,12 +15694,8 @@ "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16482,9 +15781,7 @@ "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", "nullsNotDistinct": false, - "columns": [ - "ledger_entry_id" - ] + "columns": ["ledger_entry_id"] } }, "policies": {}, @@ -16600,12 +15897,8 @@ "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16615,9 +15908,7 @@ "referral_codes_code_unique": { "name": "referral_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -16766,12 +16057,8 @@ "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16779,12 +16066,8 @@ "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referrer_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referrer_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16792,12 +16075,8 @@ "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referred_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referred_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16805,12 +16084,8 @@ "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "app_owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_owner_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -16818,12 +16093,8 @@ "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16963,12 +16234,8 @@ "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17186,12 +16453,8 @@ "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17199,12 +16462,8 @@ "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "users", - "columnsFrom": [ - "approved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -17501,12 +16760,8 @@ "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17514,12 +16769,8 @@ "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17903,12 +17154,8 @@ "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17916,12 +17163,8 @@ "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", "tableTo": "secrets", - "columnsFrom": [ - "secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["secret_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17929,12 +17172,8 @@ "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18242,12 +17481,8 @@ "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18255,12 +17490,8 @@ "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18353,12 +17584,8 @@ "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18511,12 +17738,8 @@ "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18758,12 +17981,8 @@ "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18771,12 +17990,8 @@ "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18784,12 +17999,8 @@ "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -18797,12 +18008,8 @@ "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19035,12 +18242,8 @@ "name": "service_pricing_audit_service_pricing_id_service_pricing_id_fk", "tableFrom": "service_pricing_audit", "tableTo": "service_pricing", - "columnsFrom": [ - "service_pricing_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_pricing_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19158,12 +18361,8 @@ "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19349,12 +18548,8 @@ "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19654,12 +18849,8 @@ "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19667,12 +18858,8 @@ "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19680,12 +18867,8 @@ "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -19852,12 +19035,8 @@ "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20147,12 +19326,8 @@ "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20160,12 +19335,8 @@ "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -20173,12 +19344,8 @@ "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -20727,12 +19894,8 @@ "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20740,12 +19903,8 @@ "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20755,9 +19914,7 @@ "user_characters_username_unique": { "name": "user_characters_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -21057,12 +20214,8 @@ "name": "user_identities_user_id_users_id_fk", "tableFrom": "user_identities", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21072,58 +20225,42 @@ "user_identities_user_id_unique": { "name": "user_identities_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] }, "user_identities_steward_user_id_unique": { "name": "user_identities_steward_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_user_id" - ] + "columns": ["steward_user_id"] }, "user_identities_privy_user_id_unique": { "name": "user_identities_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "user_identities_anonymous_session_id_unique": { "name": "user_identities_anonymous_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "anonymous_session_id" - ] + "columns": ["anonymous_session_id"] }, "user_identities_telegram_id_unique": { "name": "user_identities_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "user_identities_phone_number_unique": { "name": "user_identities_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] }, "user_identities_discord_id_unique": { "name": "user_identities_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "user_identities_whatsapp_id_unique": { "name": "user_identities_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] } }, "policies": {}, @@ -21310,12 +20447,8 @@ "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21323,12 +20456,8 @@ "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21336,12 +20465,8 @@ "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21824,12 +20949,8 @@ "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21837,12 +20958,8 @@ "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21850,12 +20967,8 @@ "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -21863,12 +20976,8 @@ "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "verified_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["verified_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -21980,12 +21089,8 @@ "name": "user_preferences_user_id_users_id_fk", "tableFrom": "user_preferences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21995,9 +21100,7 @@ "user_preferences_user_id_unique": { "name": "user_preferences_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -22190,12 +21293,8 @@ "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22203,12 +21302,8 @@ "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22218,9 +21313,7 @@ "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -22433,12 +21526,8 @@ "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22446,12 +21535,8 @@ "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22461,9 +21546,7 @@ "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", "nullsNotDistinct": false, - "columns": [ - "elevenlabs_voice_id" - ] + "columns": ["elevenlabs_voice_id"] } }, "policies": {}, @@ -22590,12 +21673,8 @@ "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22603,12 +21682,8 @@ "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22616,12 +21691,8 @@ "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -22742,12 +21813,8 @@ "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22755,12 +21822,8 @@ "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", "tableTo": "voice_cloning_jobs", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22768,12 +21831,8 @@ "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22781,12 +21840,8 @@ "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23195,12 +22250,8 @@ "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23210,58 +22261,42 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] }, "users_steward_user_id_unique": { "name": "users_steward_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_user_id" - ] + "columns": ["steward_user_id"] }, "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "users_discord_id_unique": { "name": "users_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "users_whatsapp_id_unique": { "name": "users_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] }, "users_phone_number_unique": { "name": "users_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] } }, "policies": {}, @@ -23516,12 +22551,8 @@ "name": "vertex_model_assignments_organization_id_organizations_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23529,12 +22560,8 @@ "name": "vertex_model_assignments_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23542,12 +22569,8 @@ "name": "vertex_model_assignments_tuned_model_id_vertex_tuned_models_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "vertex_tuned_models", - "columnsFrom": [ - "tuned_model_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuned_model_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23555,12 +22578,8 @@ "name": "vertex_model_assignments_assigned_by_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -23775,12 +22794,8 @@ "name": "vertex_tuned_models_tuning_job_id_vertex_tuning_jobs_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "vertex_tuning_jobs", - "columnsFrom": [ - "tuning_job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuning_job_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -23788,12 +22803,8 @@ "name": "vertex_tuned_models_organization_id_organizations_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23801,12 +22812,8 @@ "name": "vertex_tuned_models_user_id_users_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -24126,12 +23133,8 @@ "name": "vertex_tuning_jobs_organization_id_organizations_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24139,12 +23142,8 @@ "name": "vertex_tuning_jobs_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24152,12 +23151,8 @@ "name": "vertex_tuning_jobs_created_by_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -24302,9 +23297,7 @@ "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", "nullsNotDistinct": false, - "columns": [ - "event_id" - ] + "columns": ["event_id"] } }, "policies": {}, @@ -24316,133 +23309,67 @@ "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "whatsapp", - "other" - ] + "values": ["twilio", "blooio", "vonage", "whatsapp", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage", - "whatsapp" - ] + "values": ["sms", "voice", "both", "imessage", "whatsapp"] }, "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -24492,73 +23419,37 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -24583,11 +23474,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -24604,32 +23491,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -24647,12 +23519,7 @@ "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -24670,22 +23537,12 @@ "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.vertex_tuning_job_state": { "name": "vertex_tuning_job_state", @@ -24701,11 +23558,7 @@ "public.vertex_tuning_scope": { "name": "vertex_tuning_scope", "schema": "public", - "values": [ - "global", - "organization", - "user" - ] + "values": ["global", "organization", "user"] }, "public.vertex_tuning_slot": { "name": "vertex_tuning_slot", @@ -24730,4 +23583,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/0064_snapshot.json b/packages/db/migrations/meta/0064_snapshot.json index 46770a1c8..571f9b4a2 100644 --- a/packages/db/migrations/meta/0064_snapshot.json +++ b/packages/db/migrations/meta/0064_snapshot.json @@ -180,12 +180,8 @@ "name": "ad_accounts_organization_id_organizations_id_fk", "tableFrom": "ad_accounts", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -193,12 +189,8 @@ "name": "ad_accounts_connected_by_user_id_users_id_fk", "tableFrom": "ad_accounts", "tableTo": "users", - "columnsFrom": [ - "connected_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connected_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -206,12 +198,8 @@ "name": "ad_accounts_access_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "access_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["access_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -219,12 +207,8 @@ "name": "ad_accounts_refresh_token_secret_id_secrets_id_fk", "tableFrom": "ad_accounts", "tableTo": "secrets", - "columnsFrom": [ - "refresh_token_secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["refresh_token_secret_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -531,12 +515,8 @@ "name": "ad_campaigns_organization_id_organizations_id_fk", "tableFrom": "ad_campaigns", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -544,12 +524,8 @@ "name": "ad_campaigns_ad_account_id_ad_accounts_id_fk", "tableFrom": "ad_campaigns", "tableTo": "ad_accounts", - "columnsFrom": [ - "ad_account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ad_account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -557,12 +533,8 @@ "name": "ad_campaigns_app_id_apps_id_fk", "tableFrom": "ad_campaigns", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -756,12 +728,8 @@ "name": "ad_creatives_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_creatives", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -956,12 +924,8 @@ "name": "ad_transactions_organization_id_organizations_id_fk", "tableFrom": "ad_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -969,12 +933,8 @@ "name": "ad_transactions_campaign_id_ad_campaigns_id_fk", "tableFrom": "ad_transactions", "tableTo": "ad_campaigns", - "columnsFrom": [ - "campaign_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -982,12 +942,8 @@ "name": "ad_transactions_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "ad_transactions", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1142,12 +1098,8 @@ "name": "admin_users_user_id_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1155,12 +1107,8 @@ "name": "admin_users_granted_by_users_id_fk", "tableFrom": "admin_users", "tableTo": "users", - "columnsFrom": [ - "granted_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["granted_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1170,9 +1118,7 @@ "admin_users_wallet_address_unique": { "name": "admin_users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] } }, "policies": {}, @@ -1671,12 +1617,8 @@ "name": "affiliate_codes_user_id_users_id_fk", "tableFrom": "affiliate_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1684,12 +1626,8 @@ "name": "affiliate_codes_parent_referral_id_affiliate_codes_id_fk", "tableFrom": "affiliate_codes", "tableTo": "affiliate_codes", - "columnsFrom": [ - "parent_referral_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_referral_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1699,9 +1637,7 @@ "affiliate_codes_code_unique": { "name": "affiliate_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -1781,12 +1717,8 @@ "name": "user_affiliates_user_id_users_id_fk", "tableFrom": "user_affiliates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1794,12 +1726,8 @@ "name": "user_affiliates_affiliate_code_id_affiliate_codes_id_fk", "tableFrom": "user_affiliates", "tableTo": "affiliate_codes", - "columnsFrom": [ - "affiliate_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["affiliate_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1975,12 +1903,8 @@ "name": "agent_budget_transactions_budget_id_agent_budgets_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "agent_budgets", - "columnsFrom": [ - "budget_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["budget_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1988,12 +1912,8 @@ "name": "agent_budget_transactions_agent_id_user_characters_id_fk", "tableFrom": "agent_budget_transactions", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2199,12 +2119,8 @@ "name": "agent_budgets_agent_id_user_characters_id_fk", "tableFrom": "agent_budgets", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2212,12 +2128,8 @@ "name": "agent_budgets_owner_org_id_organizations_id_fk", "tableFrom": "agent_budgets", "tableTo": "organizations", - "columnsFrom": [ - "owner_org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_org_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2227,9 +2139,7 @@ "agent_budgets_agent_id_unique": { "name": "agent_budgets_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "agent_id" - ] + "columns": ["agent_id"] } }, "policies": {}, @@ -2408,12 +2318,8 @@ "name": "agent_events_agent_id_user_characters_id_fk", "tableFrom": "agent_events", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2421,12 +2327,8 @@ "name": "agent_events_organization_id_organizations_id_fk", "tableFrom": "agent_events", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2691,12 +2593,8 @@ "name": "agent_phone_numbers_organization_id_organizations_id_fk", "tableFrom": "agent_phone_numbers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2922,12 +2820,8 @@ "name": "phone_message_log_phone_number_id_agent_phone_numbers_id_fk", "tableFrom": "phone_message_log", "tableTo": "agent_phone_numbers", - "columnsFrom": [ - "phone_number_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["phone_number_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3152,12 +3046,8 @@ "name": "agent_server_wallets_organization_id_organizations_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3165,12 +3055,8 @@ "name": "agent_server_wallets_user_id_users_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3178,12 +3064,8 @@ "name": "agent_server_wallets_character_id_user_characters_id_fk", "tableFrom": "agent_server_wallets", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3193,9 +3075,7 @@ "agent_server_wallets_client_address_unique": { "name": "agent_server_wallets_client_address_unique", "nullsNotDistinct": false, - "columns": [ - "client_address" - ] + "columns": ["client_address"] } }, "policies": {}, @@ -3275,9 +3155,7 @@ "alb_priorities_priority_unique": { "name": "alb_priorities_priority_unique", "nullsNotDistinct": false, - "columns": [ - "priority" - ] + "columns": ["priority"] } }, "policies": {}, @@ -3487,12 +3365,8 @@ "name": "anonymous_sessions_user_id_users_id_fk", "tableFrom": "anonymous_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3502,9 +3376,7 @@ "anonymous_sessions_session_token_unique": { "name": "anonymous_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -3701,12 +3573,8 @@ "name": "api_keys_organization_id_organizations_id_fk", "tableFrom": "api_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3714,12 +3582,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3729,16 +3593,12 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] }, "api_keys_key_hash_unique": { "name": "api_keys_key_hash_unique", "nullsNotDistinct": false, - "columns": [ - "key_hash" - ] + "columns": ["key_hash"] } }, "policies": {}, @@ -3862,12 +3722,8 @@ "name": "app_billing_app_id_apps_id_fk", "tableFrom": "app_billing", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3877,9 +3733,7 @@ "app_billing_app_id_unique": { "name": "app_billing_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -3988,12 +3842,8 @@ "name": "app_config_app_id_apps_id_fk", "tableFrom": "app_config", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4003,9 +3853,7 @@ "app_config_app_id_unique": { "name": "app_config_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -4150,12 +3998,8 @@ "name": "app_credit_balances_app_id_apps_id_fk", "tableFrom": "app_credit_balances", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4163,12 +4007,8 @@ "name": "app_credit_balances_user_id_users_id_fk", "tableFrom": "app_credit_balances", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4176,12 +4016,8 @@ "name": "app_credit_balances_organization_id_organizations_id_fk", "tableFrom": "app_credit_balances", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4300,12 +4136,8 @@ "name": "app_databases_app_id_apps_id_fk", "tableFrom": "app_databases", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4315,9 +4147,7 @@ "app_databases_app_id_unique": { "name": "app_databases_app_id_unique", "nullsNotDistinct": false, - "columns": [ - "app_id" - ] + "columns": ["app_id"] } }, "policies": {}, @@ -4487,12 +4317,8 @@ "name": "app_domains_app_id_apps_id_fk", "tableFrom": "app_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4612,12 +4438,8 @@ "name": "app_earnings_app_id_apps_id_fk", "tableFrom": "app_earnings", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4757,12 +4579,8 @@ "name": "app_earnings_transactions_app_id_apps_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4770,12 +4588,8 @@ "name": "app_earnings_transactions_user_id_users_id_fk", "tableFrom": "app_earnings_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4898,12 +4712,8 @@ "name": "app_builder_prompts_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "app_builder_prompts", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5177,12 +4987,8 @@ "name": "app_sandbox_sessions_user_id_users_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5190,12 +4996,8 @@ "name": "app_sandbox_sessions_organization_id_organizations_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5203,12 +5005,8 @@ "name": "app_sandbox_sessions_app_id_apps_id_fk", "tableFrom": "app_sandbox_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5218,9 +5016,7 @@ "app_sandbox_sessions_sandbox_id_unique": { "name": "app_sandbox_sessions_sandbox_id_unique", "nullsNotDistinct": false, - "columns": [ - "sandbox_id" - ] + "columns": ["sandbox_id"] } }, "policies": {}, @@ -5405,9 +5201,7 @@ "app_templates_slug_unique": { "name": "app_templates_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -5569,9 +5363,7 @@ "sandbox_template_snapshots_snapshot_id_unique": { "name": "sandbox_template_snapshots_snapshot_id_unique", "nullsNotDistinct": false, - "columns": [ - "snapshot_id" - ] + "columns": ["snapshot_id"] } }, "policies": {}, @@ -5693,12 +5485,8 @@ "name": "session_file_snapshots_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_file_snapshots", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5800,12 +5588,8 @@ "name": "session_restore_history_sandbox_session_id_app_sandbox_sessions_id_fk", "tableFrom": "session_restore_history", "tableTo": "app_sandbox_sessions", - "columnsFrom": [ - "sandbox_session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6042,12 +5826,8 @@ "name": "app_analytics_app_id_apps_id_fk", "tableFrom": "app_analytics", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6282,12 +6062,8 @@ "name": "app_requests_app_id_apps_id_fk", "tableFrom": "app_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6295,12 +6071,8 @@ "name": "app_requests_user_id_users_id_fk", "tableFrom": "app_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6467,12 +6239,8 @@ "name": "app_users_app_id_apps_id_fk", "tableFrom": "app_users", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6480,12 +6248,8 @@ "name": "app_users_user_id_users_id_fk", "tableFrom": "app_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6909,12 +6673,8 @@ "name": "apps_organization_id_organizations_id_fk", "tableFrom": "apps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6922,12 +6682,8 @@ "name": "apps_created_by_user_id_users_id_fk", "tableFrom": "apps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6937,23 +6693,17 @@ "apps_slug_unique": { "name": "apps_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "apps_api_key_id_unique": { "name": "apps_api_key_id_unique", "nullsNotDistinct": false, - "columns": [ - "api_key_id" - ] + "columns": ["api_key_id"] }, "apps_affiliate_code_unique": { "name": "apps_affiliate_code_unique", "nullsNotDistinct": false, - "columns": [ - "affiliate_code" - ] + "columns": ["affiliate_code"] } }, "policies": {}, @@ -7096,12 +6846,8 @@ "name": "cli_auth_sessions_user_id_users_id_fk", "tableFrom": "cli_auth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7111,9 +6857,7 @@ "cli_auth_sessions_session_id_unique": { "name": "cli_auth_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -7255,12 +6999,8 @@ "name": "container_billing_records_container_id_containers_id_fk", "tableFrom": "container_billing_records", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7268,12 +7008,8 @@ "name": "container_billing_records_organization_id_organizations_id_fk", "tableFrom": "container_billing_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7281,12 +7017,8 @@ "name": "container_billing_records_credit_transaction_id_credit_transactions_id_fk", "tableFrom": "container_billing_records", "tableTo": "credit_transactions", - "columnsFrom": [ - "credit_transaction_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credit_transaction_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7735,12 +7467,8 @@ "name": "containers_organization_id_organizations_id_fk", "tableFrom": "containers", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7748,12 +7476,8 @@ "name": "containers_user_id_users_id_fk", "tableFrom": "containers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7761,12 +7485,8 @@ "name": "containers_api_key_id_api_keys_id_fk", "tableFrom": "containers", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7774,12 +7494,8 @@ "name": "containers_character_id_user_characters_id_fk", "tableFrom": "containers", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7934,12 +7650,8 @@ "name": "conversation_messages_conversation_id_conversations_id_fk", "tableFrom": "conversation_messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7947,12 +7659,8 @@ "name": "conversation_messages_usage_record_id_usage_records_id_fk", "tableFrom": "conversation_messages", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8114,12 +7822,8 @@ "name": "conversations_organization_id_organizations_id_fk", "tableFrom": "conversations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8127,12 +7831,8 @@ "name": "conversations_user_id_users_id_fk", "tableFrom": "conversations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8279,9 +7979,7 @@ "credit_packs_stripe_price_id_unique": { "name": "credit_packs_stripe_price_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_price_id" - ] + "columns": ["stripe_price_id"] } }, "policies": {}, @@ -8432,12 +8130,8 @@ "name": "credit_transactions_organization_id_organizations_id_fk", "tableFrom": "credit_transactions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8445,12 +8139,8 @@ "name": "credit_transactions_user_id_users_id_fk", "tableFrom": "credit_transactions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8720,12 +8410,8 @@ "name": "crypto_payments_organization_id_organizations_id_fk", "tableFrom": "crypto_payments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8733,12 +8419,8 @@ "name": "crypto_payments_user_id_users_id_fk", "tableFrom": "crypto_payments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9022,12 +8704,8 @@ "name": "discord_channels_organization_id_organizations_id_fk", "tableFrom": "discord_channels", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9293,12 +8971,8 @@ "name": "discord_connections_organization_id_organizations_id_fk", "tableFrom": "discord_connections", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9306,12 +8980,8 @@ "name": "discord_connections_character_id_user_characters_id_fk", "tableFrom": "discord_connections", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9456,12 +9126,8 @@ "name": "discord_guilds_organization_id_organizations_id_fk", "tableFrom": "discord_guilds", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9624,9 +9290,7 @@ "docker_nodes_node_id_unique": { "name": "docker_nodes_node_id_unique", "nullsNotDistinct": false, - "columns": [ - "node_id" - ] + "columns": ["node_id"] } }, "policies": {}, @@ -9804,12 +9468,8 @@ "name": "cache_agent_id_agents_id_fk", "tableFrom": "cache", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9817,10 +9477,7 @@ "compositePrimaryKeys": { "cache_key_agent_id_pk": { "name": "cache_key_agent_id_pk", - "columns": [ - "key", - "agent_id" - ] + "columns": ["key", "agent_id"] } }, "uniqueConstraints": {}, @@ -9851,12 +9508,8 @@ "name": "channel_participants_channel_id_channels_id_fk", "tableFrom": "channel_participants", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9864,10 +9517,7 @@ "compositePrimaryKeys": { "channel_participants_channel_id_entity_id_pk": { "name": "channel_participants_channel_id_entity_id_pk", - "columns": [ - "channel_id", - "entity_id" - ] + "columns": ["channel_id", "entity_id"] } }, "uniqueConstraints": {}, @@ -9948,12 +9598,8 @@ "name": "channels_message_server_id_message_servers_id_fk", "tableFrom": "channels", "tableTo": "message_servers", - "columnsFrom": [ - "message_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10032,12 +9678,8 @@ "name": "components_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10045,12 +9687,8 @@ "name": "components_agent_id_agents_id_fk", "tableFrom": "components", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10058,12 +9696,8 @@ "name": "components_room_id_rooms_id_fk", "tableFrom": "components", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10071,12 +9705,8 @@ "name": "components_world_id_worlds_id_fk", "tableFrom": "components", "tableTo": "worlds", - "columnsFrom": [ - "world_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["world_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10084,12 +9714,8 @@ "name": "components_source_entity_id_entities_id_fk", "tableFrom": "components", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10183,12 +9809,8 @@ "name": "embeddings_memory_id_memories_id_fk", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10196,12 +9818,8 @@ "name": "fk_embedding_memory", "tableFrom": "embeddings", "tableTo": "memories", - "columnsFrom": [ - "memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10261,12 +9879,8 @@ "name": "entities_agent_id_agents_id_fk", "tableFrom": "entities", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10276,10 +9890,7 @@ "id_agent_id_unique": { "name": "id_agent_id_unique", "nullsNotDistinct": false, - "columns": [ - "id", - "agent_id" - ] + "columns": ["id", "agent_id"] } }, "policies": {}, @@ -10335,12 +9946,8 @@ "name": "logs_entity_id_entities_id_fk", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10348,12 +9955,8 @@ "name": "logs_room_id_rooms_id_fk", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10361,12 +9964,8 @@ "name": "fk_room", "tableFrom": "logs", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10374,12 +9973,8 @@ "name": "fk_user", "tableFrom": "logs", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10822,12 +10417,8 @@ "name": "memories_entity_id_entities_id_fk", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10835,12 +10426,8 @@ "name": "memories_agent_id_agents_id_fk", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10848,12 +10435,8 @@ "name": "memories_room_id_rooms_id_fk", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10861,12 +10444,8 @@ "name": "fk_room", "tableFrom": "memories", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10874,12 +10453,8 @@ "name": "fk_user", "tableFrom": "memories", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10887,12 +10462,8 @@ "name": "fk_agent", "tableFrom": "memories", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11048,12 +10619,8 @@ "name": "central_messages_channel_id_channels_id_fk", "tableFrom": "central_messages", "tableTo": "channels", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11061,12 +10628,8 @@ "name": "central_messages_in_reply_to_root_message_id_central_messages_id_fk", "tableFrom": "central_messages", "tableTo": "central_messages", - "columnsFrom": [ - "in_reply_to_root_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["in_reply_to_root_message_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -11157,12 +10720,8 @@ "name": "participants_entity_id_entities_id_fk", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11170,12 +10729,8 @@ "name": "participants_room_id_rooms_id_fk", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11183,12 +10738,8 @@ "name": "participants_agent_id_agents_id_fk", "tableFrom": "participants", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11196,12 +10747,8 @@ "name": "fk_room", "tableFrom": "participants", "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["room_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11209,12 +10756,8 @@ "name": "fk_user", "tableFrom": "participants", "tableTo": "entities", - "columnsFrom": [ - "entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11302,12 +10845,8 @@ "name": "relationships_source_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11315,12 +10854,8 @@ "name": "relationships_target_entity_id_entities_id_fk", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11328,12 +10863,8 @@ "name": "relationships_agent_id_agents_id_fk", "tableFrom": "relationships", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11341,12 +10872,8 @@ "name": "fk_user_a", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "source_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11354,12 +10881,8 @@ "name": "fk_user_b", "tableFrom": "relationships", "tableTo": "entities", - "columnsFrom": [ - "target_entity_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_entity_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11369,11 +10892,7 @@ "unique_relationship": { "name": "unique_relationship", "nullsNotDistinct": false, - "columns": [ - "source_entity_id", - "target_entity_id", - "agent_id" - ] + "columns": ["source_entity_id", "target_entity_id", "agent_id"] } }, "policies": {}, @@ -11453,12 +10972,8 @@ "name": "rooms_agent_id_agents_id_fk", "tableFrom": "rooms", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11703,12 +11218,8 @@ "name": "tasks_agent_id_agents_id_fk", "tableFrom": "tasks", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11768,12 +11279,8 @@ "name": "worlds_agent_id_agents_id_fk", "tableFrom": "worlds", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11827,12 +11334,8 @@ "name": "eliza_room_characters_character_id_user_characters_id_fk", "tableFrom": "eliza_room_characters", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12002,12 +11505,8 @@ "name": "entity_settings_user_id_users_id_fk", "tableFrom": "entity_settings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12362,12 +11861,8 @@ "name": "generations_organization_id_organizations_id_fk", "tableFrom": "generations", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12375,12 +11870,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12388,12 +11879,8 @@ "name": "generations_api_key_id_api_keys_id_fk", "tableFrom": "generations", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -12401,12 +11888,8 @@ "name": "generations_usage_record_id_usage_records_id_fk", "tableFrom": "generations", "tableTo": "usage_records", - "columnsFrom": [ - "usage_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["usage_record_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12492,9 +11975,7 @@ "idempotency_keys_key_unique": { "name": "idempotency_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -12678,9 +12159,7 @@ "invoices_stripe_invoice_id_unique": { "name": "invoices_stripe_invoice_id_unique", "nullsNotDistinct": false, - "columns": [ - "stripe_invoice_id" - ] + "columns": ["stripe_invoice_id"] } }, "policies": {}, @@ -12886,12 +12365,8 @@ "name": "jobs_organization_id_organizations_id_fk", "tableFrom": "jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12899,12 +12374,8 @@ "name": "jobs_user_id_users_id_fk", "tableFrom": "jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12912,12 +12383,8 @@ "name": "jobs_api_key_id_api_keys_id_fk", "tableFrom": "jobs", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -12925,12 +12392,8 @@ "name": "jobs_generation_id_generations_id_fk", "tableFrom": "jobs", "tableTo": "generations", - "columnsFrom": [ - "generation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -13179,12 +12642,8 @@ "name": "llm_trajectories_organization_id_organizations_id_fk", "tableFrom": "llm_trajectories", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13192,12 +12651,8 @@ "name": "llm_trajectories_user_id_users_id_fk", "tableFrom": "llm_trajectories", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13205,12 +12660,8 @@ "name": "llm_trajectories_api_key_id_api_keys_id_fk", "tableFrom": "llm_trajectories", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13674,12 +13125,8 @@ "name": "managed_domains_organization_id_organizations_id_fk", "tableFrom": "managed_domains", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13687,12 +13134,8 @@ "name": "managed_domains_app_id_apps_id_fk", "tableFrom": "managed_domains", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13700,12 +13143,8 @@ "name": "managed_domains_container_id_containers_id_fk", "tableFrom": "managed_domains", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13713,12 +13152,8 @@ "name": "managed_domains_agent_id_user_characters_id_fk", "tableFrom": "managed_domains", "tableTo": "user_characters", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -13726,12 +13161,8 @@ "name": "managed_domains_mcp_id_user_mcps_id_fk", "tableFrom": "managed_domains", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13741,9 +13172,7 @@ "managed_domains_domain_unique": { "name": "managed_domains_domain_unique", "nullsNotDistinct": false, - "columns": [ - "domain" - ] + "columns": ["domain"] } }, "policies": {}, @@ -13869,12 +13298,8 @@ "name": "milady_pairing_tokens_organization_id_organizations_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13882,12 +13307,8 @@ "name": "milady_pairing_tokens_user_id_users_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13895,12 +13316,8 @@ "name": "milady_pairing_tokens_agent_id_milady_sandboxes_id_fk", "tableFrom": "milady_pairing_tokens", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13910,9 +13327,7 @@ "milady_pairing_tokens_token_hash_unique": { "name": "milady_pairing_tokens_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -14005,12 +13420,8 @@ "name": "milady_sandbox_backups_sandbox_record_id_milady_sandboxes_id_fk", "tableFrom": "milady_sandbox_backups", "tableTo": "milady_sandboxes", - "columnsFrom": [ - "sandbox_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["sandbox_record_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14343,12 +13754,8 @@ "name": "milady_sandboxes_organization_id_organizations_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14356,12 +13763,8 @@ "name": "milady_sandboxes_user_id_users_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14369,12 +13772,8 @@ "name": "milady_sandboxes_character_id_user_characters_id_fk", "tableFrom": "milady_sandboxes", "tableTo": "user_characters", - "columnsFrom": [ - "character_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["character_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14660,12 +14059,8 @@ "name": "moderation_violations_user_id_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14673,12 +14068,8 @@ "name": "moderation_violations_reviewed_by_users_id_fk", "tableFrom": "moderation_violations", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14847,12 +14238,8 @@ "name": "user_moderation_status_user_id_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14860,12 +14247,8 @@ "name": "user_moderation_status_banned_by_users_id_fk", "tableFrom": "user_moderation_status", "tableTo": "users", - "columnsFrom": [ - "banned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -14875,9 +14258,7 @@ "user_moderation_status_user_id_unique": { "name": "user_moderation_status_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -14952,12 +14333,8 @@ "name": "org_rate_limit_overrides_organization_id_organizations_id_fk", "tableFrom": "org_rate_limit_overrides", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14967,9 +14344,7 @@ "org_rate_limit_overrides_organization_id_unique": { "name": "org_rate_limit_overrides_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15128,12 +14503,8 @@ "name": "organization_billing_organization_id_organizations_id_fk", "tableFrom": "organization_billing", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15143,9 +14514,7 @@ "organization_billing_organization_id_unique": { "name": "organization_billing_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15252,12 +14621,8 @@ "name": "organization_config_organization_id_organizations_id_fk", "tableFrom": "organization_config", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15267,9 +14632,7 @@ "organization_config_organization_id_unique": { "name": "organization_config_organization_id_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15349,12 +14712,8 @@ "name": "organization_encryption_keys_organization_id_organizations_id_fk", "tableFrom": "organization_encryption_keys", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15364,9 +14723,7 @@ "organization_encryption_keys_org_unique": { "name": "organization_encryption_keys_org_unique", "nullsNotDistinct": false, - "columns": [ - "organization_id" - ] + "columns": ["organization_id"] } }, "policies": {}, @@ -15521,12 +14878,8 @@ "name": "organization_invites_organization_id_organizations_id_fk", "tableFrom": "organization_invites", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15534,12 +14887,8 @@ "name": "organization_invites_inviter_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "inviter_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15547,12 +14896,8 @@ "name": "organization_invites_accepted_by_user_id_users_id_fk", "tableFrom": "organization_invites", "tableTo": "users", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15562,9 +14907,7 @@ "organization_invites_token_hash_unique": { "name": "organization_invites_token_hash_unique", "nullsNotDistinct": false, - "columns": [ - "token_hash" - ] + "columns": ["token_hash"] } }, "policies": {}, @@ -15708,16 +15051,12 @@ "organizations_slug_unique": { "name": "organizations_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] }, "organizations_steward_tenant_id_unique": { "name": "organizations_steward_tenant_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_tenant_id" - ] + "columns": ["steward_tenant_id"] } }, "policies": {}, @@ -15929,12 +15268,8 @@ "name": "platform_credential_sessions_organization_id_organizations_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15942,12 +15277,8 @@ "name": "platform_credential_sessions_app_id_apps_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15955,12 +15286,8 @@ "name": "platform_credential_sessions_requesting_user_id_users_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "users", - "columnsFrom": [ - "requesting_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requesting_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -15968,12 +15295,8 @@ "name": "platform_credential_sessions_credential_id_platform_credentials_id_fk", "tableFrom": "platform_credential_sessions", "tableTo": "platform_credentials", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -15983,9 +15306,7 @@ "platform_credential_sessions_session_id_unique": { "name": "platform_credential_sessions_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "session_id" - ] + "columns": ["session_id"] } }, "policies": {}, @@ -16295,12 +15616,8 @@ "name": "platform_credentials_organization_id_organizations_id_fk", "tableFrom": "platform_credentials", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16308,12 +15625,8 @@ "name": "platform_credentials_user_id_users_id_fk", "tableFrom": "platform_credentials", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16321,12 +15634,8 @@ "name": "platform_credentials_app_id_apps_id_fk", "tableFrom": "platform_credentials", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16585,12 +15894,8 @@ "name": "redeemable_earnings_user_id_users_id_fk", "tableFrom": "redeemable_earnings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16600,9 +15905,7 @@ "redeemable_earnings_user_id_unique": { "name": "redeemable_earnings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -16788,12 +16091,8 @@ "name": "redeemable_earnings_ledger_user_id_users_id_fk", "tableFrom": "redeemable_earnings_ledger", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16879,9 +16178,7 @@ "redeemed_earnings_tracking_ledger_entry_id_unique": { "name": "redeemed_earnings_tracking_ledger_entry_id_unique", "nullsNotDistinct": false, - "columns": [ - "ledger_entry_id" - ] + "columns": ["ledger_entry_id"] } }, "policies": {}, @@ -16997,12 +16294,8 @@ "name": "referral_codes_user_id_users_id_fk", "tableFrom": "referral_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17012,9 +16305,7 @@ "referral_codes_code_unique": { "name": "referral_codes_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -17163,12 +16454,8 @@ "name": "referral_signups_referral_code_id_referral_codes_id_fk", "tableFrom": "referral_signups", "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17176,12 +16463,8 @@ "name": "referral_signups_referrer_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referrer_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referrer_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17189,12 +16472,8 @@ "name": "referral_signups_referred_user_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "referred_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["referred_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17202,12 +16481,8 @@ "name": "referral_signups_app_owner_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "app_owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_owner_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -17215,12 +16490,8 @@ "name": "referral_signups_creator_id_users_id_fk", "tableFrom": "referral_signups", "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17360,12 +16631,8 @@ "name": "social_share_rewards_user_id_users_id_fk", "tableFrom": "social_share_rewards", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17583,12 +16850,8 @@ "name": "app_secret_requirements_app_id_apps_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17596,12 +16859,8 @@ "name": "app_secret_requirements_approved_by_users_id_fk", "tableFrom": "app_secret_requirements", "tableTo": "users", - "columnsFrom": [ - "approved_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -17898,12 +17157,8 @@ "name": "oauth_sessions_organization_id_organizations_id_fk", "tableFrom": "oauth_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17911,12 +17166,8 @@ "name": "oauth_sessions_user_id_users_id_fk", "tableFrom": "oauth_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18300,12 +17551,8 @@ "name": "secret_bindings_organization_id_organizations_id_fk", "tableFrom": "secret_bindings", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18313,12 +17560,8 @@ "name": "secret_bindings_secret_id_secrets_id_fk", "tableFrom": "secret_bindings", "tableTo": "secrets", - "columnsFrom": [ - "secret_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["secret_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18326,12 +17569,8 @@ "name": "secret_bindings_created_by_users_id_fk", "tableFrom": "secret_bindings", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18639,12 +17878,8 @@ "name": "secrets_organization_id_organizations_id_fk", "tableFrom": "secrets", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -18652,12 +17887,8 @@ "name": "secrets_created_by_users_id_fk", "tableFrom": "secrets", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -18750,12 +17981,8 @@ "name": "seo_artifacts_request_id_seo_requests_id_fk", "tableFrom": "seo_artifacts", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -18908,12 +18135,8 @@ "name": "seo_provider_calls_request_id_seo_requests_id_fk", "tableFrom": "seo_provider_calls", "tableTo": "seo_requests", - "columnsFrom": [ - "request_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["request_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19155,12 +18378,8 @@ "name": "seo_requests_organization_id_organizations_id_fk", "tableFrom": "seo_requests", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -19168,12 +18387,8 @@ "name": "seo_requests_app_id_apps_id_fk", "tableFrom": "seo_requests", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19181,12 +18396,8 @@ "name": "seo_requests_user_id_users_id_fk", "tableFrom": "seo_requests", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -19194,12 +18405,8 @@ "name": "seo_requests_api_key_id_api_keys_id_fk", "tableFrom": "seo_requests", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19432,12 +18639,8 @@ "name": "service_pricing_audit_service_pricing_id_service_pricing_id_fk", "tableFrom": "service_pricing_audit", "tableTo": "service_pricing", - "columnsFrom": [ - "service_pricing_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_pricing_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -19555,12 +18758,8 @@ "name": "telegram_chats_organization_id_organizations_id_fk", "tableFrom": "telegram_chats", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -19746,12 +18945,8 @@ "name": "redemption_limits_user_id_users_id_fk", "tableFrom": "redemption_limits", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20051,12 +19246,8 @@ "name": "token_redemptions_user_id_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20064,12 +19255,8 @@ "name": "token_redemptions_app_id_apps_id_fk", "tableFrom": "token_redemptions", "tableTo": "apps", - "columnsFrom": [ - "app_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["app_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -20077,12 +19264,8 @@ "name": "token_redemptions_reviewed_by_users_id_fk", "tableFrom": "token_redemptions", "tableTo": "users", - "columnsFrom": [ - "reviewed_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -20249,12 +19432,8 @@ "name": "usage_quotas_organization_id_organizations_id_fk", "tableFrom": "usage_quotas", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -20544,12 +19723,8 @@ "name": "usage_records_organization_id_organizations_id_fk", "tableFrom": "usage_records", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -20557,12 +19732,8 @@ "name": "usage_records_user_id_users_id_fk", "tableFrom": "usage_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -20570,12 +19741,8 @@ "name": "usage_records_api_key_id_api_keys_id_fk", "tableFrom": "usage_records", "tableTo": "api_keys", - "columnsFrom": [ - "api_key_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["api_key_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -21124,12 +20291,8 @@ "name": "user_characters_organization_id_organizations_id_fk", "tableFrom": "user_characters", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21137,12 +20300,8 @@ "name": "user_characters_user_id_users_id_fk", "tableFrom": "user_characters", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21152,9 +20311,7 @@ "user_characters_username_unique": { "name": "user_characters_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -21454,12 +20611,8 @@ "name": "user_identities_user_id_users_id_fk", "tableFrom": "user_identities", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -21469,58 +20622,42 @@ "user_identities_user_id_unique": { "name": "user_identities_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] }, "user_identities_steward_user_id_unique": { "name": "user_identities_steward_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_user_id" - ] + "columns": ["steward_user_id"] }, "user_identities_privy_user_id_unique": { "name": "user_identities_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "user_identities_anonymous_session_id_unique": { "name": "user_identities_anonymous_session_id_unique", "nullsNotDistinct": false, - "columns": [ - "anonymous_session_id" - ] + "columns": ["anonymous_session_id"] }, "user_identities_telegram_id_unique": { "name": "user_identities_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "user_identities_phone_number_unique": { "name": "user_identities_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] }, "user_identities_discord_id_unique": { "name": "user_identities_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "user_identities_whatsapp_id_unique": { "name": "user_identities_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] } }, "policies": {}, @@ -21707,12 +20844,8 @@ "name": "mcp_usage_mcp_id_user_mcps_id_fk", "tableFrom": "mcp_usage", "tableTo": "user_mcps", - "columnsFrom": [ - "mcp_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21720,12 +20853,8 @@ "name": "mcp_usage_organization_id_organizations_id_fk", "tableFrom": "mcp_usage", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -21733,12 +20862,8 @@ "name": "mcp_usage_user_id_users_id_fk", "tableFrom": "mcp_usage", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -22221,12 +21346,8 @@ "name": "user_mcps_organization_id_organizations_id_fk", "tableFrom": "user_mcps", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22234,12 +21355,8 @@ "name": "user_mcps_created_by_user_id_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22247,12 +21364,8 @@ "name": "user_mcps_container_id_containers_id_fk", "tableFrom": "user_mcps", "tableTo": "containers", - "columnsFrom": [ - "container_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["container_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -22260,12 +21373,8 @@ "name": "user_mcps_verified_by_users_id_fk", "tableFrom": "user_mcps", "tableTo": "users", - "columnsFrom": [ - "verified_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["verified_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -22377,12 +21486,8 @@ "name": "user_preferences_user_id_users_id_fk", "tableFrom": "user_preferences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22392,9 +21497,7 @@ "user_preferences_user_id_unique": { "name": "user_preferences_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -22587,12 +21690,8 @@ "name": "user_sessions_user_id_users_id_fk", "tableFrom": "user_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22600,12 +21699,8 @@ "name": "user_sessions_organization_id_organizations_id_fk", "tableFrom": "user_sessions", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22615,9 +21710,7 @@ "user_sessions_session_token_unique": { "name": "user_sessions_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -22830,12 +21923,8 @@ "name": "user_voices_organization_id_organizations_id_fk", "tableFrom": "user_voices", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -22843,12 +21932,8 @@ "name": "user_voices_user_id_users_id_fk", "tableFrom": "user_voices", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -22858,9 +21943,7 @@ "user_voices_elevenlabs_voice_id_unique": { "name": "user_voices_elevenlabs_voice_id_unique", "nullsNotDistinct": false, - "columns": [ - "elevenlabs_voice_id" - ] + "columns": ["elevenlabs_voice_id"] } }, "policies": {}, @@ -22987,12 +22070,8 @@ "name": "voice_cloning_jobs_organization_id_organizations_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23000,12 +22079,8 @@ "name": "voice_cloning_jobs_user_id_users_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23013,12 +22088,8 @@ "name": "voice_cloning_jobs_user_voice_id_user_voices_id_fk", "tableFrom": "voice_cloning_jobs", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -23139,12 +22210,8 @@ "name": "voice_samples_user_voice_id_user_voices_id_fk", "tableFrom": "voice_samples", "tableTo": "user_voices", - "columnsFrom": [ - "user_voice_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_voice_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23152,12 +22219,8 @@ "name": "voice_samples_job_id_voice_cloning_jobs_id_fk", "tableFrom": "voice_samples", "tableTo": "voice_cloning_jobs", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23165,12 +22228,8 @@ "name": "voice_samples_organization_id_organizations_id_fk", "tableFrom": "voice_samples", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23178,12 +22237,8 @@ "name": "voice_samples_user_id_users_id_fk", "tableFrom": "voice_samples", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23592,12 +22647,8 @@ "name": "users_organization_id_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -23607,58 +22658,42 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "users_wallet_address_unique": { "name": "users_wallet_address_unique", "nullsNotDistinct": false, - "columns": [ - "wallet_address" - ] + "columns": ["wallet_address"] }, "users_steward_user_id_unique": { "name": "users_steward_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "steward_user_id" - ] + "columns": ["steward_user_id"] }, "users_privy_user_id_unique": { "name": "users_privy_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "privy_user_id" - ] + "columns": ["privy_user_id"] }, "users_telegram_id_unique": { "name": "users_telegram_id_unique", "nullsNotDistinct": false, - "columns": [ - "telegram_id" - ] + "columns": ["telegram_id"] }, "users_discord_id_unique": { "name": "users_discord_id_unique", "nullsNotDistinct": false, - "columns": [ - "discord_id" - ] + "columns": ["discord_id"] }, "users_whatsapp_id_unique": { "name": "users_whatsapp_id_unique", "nullsNotDistinct": false, - "columns": [ - "whatsapp_id" - ] + "columns": ["whatsapp_id"] }, "users_phone_number_unique": { "name": "users_phone_number_unique", "nullsNotDistinct": false, - "columns": [ - "phone_number" - ] + "columns": ["phone_number"] } }, "policies": {}, @@ -23913,12 +22948,8 @@ "name": "vertex_model_assignments_organization_id_organizations_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23926,12 +22957,8 @@ "name": "vertex_model_assignments_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23939,12 +22966,8 @@ "name": "vertex_model_assignments_tuned_model_id_vertex_tuned_models_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "vertex_tuned_models", - "columnsFrom": [ - "tuned_model_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuned_model_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -23952,12 +22975,8 @@ "name": "vertex_model_assignments_assigned_by_user_id_users_id_fk", "tableFrom": "vertex_model_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -24172,12 +23191,8 @@ "name": "vertex_tuned_models_tuning_job_id_vertex_tuning_jobs_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "vertex_tuning_jobs", - "columnsFrom": [ - "tuning_job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tuning_job_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -24185,12 +23200,8 @@ "name": "vertex_tuned_models_organization_id_organizations_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24198,12 +23209,8 @@ "name": "vertex_tuned_models_user_id_users_id_fk", "tableFrom": "vertex_tuned_models", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -24523,12 +23530,8 @@ "name": "vertex_tuning_jobs_organization_id_organizations_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24536,12 +23539,8 @@ "name": "vertex_tuning_jobs_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -24549,12 +23548,8 @@ "name": "vertex_tuning_jobs_created_by_user_id_users_id_fk", "tableFrom": "vertex_tuning_jobs", "tableTo": "users", - "columnsFrom": [ - "created_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -24699,9 +23694,7 @@ "webhook_events_event_id_unique": { "name": "webhook_events_event_id_unique", "nullsNotDistinct": false, - "columns": [ - "event_id" - ] + "columns": ["event_id"] } }, "policies": {}, @@ -24713,133 +23706,67 @@ "public.admin_role": { "name": "admin_role", "schema": "public", - "values": [ - "super_admin", - "moderator", - "viewer" - ] + "values": ["super_admin", "moderator", "viewer"] }, "public.phone_provider": { "name": "phone_provider", "schema": "public", - "values": [ - "twilio", - "blooio", - "vonage", - "whatsapp", - "other" - ] + "values": ["twilio", "blooio", "vonage", "whatsapp", "other"] }, "public.phone_type": { "name": "phone_type", "schema": "public", - "values": [ - "sms", - "voice", - "both", - "imessage", - "whatsapp" - ] + "values": ["sms", "voice", "both", "imessage", "whatsapp"] }, "public.app_deployment_status": { "name": "app_deployment_status", "schema": "public", - "values": [ - "draft", - "building", - "deploying", - "deployed", - "failed" - ] + "values": ["draft", "building", "deploying", "deployed", "failed"] }, "public.user_database_status": { "name": "user_database_status", "schema": "public", - "values": [ - "none", - "provisioning", - "ready", - "error" - ] + "values": ["none", "provisioning", "ready", "error"] }, "public.domain_moderation_status": { "name": "domain_moderation_status", "schema": "public", - "values": [ - "clean", - "pending_review", - "flagged", - "suspended" - ] + "values": ["clean", "pending_review", "flagged", "suspended"] }, "public.domain_nameserver_mode": { "name": "domain_nameserver_mode", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_registrar": { "name": "domain_registrar", "schema": "public", - "values": [ - "vercel", - "external" - ] + "values": ["vercel", "external"] }, "public.domain_resource_type": { "name": "domain_resource_type", "schema": "public", - "values": [ - "app", - "container", - "agent", - "mcp" - ] + "values": ["app", "container", "agent", "mcp"] }, "public.domain_status": { "name": "domain_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "suspended", - "transferring" - ] + "values": ["pending", "active", "expired", "suspended", "transferring"] }, "public.moderation_action": { "name": "moderation_action", "schema": "public", - "values": [ - "refused", - "warned", - "flagged_for_ban", - "banned" - ] + "values": ["refused", "warned", "flagged_for_ban", "banned"] }, "public.user_mod_status": { "name": "user_mod_status", "schema": "public", - "values": [ - "clean", - "warned", - "spammer", - "scammer", - "banned" - ] + "values": ["clean", "warned", "spammer", "scammer", "banned"] }, "public.platform_credential_status": { "name": "platform_credential_status", "schema": "public", - "values": [ - "pending", - "active", - "expired", - "revoked", - "error" - ] + "values": ["pending", "active", "expired", "revoked", "error"] }, "public.platform_credential_type": { "name": "platform_credential_type", @@ -24889,73 +23816,37 @@ "public.ledger_entry_type": { "name": "ledger_entry_type", "schema": "public", - "values": [ - "earning", - "redemption", - "adjustment", - "refund" - ] + "values": ["earning", "redemption", "adjustment", "refund"] }, "public.share_type": { "name": "share_type", "schema": "public", - "values": [ - "app_share", - "character_share", - "invite_share" - ] + "values": ["app_share", "character_share", "invite_share"] }, "public.social_platform": { "name": "social_platform", "schema": "public", - "values": [ - "x", - "farcaster", - "telegram", - "discord" - ] + "values": ["x", "farcaster", "telegram", "discord"] }, "public.secret_actor_type": { "name": "secret_actor_type", "schema": "public", - "values": [ - "user", - "api_key", - "system", - "deployment", - "workflow" - ] + "values": ["user", "api_key", "system", "deployment", "workflow"] }, "public.secret_audit_action": { "name": "secret_audit_action", "schema": "public", - "values": [ - "created", - "read", - "updated", - "deleted", - "rotated" - ] + "values": ["created", "read", "updated", "deleted", "rotated"] }, "public.secret_environment": { "name": "secret_environment", "schema": "public", - "values": [ - "development", - "preview", - "production" - ] + "values": ["development", "preview", "production"] }, "public.secret_project_type": { "name": "secret_project_type", "schema": "public", - "values": [ - "character", - "app", - "workflow", - "container", - "mcp" - ] + "values": ["character", "app", "workflow", "container", "mcp"] }, "public.secret_provider": { "name": "secret_provider", @@ -24980,11 +23871,7 @@ "public.secret_scope": { "name": "secret_scope", "schema": "public", - "values": [ - "organization", - "project", - "environment" - ] + "values": ["organization", "project", "environment"] }, "public.seo_artifact_type": { "name": "seo_artifact_type", @@ -25001,32 +23888,17 @@ "public.seo_provider": { "name": "seo_provider", "schema": "public", - "values": [ - "dataforseo", - "serpapi", - "claude", - "indexnow", - "bing" - ] + "values": ["dataforseo", "serpapi", "claude", "indexnow", "bing"] }, "public.seo_provider_status": { "name": "seo_provider_status", "schema": "public", - "values": [ - "pending", - "completed", - "failed" - ] + "values": ["pending", "completed", "failed"] }, "public.seo_request_status": { "name": "seo_request_status", "schema": "public", - "values": [ - "pending", - "in_progress", - "completed", - "failed" - ] + "values": ["pending", "in_progress", "completed", "failed"] }, "public.seo_request_type": { "name": "seo_request_type", @@ -25044,12 +23916,7 @@ "public.redemption_network": { "name": "redemption_network", "schema": "public", - "values": [ - "ethereum", - "base", - "bnb", - "solana" - ] + "values": ["ethereum", "base", "bnb", "solana"] }, "public.redemption_status": { "name": "redemption_status", @@ -25067,22 +23934,12 @@ "public.mcp_pricing_type": { "name": "mcp_pricing_type", "schema": "public", - "values": [ - "free", - "credits", - "x402" - ] + "values": ["free", "credits", "x402"] }, "public.mcp_status": { "name": "mcp_status", "schema": "public", - "values": [ - "draft", - "pending_review", - "live", - "suspended", - "deprecated" - ] + "values": ["draft", "pending_review", "live", "suspended", "deprecated"] }, "public.vertex_tuning_job_state": { "name": "vertex_tuning_job_state", @@ -25098,11 +23955,7 @@ "public.vertex_tuning_scope": { "name": "vertex_tuning_scope", "schema": "public", - "values": [ - "global", - "organization", - "user" - ] + "values": ["global", "organization", "user"] }, "public.vertex_tuning_slot": { "name": "vertex_tuning_slot", @@ -25127,4 +23980,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/repositories/ad-accounts.ts b/packages/db/repositories/ad-accounts.ts index db3c5ad2b..201a9ee2b 100644 --- a/packages/db/repositories/ad-accounts.ts +++ b/packages/db/repositories/ad-accounts.ts @@ -66,7 +66,10 @@ export class AdAccountsRepository { return account; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await db .update(adAccounts) .set({ ...data, updated_at: new Date() }) @@ -75,7 +78,10 @@ export class AdAccountsRepository { return updated; } - async updateStatus(id: string, status: AdAccountStatus): Promise { + async updateStatus( + id: string, + status: AdAccountStatus, + ): Promise { return this.update(id, { status }); } diff --git a/packages/db/repositories/ad-campaigns.ts b/packages/db/repositories/ad-campaigns.ts index 71de49319..fb2a2d8d8 100644 --- a/packages/db/repositories/ad-campaigns.ts +++ b/packages/db/repositories/ad-campaigns.ts @@ -10,7 +10,13 @@ import { type NewAdCampaign, } from "../schemas/ad-campaigns"; -export type { AdCampaign, BudgetType, CampaignObjective, CampaignStatus, NewAdCampaign }; +export type { + AdCampaign, + BudgetType, + CampaignObjective, + CampaignStatus, + NewAdCampaign, +}; /** * Repository for ad campaign database operations. @@ -22,7 +28,9 @@ export class AdCampaignsRepository { }); } - async findByExternalId(externalCampaignId: string): Promise { + async findByExternalId( + externalCampaignId: string, + ): Promise { return await db.query.adCampaigns.findFirst({ where: eq(adCampaigns.external_campaign_id, externalCampaignId), }); @@ -77,7 +85,10 @@ export class AdCampaignsRepository { return campaign; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await db .update(adCampaigns) .set({ ...data, updated_at: new Date() }) @@ -86,7 +97,10 @@ export class AdCampaignsRepository { return updated; } - async updateStatus(id: string, status: CampaignStatus): Promise { + async updateStatus( + id: string, + status: CampaignStatus, + ): Promise { return this.update(id, { status }); } @@ -107,7 +121,10 @@ export class AdCampaignsRepository { }); } - async incrementSpend(id: string, creditsSpent: string): Promise { + async incrementSpend( + id: string, + creditsSpent: string, + ): Promise { const [updated] = await db .update(adCampaigns) .set({ diff --git a/packages/db/repositories/ad-creatives.ts b/packages/db/repositories/ad-creatives.ts index fbcd2e209..189463e1b 100644 --- a/packages/db/repositories/ad-creatives.ts +++ b/packages/db/repositories/ad-creatives.ts @@ -9,7 +9,13 @@ import { type NewAdCreative, } from "../schemas/ad-creatives"; -export type { AdCreative, CallToAction, CreativeStatus, CreativeType, NewAdCreative }; +export type { + AdCreative, + CallToAction, + CreativeStatus, + CreativeType, + NewAdCreative, +}; /** * Repository for ad creative database operations. @@ -21,7 +27,9 @@ export class AdCreativesRepository { }); } - async findByExternalId(externalCreativeId: string): Promise { + async findByExternalId( + externalCreativeId: string, + ): Promise { return await db.query.adCreatives.findFirst({ where: eq(adCreatives.external_creative_id, externalCreativeId), }); @@ -59,7 +67,10 @@ export class AdCreativesRepository { return creative; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await db .update(adCreatives) .set({ ...data, updated_at: new Date() }) @@ -68,7 +79,10 @@ export class AdCreativesRepository { return updated; } - async updateStatus(id: string, status: CreativeStatus): Promise { + async updateStatus( + id: string, + status: CreativeStatus, + ): Promise { return this.update(id, { status }); } diff --git a/packages/db/repositories/ad-transactions.ts b/packages/db/repositories/ad-transactions.ts index dc73c9a0e..f4a10d455 100644 --- a/packages/db/repositories/ad-transactions.ts +++ b/packages/db/repositories/ad-transactions.ts @@ -64,7 +64,10 @@ export class AdTransactionsRepository { } async create(data: NewAdTransaction): Promise { - const [transaction] = await db.insert(adTransactions).values(data).returning(); + const [transaction] = await db + .insert(adTransactions) + .values(data) + .returning(); return transaction; } @@ -78,7 +81,9 @@ export class AdTransactionsRepository { ]; if (options?.startDate) { - conditions.push(sql`${adTransactions.created_at} >= ${options.startDate}`); + conditions.push( + sql`${adTransactions.created_at} >= ${options.startDate}`, + ); } if (options?.endDate) { @@ -109,7 +114,12 @@ export class AdTransactionsRepository { totalCredits: sum(adTransactions.credits_amount), }) .from(adTransactions) - .where(and(eq(adTransactions.campaign_id, campaignId), eq(adTransactions.type, "spend"))); + .where( + and( + eq(adTransactions.campaign_id, campaignId), + eq(adTransactions.type, "spend"), + ), + ); return { totalAmount: Number(result?.totalAmount ?? 0), diff --git a/packages/db/repositories/affiliates.ts b/packages/db/repositories/affiliates.ts index 69ae1d7b0..aab18de63 100644 --- a/packages/db/repositories/affiliates.ts +++ b/packages/db/repositories/affiliates.ts @@ -11,11 +11,16 @@ import { export class AffiliatesRepository { async createAffiliateCode(data: NewAffiliateCode): Promise { - const result = await dbWrite.insert(affiliateCodes).values(data).returning(); + const result = await dbWrite + .insert(affiliateCodes) + .values(data) + .returning(); return result[0]; } - async createAffiliateCodeIfNotExists(data: NewAffiliateCode): Promise { + async createAffiliateCodeIfNotExists( + data: NewAffiliateCode, + ): Promise { return dbWrite.transaction(async (tx) => { await tx.execute( sql`SELECT pg_advisory_xact_lock(hashtext(${`affiliate_code:${data.user_id}`}))`, @@ -32,7 +37,10 @@ export class AffiliatesRepository { return existing; } - const [created] = await tx.insert(affiliateCodes).values(data).returning(); + const [created] = await tx + .insert(affiliateCodes) + .values(data) + .returning(); return created ?? null; }); @@ -50,7 +58,9 @@ export class AffiliatesRepository { return result[0] || null; } - async getAffiliateCodeByUserId(userId: string): Promise { + async getAffiliateCodeByUserId( + userId: string, + ): Promise { const [result] = await dbRead .select() .from(affiliateCodes) @@ -75,7 +85,10 @@ export class AffiliatesRepository { } async linkUserToAffiliate(data: NewUserAffiliate): Promise { - const result = await dbWrite.insert(userAffiliates).values(data).returning(); + const result = await dbWrite + .insert(userAffiliates) + .values(data) + .returning(); return result[0]; } diff --git a/packages/db/repositories/agent-events.ts b/packages/db/repositories/agent-events.ts index 67fd16542..02c553808 100644 --- a/packages/db/repositories/agent-events.ts +++ b/packages/db/repositories/agent-events.ts @@ -28,7 +28,10 @@ export class AgentEventsRepository { }); } - async listByAgent(agentId: string, filters?: AgentEventFilters): Promise { + async listByAgent( + agentId: string, + filters?: AgentEventFilters, + ): Promise { const conditions = [eq(agentEvents.agent_id, agentId)]; if (filters?.eventTypes && filters.eventTypes.length > 0) { @@ -93,7 +96,10 @@ export class AgentEventsRepository { async getLatestError(agentId: string): Promise { return await dbRead.query.agentEvents.findFirst({ - where: and(eq(agentEvents.agent_id, agentId), eq(agentEvents.level, "error")), + where: and( + eq(agentEvents.agent_id, agentId), + eq(agentEvents.level, "error"), + ), orderBy: desc(agentEvents.created_at), }); } diff --git a/packages/db/repositories/agents/agents.ts b/packages/db/repositories/agents/agents.ts index 066735706..c015a2fc2 100644 --- a/packages/db/repositories/agents/agents.ts +++ b/packages/db/repositories/agents/agents.ts @@ -13,7 +13,9 @@ import { eq, inArray } from "drizzle-orm"; import { dbRead, dbWrite } from "@/db/helpers"; import { agentTable } from "@/db/schemas/eliza"; -const toDate = (value: Date | string | number | bigint | null | undefined): Date => { +const toDate = ( + value: Date | string | number | bigint | null | undefined, +): Date => { if (value instanceof Date) { return value; } @@ -129,7 +131,9 @@ export class AgentsRepository { .limit(1); if (existing.length > 0) { - console.warn(`Attempted to create an agent with a duplicate ID. ID: ${agent.id}`); + console.warn( + `Attempted to create an agent with a duplicate ID. ID: ${agent.id}`, + ); return false; } } diff --git a/packages/db/repositories/agents/entities.ts b/packages/db/repositories/agents/entities.ts index 667938e6e..bdd1f2628 100644 --- a/packages/db/repositories/agents/entities.ts +++ b/packages/db/repositories/agents/entities.ts @@ -127,7 +127,11 @@ export class EntitiesRepository { /** * Searches entities by name pattern (case-insensitive). */ - async searchByName(agentId: string, namePattern: string, limit = 10): Promise { + async searchByName( + agentId: string, + namePattern: string, + limit = 10, + ): Promise { const result = await dbRead.execute<{ id: string; agent_id: string; @@ -205,7 +209,10 @@ export class EntitiesRepository { /** * Updates entity metadata. */ - async updateMetadata(entityId: string, metadata: Record): Promise { + async updateMetadata( + entityId: string, + metadata: Record, + ): Promise { const [entity] = await dbWrite .update(entityTable) .set({ metadata }) diff --git a/packages/db/repositories/agents/memories.ts b/packages/db/repositories/agents/memories.ts index 428118037..3c885ce4a 100644 --- a/packages/db/repositories/agents/memories.ts +++ b/packages/db/repositories/agents/memories.ts @@ -56,18 +56,31 @@ export class MemoriesRepository { beforeTimestamp?: number; } = {}, ): Promise { - const { agentId, limit = 50, offset = 0, afterTimestamp, beforeTimestamp } = options; + const { + agentId, + limit = 50, + offset = 0, + afterTimestamp, + beforeTimestamp, + } = options; - const conditions = [eq(memoryTable.roomId, roomId), eq(memoryTable.type, "messages")]; + const conditions = [ + eq(memoryTable.roomId, roomId), + eq(memoryTable.type, "messages"), + ]; if (agentId) { conditions.push(eq(memoryTable.agentId, agentId)); } if (afterTimestamp) { - conditions.push(sql`${memoryTable.createdAt} > ${new Date(afterTimestamp)}`); + conditions.push( + sql`${memoryTable.createdAt} > ${new Date(afterTimestamp)}`, + ); } if (beforeTimestamp) { - conditions.push(sql`${memoryTable.createdAt} < ${new Date(beforeTimestamp)}`); + conditions.push( + sql`${memoryTable.createdAt} < ${new Date(beforeTimestamp)}`, + ); } const results = await dbRead @@ -84,10 +97,17 @@ export class MemoriesRepository { /** * Gets messages for multiple rooms. */ - async findMessagesByRoomIds(roomIds: string[], agentId?: string, limit = 50): Promise { + async findMessagesByRoomIds( + roomIds: string[], + agentId?: string, + limit = 50, + ): Promise { if (roomIds.length === 0) return []; - const conditions = [inArray(memoryTable.roomId, roomIds), eq(memoryTable.type, "messages")]; + const conditions = [ + inArray(memoryTable.roomId, roomIds), + eq(memoryTable.type, "messages"), + ]; if (agentId) { conditions.push(eq(memoryTable.agentId, agentId)); @@ -107,7 +127,10 @@ export class MemoriesRepository { * Counts messages in a room. */ async countMessages(roomId: string, agentId?: string): Promise { - const conditions = [eq(memoryTable.roomId, roomId), eq(memoryTable.type, "messages")]; + const conditions = [ + eq(memoryTable.roomId, roomId), + eq(memoryTable.type, "messages"), + ]; if (agentId) { conditions.push(eq(memoryTable.agentId, agentId)); @@ -128,7 +151,9 @@ export class MemoriesRepository { const result = await dbRead .select({ count: sql`count(*)` }) .from(memoryTable) - .where(and(eq(memoryTable.agentId, agentId), eq(memoryTable.type, "messages"))); + .where( + and(eq(memoryTable.agentId, agentId), eq(memoryTable.type, "messages")), + ); return Number(result[0]?.count || 0); } @@ -140,7 +165,9 @@ export class MemoriesRepository { const result = await dbRead .select({ createdAt: memoryTable.createdAt }) .from(memoryTable) - .where(and(eq(memoryTable.agentId, agentId), eq(memoryTable.type, "messages"))) + .where( + and(eq(memoryTable.agentId, agentId), eq(memoryTable.type, "messages")), + ) .orderBy(desc(memoryTable.createdAt)) .limit(1); @@ -150,7 +177,12 @@ export class MemoriesRepository { /** * Gets memories for a room (excluding messages). */ - async findByRoomId(roomId: string, agentId: string, limit = 50, offset = 0): Promise { + async findByRoomId( + roomId: string, + agentId: string, + limit = 50, + offset = 0, + ): Promise { const results = await dbRead .select() .from(memoryTable) @@ -171,7 +203,11 @@ export class MemoriesRepository { /** * Gets memories by agent across all rooms (excluding messages). */ - async findByAgentId(agentId: string, limit = 50, offset = 0): Promise { + async findByAgentId( + agentId: string, + limit = 50, + offset = 0, + ): Promise { const results = await dbRead .select() .from(memoryTable) @@ -254,8 +290,15 @@ export class MemoriesRepository { /** * Counts memories by type. */ - async countByType(agentId: string, type: string, roomId?: string): Promise { - const conditions = [eq(memoryTable.agentId, agentId), eq(memoryTable.type, type)]; + async countByType( + agentId: string, + type: string, + roomId?: string, + ): Promise { + const conditions = [ + eq(memoryTable.agentId, agentId), + eq(memoryTable.type, type), + ]; if (roomId) { conditions.push(eq(memoryTable.roomId, roomId)); @@ -276,7 +319,12 @@ export class MemoriesRepository { const result = await dbRead .selectDistinct({ type: memoryTable.type }) .from(memoryTable) - .where(and(eq(memoryTable.agentId, agentId), sql`${memoryTable.type} != 'messages'`)); + .where( + and( + eq(memoryTable.agentId, agentId), + sql`${memoryTable.type} != 'messages'`, + ), + ); return result.map((r) => r.type).filter((t): t is string => t !== null); } @@ -290,7 +338,9 @@ export class MemoriesRepository { const result = await dbRead .select() .from(memoryTable) - .where(and(eq(memoryTable.roomId, roomId), eq(memoryTable.type, "messages"))) + .where( + and(eq(memoryTable.roomId, roomId), eq(memoryTable.type, "messages")), + ) .orderBy(desc(memoryTable.createdAt)) .limit(1); @@ -309,7 +359,9 @@ export class MemoriesRepository { async deleteMessages(roomId: string): Promise { const result = await dbWrite .delete(memoryTable) - .where(and(eq(memoryTable.roomId, roomId), eq(memoryTable.type, "messages"))) + .where( + and(eq(memoryTable.roomId, roomId), eq(memoryTable.type, "messages")), + ) .returning({ id: memoryTable.id }); return result.length; @@ -380,7 +432,12 @@ export class MemoriesRepository { async deleteByAgentId(agentId: string): Promise { const result = await dbWrite .delete(memoryTable) - .where(and(eq(memoryTable.agentId, agentId), sql`${memoryTable.type} != 'messages'`)) + .where( + and( + eq(memoryTable.agentId, agentId), + sql`${memoryTable.type} != 'messages'`, + ), + ) .returning({ id: memoryTable.id }); return result.length; @@ -394,7 +451,11 @@ export class MemoriesRepository { * @param types - Optional array of memory types to delete (all types if not specified). * @returns Number of memories deleted. */ - async deleteOlderThan(agentId: string, days: number, types?: string[]): Promise { + async deleteOlderThan( + agentId: string, + days: number, + types?: string[], + ): Promise { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - days); diff --git a/packages/db/repositories/agents/participants.ts b/packages/db/repositories/agents/participants.ts index e4a346558..6ea9dbc5a 100644 --- a/packages/db/repositories/agents/participants.ts +++ b/packages/db/repositories/agents/participants.ts @@ -58,7 +58,9 @@ export class ParticipantsRepository { * * @returns Map of entity ID to array of room IDs. */ - async findRoomsByEntityIds(entityIds: string[]): Promise> { + async findRoomsByEntityIds( + entityIds: string[], + ): Promise> { if (entityIds.length === 0) return new Map(); const results = await dbRead @@ -86,7 +88,12 @@ export class ParticipantsRepository { const result = await dbRead .select({ id: participantTable.id }) .from(participantTable) - .where(and(eq(participantTable.roomId, roomId), eq(participantTable.entityId, entityId))) + .where( + and( + eq(participantTable.roomId, roomId), + eq(participantTable.entityId, entityId), + ), + ) .limit(1); return result.length > 0; @@ -166,7 +173,12 @@ export class ParticipantsRepository { async delete(roomId: string, entityId: string): Promise { const result = await dbWrite .delete(participantTable) - .where(and(eq(participantTable.roomId, roomId), eq(participantTable.entityId, entityId))) + .where( + and( + eq(participantTable.roomId, roomId), + eq(participantTable.entityId, entityId), + ), + ) .returning({ id: participantTable.id }); return result.length > 0; @@ -197,7 +209,12 @@ export class ParticipantsRepository { const results = await dbWrite .update(participantTable) .set({ roomState }) - .where(and(eq(participantTable.roomId, roomId), eq(participantTable.entityId, entityId))) + .where( + and( + eq(participantTable.roomId, roomId), + eq(participantTable.entityId, entityId), + ), + ) .returning(); return (results as Participant[])[0]; diff --git a/packages/db/repositories/agents/rooms.ts b/packages/db/repositories/agents/rooms.ts index 8cf23bf86..784a3ba19 100644 --- a/packages/db/repositories/agents/rooms.ts +++ b/packages/db/repositories/agents/rooms.ts @@ -77,7 +77,11 @@ export class RoomsRepository { * Gets a room by ID. */ async findById(roomId: string): Promise { - const result = await dbRead.select().from(roomTable).where(eq(roomTable.id, roomId)).limit(1); + const result = await dbRead + .select() + .from(roomTable) + .where(eq(roomTable.id, roomId)) + .limit(1); return (result[0] || null) as Room | null; } @@ -88,7 +92,10 @@ export class RoomsRepository { async findByIds(roomIds: string[]): Promise { if (roomIds.length === 0) return []; - const results = await dbRead.select().from(roomTable).where(inArray(roomTable.id, roomIds)); + const results = await dbRead + .select() + .from(roomTable) + .where(inArray(roomTable.id, roomIds)); return results as Room[]; } @@ -97,7 +104,10 @@ export class RoomsRepository { * Finds rooms by agent ID, sorted by last activity. */ async findByAgentId(agentId: string, limit = 50): Promise { - const results = await dbRead.select().from(roomTable).where(eq(roomTable.agentId, agentId)); + const results = await dbRead + .select() + .from(roomTable) + .where(eq(roomTable.agentId, agentId)); // Sort by lastTime from metadata in memory const sorted = results.sort((a, b) => { @@ -171,7 +181,10 @@ export class RoomsRepository { * Counts rooms for an agent. */ async countByAgentId(agentId: string): Promise { - const results = await dbRead.select().from(roomTable).where(eq(roomTable.agentId, agentId)); + const results = await dbRead + .select() + .from(roomTable) + .where(eq(roomTable.agentId, agentId)); return results.length; } @@ -179,7 +192,10 @@ export class RoomsRepository { /** * Updates room metadata by merging with existing metadata. */ - async updateMetadata(roomId: string, metadata: Record): Promise { + async updateMetadata( + roomId: string, + metadata: Record, + ): Promise { // Read current metadata const room = await this.findById(roomId); if (!room) return; @@ -207,7 +223,9 @@ export class RoomsRepository { * @param entityId - The user's ID (from auth). * @returns Rooms with preview data, sorted by most recent activity. */ - async findRoomsWithPreviewForEntity(entityId: string): Promise { + async findRoomsWithPreviewForEntity( + entityId: string, + ): Promise { // Use a subquery to get the latest message per room const latestMessagesSubquery = dbRead .select({ @@ -240,7 +258,10 @@ export class RoomsRepository { .innerJoin(roomTable, eq(participantTable.roomId, roomTable.id)) .leftJoin( latestMessagesSubquery, - and(eq(latestMessagesSubquery.roomId, roomTable.id), eq(latestMessagesSubquery.rn, 1)), + and( + eq(latestMessagesSubquery.roomId, roomTable.id), + eq(latestMessagesSubquery.rn, 1), + ), ) .leftJoin(userCharacters, eq(roomTable.agentId, userCharacters.id)) .where(eq(participantTable.entityId, entityId)); diff --git a/packages/db/repositories/ai-pricing.ts b/packages/db/repositories/ai-pricing.ts index 7989aed89..e0f04d2cf 100644 --- a/packages/db/repositories/ai-pricing.ts +++ b/packages/db/repositories/ai-pricing.ts @@ -9,7 +9,12 @@ import { type NewAiPricingRefreshRun, } from "../schemas/ai-pricing"; -export type { AiPricingEntry, AiPricingRefreshRun, NewAiPricingEntry, NewAiPricingRefreshRun }; +export type { + AiPricingEntry, + AiPricingRefreshRun, + NewAiPricingEntry, + NewAiPricingRefreshRun, +}; export class AiPricingRepository { async listActiveEntries(filters?: { @@ -24,11 +29,16 @@ export class AiPricingRepository { const conditions = [ eq(aiPricingEntries.is_active, true), lte(aiPricingEntries.effective_from, now), - or(isNull(aiPricingEntries.effective_until), gte(aiPricingEntries.effective_until, now)), + or( + isNull(aiPricingEntries.effective_until), + gte(aiPricingEntries.effective_until, now), + ), ]; if (filters?.billingSource) { - conditions.push(eq(aiPricingEntries.billing_source, filters.billingSource)); + conditions.push( + eq(aiPricingEntries.billing_source, filters.billingSource), + ); } if (filters?.provider) { conditions.push(eq(aiPricingEntries.provider, filters.provider)); @@ -37,7 +47,9 @@ export class AiPricingRepository { conditions.push(eq(aiPricingEntries.model, filters.model)); } if (filters?.productFamily) { - conditions.push(eq(aiPricingEntries.product_family, filters.productFamily)); + conditions.push( + eq(aiPricingEntries.product_family, filters.productFamily), + ); } if (filters?.chargeType) { conditions.push(eq(aiPricingEntries.charge_type, filters.chargeType)); @@ -48,12 +60,18 @@ export class AiPricingRepository { return await dbRead.query.aiPricingEntries.findMany({ where: and(...conditions), - orderBy: [desc(aiPricingEntries.priority), desc(aiPricingEntries.effective_from)], + orderBy: [ + desc(aiPricingEntries.priority), + desc(aiPricingEntries.effective_from), + ], }); } async create(entry: NewAiPricingEntry): Promise { - const [created] = await dbWrite.insert(aiPricingEntries).values(entry).returning(); + const [created] = await dbWrite + .insert(aiPricingEntries) + .values(entry) + .returning(); return created; } @@ -75,7 +93,10 @@ export class AiPricingRepository { return rows.length; } - async deactivateEntries(ids: string[], effectiveUntil: Date = new Date()): Promise { + async deactivateEntries( + ids: string[], + effectiveUntil: Date = new Date(), + ): Promise { if (ids.length === 0) { return 0; } @@ -87,7 +108,12 @@ export class AiPricingRepository { effective_until: effectiveUntil, updated_at: effectiveUntil, }) - .where(and(eq(aiPricingEntries.is_active, true), inArray(aiPricingEntries.id, ids))) + .where( + and( + eq(aiPricingEntries.is_active, true), + inArray(aiPricingEntries.id, ids), + ), + ) .returning({ id: aiPricingEntries.id }); return updated.length; @@ -105,15 +131,23 @@ export class AiPricingRepository { updated_at: effectiveUntil, }) .where( - and(eq(aiPricingEntries.is_active, true), eq(aiPricingEntries.source_kind, sourceKind)), + and( + eq(aiPricingEntries.is_active, true), + eq(aiPricingEntries.source_kind, sourceKind), + ), ) .returning({ id: aiPricingEntries.id }); return updated.length; } - async createRefreshRun(run: NewAiPricingRefreshRun): Promise { - const [created] = await dbWrite.insert(aiPricingRefreshRuns).values(run).returning(); + async createRefreshRun( + run: NewAiPricingRefreshRun, + ): Promise { + const [created] = await dbWrite + .insert(aiPricingRefreshRuns) + .values(run) + .returning(); return created; } @@ -130,7 +164,9 @@ export class AiPricingRepository { return updated; } - async listRecentRefreshRuns(limit: number = 20): Promise { + async listRecentRefreshRuns( + limit: number = 20, + ): Promise { return await dbRead.query.aiPricingRefreshRuns.findMany({ orderBy: [desc(aiPricingRefreshRuns.started_at)], limit, diff --git a/packages/db/repositories/anonymous-sessions.ts b/packages/db/repositories/anonymous-sessions.ts index e25ecafe9..88f6d723e 100644 --- a/packages/db/repositories/anonymous-sessions.ts +++ b/packages/db/repositories/anonymous-sessions.ts @@ -113,8 +113,13 @@ export class AnonymousSessionsRepository { * * @throws Error if session not found. */ - async incrementHourlyCount(sessionId: string): Promise<{ allowed: boolean; remaining: number }> { - const hourlyLimit = Number.parseInt(process.env.ANON_HOURLY_LIMIT || "10", 10); + async incrementHourlyCount( + sessionId: string, + ): Promise<{ allowed: boolean; remaining: number }> { + const hourlyLimit = Number.parseInt( + process.env.ANON_HOURLY_LIMIT || "10", + 10, + ); const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); diff --git a/packages/db/repositories/api-keys.ts b/packages/db/repositories/api-keys.ts index 7c2886fe9..1a7426755 100644 --- a/packages/db/repositories/api-keys.ts +++ b/packages/db/repositories/api-keys.ts @@ -83,7 +83,10 @@ export class ApiKeysRepository { /** * Updates an existing API key. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(apiKeys) .set({ @@ -125,7 +128,13 @@ export class ApiKeysRepository { is_active: false, updated_at: new Date(), }) - .where(and(eq(apiKeys.user_id, userId), eq(apiKeys.name, name), eq(apiKeys.is_active, true))); + .where( + and( + eq(apiKeys.user_id, userId), + eq(apiKeys.name, name), + eq(apiKeys.is_active, true), + ), + ); } } diff --git a/packages/db/repositories/app-credit-balances.ts b/packages/db/repositories/app-credit-balances.ts index d0c04bc31..25681b026 100644 --- a/packages/db/repositories/app-credit-balances.ts +++ b/packages/db/repositories/app-credit-balances.ts @@ -31,9 +31,15 @@ export class AppCreditBalancesRepository { /** * Finds an app credit balance by app ID and user ID. */ - async findByAppAndUser(appId: string, userId: string): Promise { + async findByAppAndUser( + appId: string, + userId: string, + ): Promise { return await dbRead.query.appCreditBalances.findFirst({ - where: and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId)), + where: and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), }); } @@ -102,7 +108,10 @@ export class AppCreditBalancesRepository { * Creates a new app credit balance record. */ async create(data: NewAppCreditBalance): Promise { - const [balance] = await dbWrite.insert(appCreditBalances).values(data).returning(); + const [balance] = await dbWrite + .insert(appCreditBalances) + .values(data) + .returning(); return balance; } @@ -142,7 +151,10 @@ export class AppCreditBalancesRepository { }> { return await dbWrite.transaction(async (tx) => { const balance = await tx.query.appCreditBalances.findFirst({ - where: and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId)), + where: and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), }); if (!balance) { @@ -170,7 +182,12 @@ export class AppCreditBalancesRepository { total_purchased: sql`${appCreditBalances.total_purchased} + ${amount}`, updated_at: new Date(), }) - .where(and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId))) + .where( + and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), + ) .returning(); return { @@ -199,7 +216,12 @@ export class AppCreditBalancesRepository { const [balance] = await tx .select() .from(appCreditBalances) - .where(and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId))) + .where( + and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), + ) .for("update"); if (!balance) { @@ -228,7 +250,12 @@ export class AppCreditBalancesRepository { total_spent: sql`${appCreditBalances.total_spent} + ${amount}`, updated_at: new Date(), }) - .where(and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId))) + .where( + and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), + ) .returning(); return { diff --git a/packages/db/repositories/app-earnings.ts b/packages/db/repositories/app-earnings.ts index 1d8a051c6..884ee0111 100644 --- a/packages/db/repositories/app-earnings.ts +++ b/packages/db/repositories/app-earnings.ts @@ -9,7 +9,12 @@ import { type NewAppEarningsTransaction, } from "../schemas/app-earnings"; -export type { AppEarnings, AppEarningsTransaction, NewAppEarnings, NewAppEarningsTransaction }; +export type { + AppEarnings, + AppEarningsTransaction, + NewAppEarnings, + NewAppEarningsTransaction, +}; /** * Repository for app earnings database operations. @@ -56,7 +61,10 @@ export class AppEarningsRepository { limit: number = 50, ): Promise { return await dbRead.query.appEarningsTransactions.findMany({ - where: and(eq(appEarningsTransactions.app_id, appId), eq(appEarningsTransactions.type, type)), + where: and( + eq(appEarningsTransactions.app_id, appId), + eq(appEarningsTransactions.type, type), + ), orderBy: [desc(appEarningsTransactions.created_at)], limit, }); @@ -183,7 +191,10 @@ export class AppEarningsRepository { lte(appEarningsTransactions.created_at, endDate), ), ) - .groupBy(sql`DATE(${appEarningsTransactions.created_at})`, appEarningsTransactions.type) + .groupBy( + sql`DATE(${appEarningsTransactions.created_at})`, + appEarningsTransactions.type, + ) .orderBy(sql`DATE(${appEarningsTransactions.created_at})`); const byDate: Record< @@ -254,7 +265,10 @@ export class AppEarningsRepository { * Earnings go directly to withdrawable_balance for immediate availability. * This provides a better developer experience for solo creators. */ - async addInferenceEarnings(appId: string, amount: number): Promise { + async addInferenceEarnings( + appId: string, + amount: number, + ): Promise { await this.getOrCreate(appId); const [updated] = await dbWrite @@ -277,7 +291,10 @@ export class AppEarningsRepository { * Earnings go directly to withdrawable_balance for immediate availability. * This provides a better developer experience for solo creators. */ - async addPurchaseEarnings(appId: string, amount: number): Promise { + async addPurchaseEarnings( + appId: string, + amount: number, + ): Promise { await this.getOrCreate(appId); const [updated] = await dbWrite @@ -393,7 +410,10 @@ export class AppEarningsRepository { /** * Updates the payout threshold for an app. */ - async updatePayoutThreshold(appId: string, threshold: number): Promise { + async updatePayoutThreshold( + appId: string, + threshold: number, + ): Promise { const [updated] = await dbWrite .update(appEarnings) .set({ @@ -409,8 +429,13 @@ export class AppEarningsRepository { /** * Creates a new earnings transaction record. */ - async createTransaction(data: NewAppEarningsTransaction): Promise { - const [transaction] = await dbWrite.insert(appEarningsTransactions).values(data).returning(); + async createTransaction( + data: NewAppEarningsTransaction, + ): Promise { + const [transaction] = await dbWrite + .insert(appEarningsTransactions) + .values(data) + .returning(); return transaction; } } diff --git a/packages/db/repositories/apps.ts b/packages/db/repositories/apps.ts index 7da3843c9..f6431b33c 100644 --- a/packages/db/repositories/apps.ts +++ b/packages/db/repositories/apps.ts @@ -1,4 +1,14 @@ -import { and, count, countDistinct, desc, eq, gte, lte, or, sql } from "drizzle-orm"; +import { + and, + count, + countDistinct, + desc, + eq, + gte, + lte, + or, + sql, +} from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; import { type App, @@ -28,7 +38,8 @@ export type { NewAppUser, }; -const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; /** * Repository for app database operations. @@ -156,7 +167,10 @@ export class AppsRepository { /** * Lists all apps with optional filters. */ - async listAll(filters?: { isActive?: boolean; isApproved?: boolean }): Promise { + async listAll(filters?: { + isActive?: boolean; + isApproved?: boolean; + }): Promise { const conditions = []; if (filters?.isActive !== undefined) { @@ -176,7 +190,10 @@ export class AppsRepository { /** * Finds an app user by app ID and user ID. */ - async findAppUser(appId: string, userId: string): Promise { + async findAppUser( + appId: string, + userId: string, + ): Promise { return await dbRead.query.appUsers.findFirst({ where: and(eq(appUsers.app_id, appId), eq(appUsers.user_id, userId)), }); @@ -211,7 +228,12 @@ export class AppsRepository { total_cost: string; }> > { - const truncUnit = periodType === "hourly" ? "hour" : periodType === "monthly" ? "month" : "day"; + const truncUnit = + periodType === "hourly" + ? "hour" + : periodType === "monthly" + ? "month" + : "day"; const results = await dbRead.execute<{ period_start: string; @@ -244,7 +266,10 @@ export class AppsRepository { /** * Gets the latest app analytics records. */ - async getLatestAnalytics(appId: string, limit: number = 30): Promise { + async getLatestAnalytics( + appId: string, + limit: number = 30, + ): Promise { return await dbRead.query.appAnalytics.findMany({ where: eq(appAnalytics.app_id, appId), orderBy: [desc(appAnalytics.period_start)], @@ -313,7 +338,10 @@ export class AppsRepository { * @param region Database region to set * @returns Updated app if the status transition succeeded, undefined if another process won the race */ - async trySetDatabaseProvisioning(id: string, region: string): Promise { + async trySetDatabaseProvisioning( + id: string, + region: string, + ): Promise { const [updated] = await dbWrite .update(apps) .set({ @@ -325,7 +353,10 @@ export class AppsRepository { .where( and( eq(apps.id, id), - or(eq(apps.user_database_status, "none"), eq(apps.user_database_status, "error")), + or( + eq(apps.user_database_status, "none"), + eq(apps.user_database_status, "error"), + ), ), ) .returning(); @@ -342,7 +373,10 @@ export class AppsRepository { /** * Atomically increments app usage statistics. */ - async incrementUsage(id: string, creditsUsed: string = "0.00"): Promise { + async incrementUsage( + id: string, + creditsUsed: string = "0.00", + ): Promise { await dbWrite .update(apps) .set({ @@ -441,7 +475,10 @@ export class AppsRepository { * Creates a new app analytics record. */ async createAnalytics(data: NewAppAnalytics): Promise { - const [analytics] = await dbWrite.insert(appAnalytics).values(data).returning(); + const [analytics] = await dbWrite + .insert(appAnalytics) + .values(data) + .returning(); return analytics; } @@ -453,7 +490,10 @@ export class AppsRepository { * Logs an individual app request for detailed analytics. */ async logRequest(data: NewAppRequest): Promise { - const [request] = await dbWrite.insert(appRequests).values(data).returning(); + const [request] = await dbWrite + .insert(appRequests) + .values(data) + .returning(); return request; } @@ -471,7 +511,14 @@ export class AppsRepository { endDate?: Date; } = {}, ): Promise<{ requests: AppRequest[]; total: number }> { - const { limit = 50, offset = 0, requestType, source, startDate, endDate } = options; + const { + limit = 50, + offset = 0, + requestType, + source, + startDate, + endDate, + } = options; const conditions = [eq(appRequests.app_id, appId)]; diff --git a/packages/db/repositories/characters.ts b/packages/db/repositories/characters.ts index 4e43ecf1f..9296d56a5 100644 --- a/packages/db/repositories/characters.ts +++ b/packages/db/repositories/characters.ts @@ -79,7 +79,10 @@ export class UserCharactersRepository { .where(eq(elizaRoomCharactersTable.user_id, userId)); conditions.push( - or(eq(userCharacters.user_id, userId), inArray(userCharacters.id, interactedCharacterIds))!, + or( + eq(userCharacters.user_id, userId), + inArray(userCharacters.id, interactedCharacterIds), + )!, ); return conditions; @@ -94,7 +97,12 @@ export class UserCharactersRepository { ): SQL[] { const conditions: SQL[] = []; - conditions.push(or(eq(userCharacters.is_template, true), eq(userCharacters.is_public, true))!); + conditions.push( + or( + eq(userCharacters.is_template, true), + eq(userCharacters.is_public, true), + )!, + ); if (filters.search) { conditions.push( @@ -148,7 +156,10 @@ export class UserCharactersRepository { organizationId: string, ): Promise { return await dbRead.query.userCharacters.findFirst({ - where: and(eq(userCharacters.id, id), eq(userCharacters.organization_id, organizationId)), + where: and( + eq(userCharacters.id, id), + eq(userCharacters.organization_id, organizationId), + ), }); } @@ -161,7 +172,10 @@ export class UserCharactersRepository { organizationId: string, ): Promise { return await dbWrite.query.userCharacters.findFirst({ - where: and(eq(userCharacters.id, id), eq(userCharacters.organization_id, organizationId)), + where: and( + eq(userCharacters.id, id), + eq(userCharacters.organization_id, organizationId), + ), }); } @@ -173,13 +187,19 @@ export class UserCharactersRepository { return []; } - return await dbRead.select().from(userCharacters).where(inArray(userCharacters.id, ids)); + return await dbRead + .select() + .from(userCharacters) + .where(inArray(userCharacters.id, ids)); } /** * Finds characters by ID within an organization. */ - async findByIdsInOrganization(ids: string[], organizationId: string): Promise { + async findByIdsInOrganization( + ids: string[], + organizationId: string, + ): Promise { if (ids.length === 0) { return []; } @@ -188,7 +208,10 @@ export class UserCharactersRepository { .select() .from(userCharacters) .where( - and(inArray(userCharacters.id, ids), eq(userCharacters.organization_id, organizationId)), + and( + inArray(userCharacters.id, ids), + eq(userCharacters.organization_id, organizationId), + ), ); } @@ -240,12 +263,16 @@ export class UserCharactersRepository { organizationId?: string; limit?: number; }): Promise { - const conditions: SQL[] = [sql`${userCharacters.token_address} IS NOT NULL`]; + const conditions: SQL[] = [ + sql`${userCharacters.token_address} IS NOT NULL`, + ]; if (options?.chain) { conditions.push(eq(userCharacters.token_chain, options.chain)); } if (options?.organizationId) { - conditions.push(eq(userCharacters.organization_id, options.organizationId)); + conditions.push( + eq(userCharacters.organization_id, options.organizationId), + ); } return await dbRead .select() @@ -280,7 +307,9 @@ export class UserCharactersRepository { * Gets all existing usernames (for bulk uniqueness check). */ async getAllUsernames(): Promise> { - const result = await dbRead.select({ username: userCharacters.username }).from(userCharacters); + const result = await dbRead + .select({ username: userCharacters.username }) + .from(userCharacters); const usernames = new Set(); for (const row of result) { @@ -348,7 +377,10 @@ export class UserCharactersRepository { */ async listPublic(): Promise { return await dbRead.query.userCharacters.findMany({ - where: and(eq(userCharacters.is_public, true), eq(userCharacters.source, "cloud")), + where: and( + eq(userCharacters.is_public, true), + eq(userCharacters.source, "cloud"), + ), orderBy: desc(userCharacters.created_at), }); } @@ -358,7 +390,10 @@ export class UserCharactersRepository { */ async listTemplates(): Promise { return await dbRead.query.userCharacters.findMany({ - where: and(eq(userCharacters.is_template, true), eq(userCharacters.source, "cloud")), + where: and( + eq(userCharacters.is_template, true), + eq(userCharacters.source, "cloud"), + ), orderBy: desc(userCharacters.created_at), }); } @@ -367,14 +402,20 @@ export class UserCharactersRepository { * Creates a new character. */ async create(data: NewUserCharacter): Promise { - const [character] = await dbWrite.insert(userCharacters).values(data).returning(); + const [character] = await dbWrite + .insert(userCharacters) + .values(data) + .returning(); return character; } /** * Updates an existing character. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(userCharacters) .set({ @@ -406,11 +447,17 @@ export class UserCharactersRepository { ? userCharacters.popularity_score : desc(userCharacters.popularity_score); case "newest": - return direction === "asc" ? userCharacters.created_at : desc(userCharacters.created_at); + return direction === "asc" + ? userCharacters.created_at + : desc(userCharacters.created_at); case "name": - return direction === "asc" ? userCharacters.name : desc(userCharacters.name); + return direction === "asc" + ? userCharacters.name + : desc(userCharacters.name); case "updated": - return direction === "asc" ? userCharacters.updated_at : desc(userCharacters.updated_at); + return direction === "asc" + ? userCharacters.updated_at + : desc(userCharacters.updated_at); default: return desc(userCharacters.popularity_score); } @@ -444,7 +491,11 @@ export class UserCharactersRepository { /** * Counts characters matching the search filters. */ - async count(filters: SearchFilters, userId: string, _organizationId: string): Promise { + async count( + filters: SearchFilters, + userId: string, + _organizationId: string, + ): Promise { const conditions = this.buildSearchConditions(filters, userId); const result = await dbRead @@ -498,7 +549,10 @@ export class UserCharactersRepository { */ async getFeatured(limit: number = 10): Promise { return await dbRead.query.userCharacters.findMany({ - where: and(eq(userCharacters.featured, true), eq(userCharacters.source, "cloud")), + where: and( + eq(userCharacters.featured, true), + eq(userCharacters.source, "cloud"), + ), orderBy: desc(userCharacters.popularity_score), limit, }); @@ -512,7 +566,10 @@ export class UserCharactersRepository { async getPopular(limit: number = 20): Promise { return await dbRead.query.userCharacters.findMany({ where: and( - or(eq(userCharacters.is_template, true), eq(userCharacters.is_public, true)), + or( + eq(userCharacters.is_template, true), + eq(userCharacters.is_public, true), + ), eq(userCharacters.source, "cloud"), ), orderBy: desc(userCharacters.popularity_score), @@ -544,7 +601,9 @@ export class UserCharactersRepository { /** * Counts public characters matching the filters. */ - async countPublic(filters: Omit): Promise { + async countPublic( + filters: Omit, + ): Promise { const conditions = this.buildPublicSearchConditions(filters); const result = await dbRead diff --git a/packages/db/repositories/cli-auth-sessions.ts b/packages/db/repositories/cli-auth-sessions.ts index 96f8c1ee6..0974bfc90 100644 --- a/packages/db/repositories/cli-auth-sessions.ts +++ b/packages/db/repositories/cli-auth-sessions.ts @@ -19,7 +19,9 @@ export class CliAuthSessionsRepository { /** * Finds a CLI auth session by session ID. */ - async findBySessionId(sessionId: string): Promise { + async findBySessionId( + sessionId: string, + ): Promise { const [session] = await dbRead .select() .from(cliAuthSessions) @@ -32,12 +34,19 @@ export class CliAuthSessionsRepository { /** * Finds an active (non-expired) CLI auth session by session ID. */ - async findActiveBySessionId(sessionId: string): Promise { + async findActiveBySessionId( + sessionId: string, + ): Promise { const now = new Date(); const [session] = await dbRead .select() .from(cliAuthSessions) - .where(and(eq(cliAuthSessions.session_id, sessionId), gt(cliAuthSessions.expires_at, now))) + .where( + and( + eq(cliAuthSessions.session_id, sessionId), + gt(cliAuthSessions.expires_at, now), + ), + ) .limit(1); return session; @@ -53,7 +62,10 @@ export class CliAuthSessionsRepository { * @throws Error if session creation fails. */ async create(data: NewCliAuthSession): Promise { - const [session] = await dbWrite.insert(cliAuthSessions).values(data).returning(); + const [session] = await dbWrite + .insert(cliAuthSessions) + .values(data) + .returning(); if (!session) { throw new Error("Failed to create CLI auth session"); @@ -130,7 +142,9 @@ export class CliAuthSessionsRepository { */ async deleteExpiredSessions(): Promise { const now = new Date(); - await dbWrite.delete(cliAuthSessions).where(lt(cliAuthSessions.expires_at, now)); + await dbWrite + .delete(cliAuthSessions) + .where(lt(cliAuthSessions.expires_at, now)); } } diff --git a/packages/db/repositories/containers.ts b/packages/db/repositories/containers.ts index 7ec8d26a9..be9732718 100644 --- a/packages/db/repositories/containers.ts +++ b/packages/db/repositories/containers.ts @@ -87,11 +87,19 @@ export class ContainersRepository { /** * Finds a container by ID within an organization. */ - async findById(id: string, organizationId: string): Promise { + async findById( + id: string, + organizationId: string, + ): Promise { const results = await dbRead .select() .from(containers) - .where(and(eq(containers.id, id), eq(containers.organization_id, organizationId))) + .where( + and( + eq(containers.id, id), + eq(containers.organization_id, organizationId), + ), + ) .limit(1); return results[0] || null; @@ -175,7 +183,9 @@ export class ContainersRepository { allowed, current: count, max: maxContainers, - error: allowed ? undefined : `Container quota exceeded (${count}/${maxContainers})`, + error: allowed + ? undefined + : `Container quota exceeded (${count}/${maxContainers})`, }; } @@ -212,7 +222,12 @@ export class ContainersRepository { ...data, updated_at: new Date(), }) - .where(and(eq(containers.id, id), eq(containers.organization_id, organizationId))) + .where( + and( + eq(containers.id, id), + eq(containers.organization_id, organizationId), + ), + ) .returning(); return updated || null; @@ -224,7 +239,12 @@ export class ContainersRepository { async delete(id: string, organizationId: string): Promise { const results = await dbWrite .delete(containers) - .where(and(eq(containers.id, id), eq(containers.organization_id, organizationId))) + .where( + and( + eq(containers.id, id), + eq(containers.organization_id, organizationId), + ), + ) .returning(); return results.length > 0; @@ -273,7 +293,10 @@ export class ContainersRepository { * Prevents race conditions where multiple concurrent requests could bypass quota limits. * Uses row-level locking (FOR UPDATE) to ensure atomicity. */ - async createWithQuotaCheck(data: NewContainer, transaction?: Database): Promise { + async createWithQuotaCheck( + data: NewContainer, + transaction?: Database, + ): Promise { const executeInTransaction = async (tx: Database) => { // 1. Lock the organization row to prevent concurrent quota checks const [org] = await tx @@ -352,7 +375,10 @@ export class ContainersRepository { ): Promise<{ container: Container; newBalance: number }> { return await dbWrite.transaction(async (tx) => { // Create container with quota check - const container = await this.createWithQuotaCheck(containerData, tx as typeof dbWrite); + const container = await this.createWithQuotaCheck( + containerData, + tx as typeof dbWrite, + ); // Check and deduct credits const org = await tx.query.organizations.findFirst({ diff --git a/packages/db/repositories/conversations.ts b/packages/db/repositories/conversations.ts index 2b1238d72..35c61c585 100644 --- a/packages/db/repositories/conversations.ts +++ b/packages/db/repositories/conversations.ts @@ -9,7 +9,12 @@ import { type NewConversationMessage, } from "../schemas/conversations"; -export type { Conversation, ConversationMessage, NewConversation, NewConversationMessage }; +export type { + Conversation, + ConversationMessage, + NewConversation, + NewConversationMessage, +}; /** * Conversation with associated messages. @@ -41,7 +46,9 @@ export class ConversationsRepository { /** * Finds a conversation with all associated messages. */ - async findWithMessages(id: string): Promise { + async findWithMessages( + id: string, + ): Promise { const conversation = await dbRead.query.conversations.findFirst({ where: eq(conversations.id, id), with: { @@ -68,7 +75,10 @@ export class ConversationsRepository { /** * Lists conversations for an organization. */ - async listByOrganization(organizationId: string, limit?: number): Promise { + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { return await dbRead.query.conversations.findMany({ where: eq(conversations.organization_id, organizationId), orderBy: desc(conversations.updated_at), @@ -106,14 +116,20 @@ export class ConversationsRepository { * Creates a new conversation. */ async create(data: NewConversation): Promise { - const [conversation] = await dbWrite.insert(conversations).values(data).returning(); + const [conversation] = await dbWrite + .insert(conversations) + .values(data) + .returning(); return conversation; } /** * Updates an existing conversation. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(conversations) .set({ @@ -136,7 +152,10 @@ export class ConversationsRepository { * Adds a message to a conversation. */ async addMessage(data: NewConversationMessage): Promise { - const [message] = await dbWrite.insert(conversationMessages).values(data).returning(); + const [message] = await dbWrite + .insert(conversationMessages) + .values(data) + .returning(); return message; } @@ -176,7 +195,9 @@ export class ConversationsRepository { .set({ message_count: conversation.message_count + 1, last_message_at: new Date(), - total_cost: String(Number(conversation.total_cost) + Number(data.cost || 0)), + total_cost: String( + Number(conversation.total_cost) + Number(data.cost || 0), + ), updated_at: new Date(), }) .where(eq(conversations.id, conversationId)); diff --git a/packages/db/repositories/credit-packs.ts b/packages/db/repositories/credit-packs.ts index 50c7e8202..b946c54ec 100644 --- a/packages/db/repositories/credit-packs.ts +++ b/packages/db/repositories/credit-packs.ts @@ -1,6 +1,10 @@ import { asc, eq } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; -import { type CreditPack, creditPacks, type NewCreditPack } from "../schemas/credit-packs"; +import { + type CreditPack, + creditPacks, + type NewCreditPack, +} from "../schemas/credit-packs"; export type { CreditPack, NewCreditPack }; @@ -24,7 +28,9 @@ export class CreditPacksRepository { /** * Finds a credit pack by Stripe price ID. */ - async findByStripePriceId(stripePriceId: string): Promise { + async findByStripePriceId( + stripePriceId: string, + ): Promise { return await dbRead.query.creditPacks.findFirst({ where: eq(creditPacks.stripe_price_id, stripePriceId), }); @@ -57,14 +63,20 @@ export class CreditPacksRepository { * Creates a new credit pack. */ async create(data: NewCreditPack): Promise { - const [creditPack] = await dbWrite.insert(creditPacks).values(data).returning(); + const [creditPack] = await dbWrite + .insert(creditPacks) + .values(data) + .returning(); return creditPack; } /** * Updates an existing credit pack. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(creditPacks) .set({ diff --git a/packages/db/repositories/credit-transactions.ts b/packages/db/repositories/credit-transactions.ts index 6b4e82799..c0e461be4 100644 --- a/packages/db/repositories/credit-transactions.ts +++ b/packages/db/repositories/credit-transactions.ts @@ -31,7 +31,9 @@ export class CreditTransactionsRepository { /** * Finds a credit transaction by Stripe payment intent ID. */ - async findByStripePaymentIntent(paymentIntentId: string): Promise { + async findByStripePaymentIntent( + paymentIntentId: string, + ): Promise { return await dbRead.query.creditTransactions.findFirst({ where: eq(creditTransactions.stripe_payment_intent_id, paymentIntentId), }); @@ -40,7 +42,10 @@ export class CreditTransactionsRepository { /** * Lists credit transactions for an organization, ordered by creation date. */ - async listByOrganization(organizationId: string, limit?: number): Promise { + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { return await dbRead.query.creditTransactions.findMany({ where: eq(creditTransactions.organization_id, organizationId), orderBy: desc(creditTransactions.created_at), @@ -91,7 +96,10 @@ export class CreditTransactionsRepository { * Creates a new credit transaction. */ async create(data: NewCreditTransaction): Promise { - const [transaction] = await dbWrite.insert(creditTransactions).values(data).returning(); + const [transaction] = await dbWrite + .insert(creditTransactions) + .values(data) + .returning(); return transaction; } } diff --git a/packages/db/repositories/crypto-payments.ts b/packages/db/repositories/crypto-payments.ts index 1dc0420bc..cbbbb678f 100644 --- a/packages/db/repositories/crypto-payments.ts +++ b/packages/db/repositories/crypto-payments.ts @@ -25,14 +25,18 @@ export class CryptoPaymentsRepository { }); } - async findByPaymentAddress(address: string): Promise { + async findByPaymentAddress( + address: string, + ): Promise { return await dbRead.query.cryptoPayments.findFirst({ where: eq(cryptoPayments.payment_address, address), orderBy: desc(cryptoPayments.created_at), }); } - async findByTransactionHash(txHash: string): Promise { + async findByTransactionHash( + txHash: string, + ): Promise { return await dbRead.query.cryptoPayments.findFirst({ where: eq(cryptoPayments.transaction_hash, txHash), }); @@ -47,9 +51,14 @@ export class CryptoPaymentsRepository { return payment; } - async findPendingByAddress(address: string): Promise { + async findPendingByAddress( + address: string, + ): Promise { return await dbRead.query.cryptoPayments.findFirst({ - where: and(eq(cryptoPayments.payment_address, address), eq(cryptoPayments.status, "pending")), + where: and( + eq(cryptoPayments.payment_address, address), + eq(cryptoPayments.status, "pending"), + ), }); } @@ -69,7 +78,10 @@ export class CryptoPaymentsRepository { async listExpiredPendingPayments(): Promise { return await dbRead.query.cryptoPayments.findMany({ - where: and(eq(cryptoPayments.status, "pending"), lt(cryptoPayments.expires_at, new Date())), + where: and( + eq(cryptoPayments.status, "pending"), + lt(cryptoPayments.expires_at, new Date()), + ), }); } @@ -89,7 +101,10 @@ export class CryptoPaymentsRepository { return payment; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [payment] = await dbWrite .update(cryptoPayments) .set({ @@ -134,13 +149,18 @@ export class CryptoPaymentsRepository { return payment; } - async markAsFailed(id: string, reason?: string): Promise { + async markAsFailed( + id: string, + reason?: string, + ): Promise { const existing = await this.findById(id); const [payment] = await dbWrite .update(cryptoPayments) .set({ status: "failed", - metadata: reason ? { ...existing?.metadata, failureReason: reason } : existing?.metadata, + metadata: reason + ? { ...existing?.metadata, failureReason: reason } + : existing?.metadata, updated_at: new Date(), }) .where(eq(cryptoPayments.id, id)) diff --git a/packages/db/repositories/discord-channels.ts b/packages/db/repositories/discord-channels.ts index 5e2e9a554..5684b52c0 100644 --- a/packages/db/repositories/discord-channels.ts +++ b/packages/db/repositories/discord-channels.ts @@ -7,7 +7,10 @@ import { } from "@/db/schemas/discord-channels"; class DiscordChannelsRepository { - async findByGuild(organizationId: string, guildId: string): Promise { + async findByGuild( + organizationId: string, + guildId: string, + ): Promise { return dbRead .select() .from(discordChannels) @@ -37,7 +40,10 @@ class DiscordChannelsRepository { return results[0]; } - async findSendableByGuild(organizationId: string, guildId: string): Promise { + async findSendableByGuild( + organizationId: string, + guildId: string, + ): Promise { return dbRead .select() .from(discordChannels) @@ -54,7 +60,10 @@ class DiscordChannelsRepository { async upsert( data: Omit, ): Promise { - const existing = await this.findByChannelId(data.organization_id, data.channel_id); + const existing = await this.findByChannelId( + data.organization_id, + data.channel_id, + ); if (existing) { const [updated] = await dbWrite @@ -75,7 +84,10 @@ class DiscordChannelsRepository { return updated; } - const [created] = await dbWrite.insert(discordChannels).values(data).returning(); + const [created] = await dbWrite + .insert(discordChannels) + .values(data) + .returning(); return created; } diff --git a/packages/db/repositories/discord-connections.ts b/packages/db/repositories/discord-connections.ts index eb304e01d..6ee90d8b7 100644 --- a/packages/db/repositories/discord-connections.ts +++ b/packages/db/repositories/discord-connections.ts @@ -27,14 +27,18 @@ interface DecryptedAssignment { } /** Valid connection status values (matches migration CHECK constraint) */ -type ConnectionStatus = "pending" | "connecting" | "connected" | "disconnected" | "error"; +type ConnectionStatus = + | "pending" + | "connecting" + | "connected" + | "disconnected" + | "error"; export const discordConnectionsRepository = { async create(input: CreateConnectionInput): Promise { const encryption = getEncryptionService(); - const { encryptedValue, encryptedDek, nonce, authTag, keyId } = await encryption.encrypt( - input.botToken, - ); + const { encryptedValue, encryptedDek, nonce, authTag, keyId } = + await encryption.encrypt(input.botToken); const [connection] = await db .insert(discordConnections) @@ -63,7 +67,9 @@ export const discordConnectionsRepository = { return connection ?? null; }, - async findByOrganizationId(organizationId: string): Promise { + async findByOrganizationId( + organizationId: string, + ): Promise { return db .select() .from(discordConnections) @@ -91,7 +97,12 @@ export const discordConnectionsRepository = { return db .select() .from(discordConnections) - .where(and(eq(discordConnections.is_active, true), isNull(discordConnections.assigned_pod))); + .where( + and( + eq(discordConnections.is_active, true), + isNull(discordConnections.assigned_pod), + ), + ); }, async findByAssignedPod(podName: string): Promise { @@ -99,7 +110,10 @@ export const discordConnectionsRepository = { .select() .from(discordConnections) .where( - and(eq(discordConnections.is_active, true), eq(discordConnections.assigned_pod, podName)), + and( + eq(discordConnections.is_active, true), + eq(discordConnections.assigned_pod, podName), + ), ); }, @@ -107,7 +121,9 @@ export const discordConnectionsRepository = { * Atomically assign an unassigned connection to a pod using row-level locking. * Prevents race conditions when multiple pods request assignments simultaneously. */ - async assignUnassignedToPod(podName: string): Promise { + async assignUnassignedToPod( + podName: string, + ): Promise { // Use raw SQL for SELECT ... FOR UPDATE SKIP LOCKED const result = await db.execute(sql` UPDATE discord_connections @@ -190,7 +206,10 @@ export const discordConnectionsRepository = { * Batch update heartbeats for all connections assigned to a pod. * Returns the number of connections updated. */ - async updateHeartbeatBatch(podName: string, connectionIds: string[]): Promise { + async updateHeartbeatBatch( + podName: string, + connectionIds: string[], + ): Promise { if (connectionIds.length === 0) return 0; const result = await db @@ -232,14 +251,20 @@ export const discordConnectionsRepository = { updates.events_routed = stats.eventsRouted; } - await db.update(discordConnections).set(updates).where(eq(discordConnections.id, connectionId)); + await db + .update(discordConnections) + .set(updates) + .where(eq(discordConnections.id, connectionId)); }, /** * Check if a pod has any connections with a recent heartbeat. * Used to prevent false failover claims against healthy pods. */ - async hasRecentHeartbeat(podName: string, thresholdMs: number): Promise { + async hasRecentHeartbeat( + podName: string, + thresholdMs: number, + ): Promise { const cutoffTime = new Date(Date.now() - thresholdMs); const connections = await db .select({ id: discordConnections.id }) @@ -347,7 +372,10 @@ export const discordConnectionsRepository = { * @param claimNew - Whether to claim a new unassigned connection (default: true) * Set to false when pod is at capacity to prevent stuck assignments */ - async getAssignmentsForPod(podName: string, claimNew = true): Promise { + async getAssignmentsForPod( + podName: string, + claimNew = true, + ): Promise { const encryption = getEncryptionService(); // Only try to claim new connections if pod has capacity @@ -378,7 +406,8 @@ export const discordConnectionsRepository = { characterId: conn.character_id ?? null, }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); logger.error("[DiscordConnections] Failed to decrypt bot token", { connectionId: conn.id, error: errorMessage, @@ -398,10 +427,14 @@ export const discordConnectionsRepository = { }) .where(eq(discordConnections.id, conn.id)); } catch (dbError) { - logger.error("[DiscordConnections] Failed to mark connection as error", { - connectionId: conn.id, - error: dbError instanceof Error ? dbError.message : String(dbError), - }); + logger.error( + "[DiscordConnections] Failed to mark connection as error", + { + connectionId: conn.id, + error: + dbError instanceof Error ? dbError.message : String(dbError), + }, + ); } return null; diff --git a/packages/db/repositories/discord-guilds.ts b/packages/db/repositories/discord-guilds.ts index f375b9f15..2febfd371 100644 --- a/packages/db/repositories/discord-guilds.ts +++ b/packages/db/repositories/discord-guilds.ts @@ -12,17 +12,26 @@ class DiscordGuildsRepository { .select() .from(discordGuilds) .where( - and(eq(discordGuilds.organization_id, organizationId), eq(discordGuilds.is_active, true)), + and( + eq(discordGuilds.organization_id, organizationId), + eq(discordGuilds.is_active, true), + ), ) .orderBy(desc(discordGuilds.bot_joined_at)); } - async findByGuildId(organizationId: string, guildId: string): Promise { + async findByGuildId( + organizationId: string, + guildId: string, + ): Promise { const results = await dbRead .select() .from(discordGuilds) .where( - and(eq(discordGuilds.organization_id, organizationId), eq(discordGuilds.guild_id, guildId)), + and( + eq(discordGuilds.organization_id, organizationId), + eq(discordGuilds.guild_id, guildId), + ), ) .limit(1); return results[0]; @@ -31,7 +40,10 @@ class DiscordGuildsRepository { async upsert( data: Omit, ): Promise { - const existing = await this.findByGuildId(data.organization_id, data.guild_id); + const existing = await this.findByGuildId( + data.organization_id, + data.guild_id, + ); if (existing) { const [updated] = await dbWrite @@ -49,7 +61,10 @@ class DiscordGuildsRepository { return updated; } - const [created] = await dbWrite.insert(discordGuilds).values(data).returning(); + const [created] = await dbWrite + .insert(discordGuilds) + .values(data) + .returning(); return created; } @@ -57,7 +72,10 @@ class DiscordGuildsRepository { await dbWrite .delete(discordGuilds) .where( - and(eq(discordGuilds.organization_id, organizationId), eq(discordGuilds.guild_id, guildId)), + and( + eq(discordGuilds.organization_id, organizationId), + eq(discordGuilds.guild_id, guildId), + ), ); } @@ -69,12 +87,17 @@ class DiscordGuildsRepository { updated_at: new Date(), }) .where( - and(eq(discordGuilds.organization_id, organizationId), eq(discordGuilds.guild_id, guildId)), + and( + eq(discordGuilds.organization_id, organizationId), + eq(discordGuilds.guild_id, guildId), + ), ); } async deleteByOrganization(organizationId: string): Promise { - await dbWrite.delete(discordGuilds).where(eq(discordGuilds.organization_id, organizationId)); + await dbWrite + .delete(discordGuilds) + .where(eq(discordGuilds.organization_id, organizationId)); } } diff --git a/packages/db/repositories/docker-nodes.ts b/packages/db/repositories/docker-nodes.ts index 53f39e413..48954240b 100644 --- a/packages/db/repositories/docker-nodes.ts +++ b/packages/db/repositories/docker-nodes.ts @@ -37,7 +37,11 @@ export class DockerNodesRepository { } async findById(id: string): Promise { - const [r] = await dbRead.select().from(dockerNodes).where(eq(dockerNodes.id, id)).limit(1); + const [r] = await dbRead + .select() + .from(dockerNodes) + .where(eq(dockerNodes.id, id)) + .limit(1); return r ?? null; } @@ -56,7 +60,9 @@ export class DockerNodesRepository { sql`${dockerNodes.allocated_count} < ${dockerNodes.capacity}`, ), ) - .orderBy(sql`(${dockerNodes.capacity} - ${dockerNodes.allocated_count}) DESC`) + .orderBy( + sql`(${dockerNodes.capacity} - ${dockerNodes.allocated_count}) DESC`, + ) .limit(1); return r ?? null; } @@ -71,7 +77,10 @@ export class DockerNodesRepository { return r; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [r] = await dbWrite .update(dockerNodes) .set({ ...data, updated_at: new Date() }) diff --git a/packages/db/repositories/eliza-room-characters.ts b/packages/db/repositories/eliza-room-characters.ts index 560283dd6..bb5fd0c46 100644 --- a/packages/db/repositories/eliza-room-characters.ts +++ b/packages/db/repositories/eliza-room-characters.ts @@ -44,7 +44,9 @@ export const elizaRoomCharactersRepository = { /** * Count rooms for multiple characters in one query */ - async countByCharacterIds(characterIds: string[]): Promise> { + async countByCharacterIds( + characterIds: string[], + ): Promise> { if (characterIds.length === 0) { return new Map(); } @@ -90,7 +92,10 @@ export const elizaRoomCharactersRepository = { .limit(1); const character = result[0]; - console.log(`[RoomCharRepo] DB result - characterId:`, character?.character_id || "none"); + console.log( + `[RoomCharRepo] DB result - characterId:`, + character?.character_id || "none", + ); return character; }, @@ -168,7 +173,10 @@ export const elizaRoomCharactersRepository = { * Creates a new room-character mapping. */ async create(data: NewElizaRoomCharacter): Promise { - const result = await dbWrite.insert(elizaRoomCharactersTable).values(data).returning(); + const result = await dbWrite + .insert(elizaRoomCharactersTable) + .values(data) + .returning(); return result[0]; }, @@ -176,7 +184,10 @@ export const elizaRoomCharactersRepository = { /** * Updates the character mapping for a room. */ - async update(roomId: string, characterId: string): Promise { + async update( + roomId: string, + characterId: string, + ): Promise { const result = await dbWrite .update(elizaRoomCharactersTable) .set({ diff --git a/packages/db/repositories/generations.ts b/packages/db/repositories/generations.ts index f4231bf1f..8e81be44e 100644 --- a/packages/db/repositories/generations.ts +++ b/packages/db/repositories/generations.ts @@ -1,6 +1,10 @@ import { and, count, desc, eq, sql, sum } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; -import { type Generation, generations, type NewGeneration } from "../schemas/generations"; +import { + type Generation, + generations, + type NewGeneration, +} from "../schemas/generations"; export type { Generation, NewGeneration }; @@ -36,7 +40,10 @@ export class GenerationsRepository { /** * Lists generations for an organization, ordered by creation date. */ - async listByOrganization(organizationId: string, limit?: number): Promise { + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { return await dbRead.query.generations.findMany({ where: eq(generations.organization_id, organizationId), orderBy: desc(generations.created_at), @@ -53,7 +60,10 @@ export class GenerationsRepository { limit?: number, ): Promise { return await dbRead.query.generations.findMany({ - where: and(eq(generations.organization_id, organizationId), eq(generations.type, type)), + where: and( + eq(generations.organization_id, organizationId), + eq(generations.type, type), + ), orderBy: desc(generations.created_at), limit, }); @@ -180,14 +190,20 @@ export class GenerationsRepository { * Creates a new generation record. */ async create(data: NewGeneration): Promise { - const [generation] = await dbWrite.insert(generations).values(data).returning(); + const [generation] = await dbWrite + .insert(generations) + .values(data) + .returning(); return generation; } /** * Updates an existing generation. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(generations) .set({ diff --git a/packages/db/repositories/jobs.ts b/packages/db/repositories/jobs.ts index 109116e24..b2fff7b1d 100644 --- a/packages/db/repositories/jobs.ts +++ b/packages/db/repositories/jobs.ts @@ -28,7 +28,11 @@ export class JobsRepository { * @returns Job record or undefined. */ async findById(id: string): Promise { - const [job] = await dbRead.select().from(jobs).where(eq(jobs.id, id)).limit(1); + const [job] = await dbRead + .select() + .from(jobs) + .where(eq(jobs.id, id)) + .limit(1); return job; } @@ -39,7 +43,10 @@ export class JobsRepository { * @param organizationId - Owning organization ID. * @returns Job record or undefined. */ - async findByIdAndOrg(id: string, organizationId: string): Promise { + async findByIdAndOrg( + id: string, + organizationId: string, + ): Promise { const [job] = await dbRead .select() .from(jobs) @@ -77,9 +84,14 @@ export class JobsRepository { // Build query in one chain to avoid TypeScript inference issues const query = dbRead.select().from(jobs).$dynamic(); - return await (conditions.length > 0 ? query.where(and(...conditions)) : query) + return await (conditions.length > 0 + ? query.where(and(...conditions)) + : query + ) .limit(filters.limit || 1000) - .orderBy(filters.orderBy === "desc" ? desc(jobs.created_at) : jobs.created_at); + .orderBy( + filters.orderBy === "desc" ? desc(jobs.created_at) : jobs.created_at, + ); } /** @@ -143,7 +155,9 @@ export class JobsRepository { dataFieldFilter, ), ) - .orderBy(filters.orderBy === "desc" ? desc(jobs.created_at) : jobs.created_at); + .orderBy( + filters.orderBy === "desc" ? desc(jobs.created_at) : jobs.created_at, + ); } // ============================================================================ @@ -292,7 +306,11 @@ export class JobsRepository { * @param status - New status. * @param additionalFields - Optional additional fields to update. */ - async updateStatus(id: string, status: string, additionalFields?: Partial): Promise { + async updateStatus( + id: string, + status: string, + additionalFields?: Partial, + ): Promise { const updates: Partial = { status, updated_at: new Date(), @@ -319,7 +337,11 @@ export class JobsRepository { * @param maxAttempts - Maximum allowed attempts. * @returns Updated job record or undefined if not found. */ - async incrementAttempt(id: string, error: string, maxAttempts: number): Promise { + async incrementAttempt( + id: string, + error: string, + maxAttempts: number, + ): Promise { const job = await this.findById(id); if (!job) return undefined; diff --git a/packages/db/repositories/milady-pairing-tokens.ts b/packages/db/repositories/milady-pairing-tokens.ts index e6946abd9..191c24504 100644 --- a/packages/db/repositories/milady-pairing-tokens.ts +++ b/packages/db/repositories/milady-pairing-tokens.ts @@ -10,7 +10,10 @@ export type { MiladyPairingToken, NewMiladyPairingToken }; export class MiladyPairingTokensRepository { async create(data: NewMiladyPairingToken): Promise { - const [row] = await dbWrite.insert(miladyPairingTokens).values(data).returning(); + const [row] = await dbWrite + .insert(miladyPairingTokens) + .values(data) + .returning(); if (!row) { throw new Error("Failed to create pairing token"); @@ -45,13 +48,20 @@ export class MiladyPairingTokensRepository { const now = new Date(); const deleted = await dbWrite .delete(miladyPairingTokens) - .where(and(lt(miladyPairingTokens.expires_at, now), isNull(miladyPairingTokens.used_at))) + .where( + and( + lt(miladyPairingTokens.expires_at, now), + isNull(miladyPairingTokens.used_at), + ), + ) .returning({ id: miladyPairingTokens.id }); return deleted.length; } - async findByTokenHash(tokenHash: string): Promise { + async findByTokenHash( + tokenHash: string, + ): Promise { const [row] = await dbRead .select() .from(miladyPairingTokens) @@ -62,4 +72,5 @@ export class MiladyPairingTokensRepository { } } -export const miladyPairingTokensRepository = new MiladyPairingTokensRepository(); +export const miladyPairingTokensRepository = + new MiladyPairingTokensRepository(); diff --git a/packages/db/repositories/milady-sandboxes.ts b/packages/db/repositories/milady-sandboxes.ts index 0d1f68b5d..8d729badf 100644 --- a/packages/db/repositories/milady-sandboxes.ts +++ b/packages/db/repositories/milady-sandboxes.ts @@ -33,20 +33,36 @@ export class MiladySandboxesRepository { return r; } - async findByIdAndOrg(id: string, orgId: string): Promise { + async findByIdAndOrg( + id: string, + orgId: string, + ): Promise { const [r] = await dbRead .select() .from(miladySandboxes) - .where(and(eq(miladySandboxes.id, id), eq(miladySandboxes.organization_id, orgId))) + .where( + and( + eq(miladySandboxes.id, id), + eq(miladySandboxes.organization_id, orgId), + ), + ) .limit(1); return r; } - async findByIdAndOrgForWrite(id: string, orgId: string): Promise { + async findByIdAndOrgForWrite( + id: string, + orgId: string, + ): Promise { const [r] = await dbWrite .select() .from(miladySandboxes) - .where(and(eq(miladySandboxes.id, id), eq(miladySandboxes.organization_id, orgId))) + .where( + and( + eq(miladySandboxes.id, id), + eq(miladySandboxes.organization_id, orgId), + ), + ) .limit(1); return r; } @@ -82,7 +98,10 @@ export class MiladySandboxesRepository { ); } - async findRunningSandbox(id: string, orgId: string): Promise { + async findRunningSandbox( + id: string, + orgId: string, + ): Promise { // Use dbWrite (primary) instead of dbRead (replica) to ensure fresh data. // The VPS worker writes bridge_url/status to primary, and read replicas // may lag behind, causing the wallet proxy to return "not running". @@ -124,7 +143,10 @@ export class MiladySandboxesRepository { return r; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [r] = await dbWrite .update(miladySandboxes) .set({ ...data, updated_at: new Date() }) @@ -155,20 +177,33 @@ export class MiladySandboxesRepository { async delete(id: string, orgId: string): Promise { const r = await dbWrite .delete(miladySandboxes) - .where(and(eq(miladySandboxes.id, id), eq(miladySandboxes.organization_id, orgId))) + .where( + and( + eq(miladySandboxes.id, id), + eq(miladySandboxes.organization_id, orgId), + ), + ) .returning({ id: miladySandboxes.id }); return r.length > 0; } // Backups - async createBackup(data: NewMiladySandboxBackup): Promise { - const [r] = await dbWrite.insert(miladySandboxBackups).values(data).returning(); + async createBackup( + data: NewMiladySandboxBackup, + ): Promise { + const [r] = await dbWrite + .insert(miladySandboxBackups) + .values(data) + .returning(); if (!r) throw new Error("Failed to create backup"); return r; } - async listBackups(sandboxRecordId: string, limit = 10): Promise { + async listBackups( + sandboxRecordId: string, + limit = 10, + ): Promise { return dbRead .select() .from(miladySandboxBackups) @@ -177,7 +212,9 @@ export class MiladySandboxesRepository { .limit(limit); } - async getLatestBackup(sandboxRecordId: string): Promise { + async getLatestBackup( + sandboxRecordId: string, + ): Promise { const [r] = await dbRead .select() .from(miladySandboxBackups) @@ -187,7 +224,9 @@ export class MiladySandboxesRepository { return r; } - async getBackupById(backupId: string): Promise { + async getBackupById( + backupId: string, + ): Promise { const [r] = await dbRead .select() .from(miladySandboxBackups) diff --git a/packages/db/repositories/model-pricing.ts b/packages/db/repositories/model-pricing.ts index d5726668c..c97516133 100644 --- a/packages/db/repositories/model-pricing.ts +++ b/packages/db/repositories/model-pricing.ts @@ -1,6 +1,10 @@ import { and, eq } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; -import { type ModelPricing, modelPricing, type NewModelPricing } from "../schemas/model-pricing"; +import { + type ModelPricing, + modelPricing, + type NewModelPricing, +} from "../schemas/model-pricing"; export type { ModelPricing, NewModelPricing }; @@ -15,7 +19,10 @@ export class ModelPricingRepository { /** * Finds active pricing for a model and provider combination. */ - async findByModelAndProvider(model: string, provider: string): Promise { + async findByModelAndProvider( + model: string, + provider: string, + ): Promise { return await dbRead.query.modelPricing.findFirst({ where: and( eq(modelPricing.model, model), @@ -51,14 +58,20 @@ export class ModelPricingRepository { * Creates a new model pricing record. */ async create(data: NewModelPricing): Promise { - const [pricing] = await dbWrite.insert(modelPricing).values(data).returning(); + const [pricing] = await dbWrite + .insert(modelPricing) + .values(data) + .returning(); return pricing; } /** * Updates an existing model pricing record. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(modelPricing) .set({ diff --git a/packages/db/repositories/org-rate-limit-overrides.ts b/packages/db/repositories/org-rate-limit-overrides.ts index 7b32db9a9..bb615e8cb 100644 --- a/packages/db/repositories/org-rate-limit-overrides.ts +++ b/packages/db/repositories/org-rate-limit-overrides.ts @@ -15,7 +15,9 @@ export type { NewOrgRateLimitOverride, OrgRateLimitOverride }; * Write operations → dbWrite (primary) */ export class OrgRateLimitOverridesRepository { - async findByOrganizationId(organizationId: string): Promise { + async findByOrganizationId( + organizationId: string, + ): Promise { return await dbRead.query.orgRateLimitOverrides.findFirst({ where: eq(orgRateLimitOverrides.organization_id, organizationId), }); @@ -26,7 +28,11 @@ export class OrgRateLimitOverridesRepository { Partial< Pick< NewOrgRateLimitOverride, - "completions_rpm" | "embeddings_rpm" | "standard_rpm" | "strict_rpm" | "note" + | "completions_rpm" + | "embeddings_rpm" + | "standard_rpm" + | "strict_rpm" + | "note" > >, ): Promise { @@ -61,4 +67,5 @@ export class OrgRateLimitOverridesRepository { } } -export const orgRateLimitOverridesRepository = new OrgRateLimitOverridesRepository(); +export const orgRateLimitOverridesRepository = + new OrgRateLimitOverridesRepository(); diff --git a/packages/db/repositories/organization-invites.ts b/packages/db/repositories/organization-invites.ts index 75a7ea92e..981002f16 100644 --- a/packages/db/repositories/organization-invites.ts +++ b/packages/db/repositories/organization-invites.ts @@ -31,7 +31,9 @@ export class OrganizationInvitesRepository { /** * Finds an organization invite by token hash with organization and inviter data. */ - async findByTokenHash(tokenHash: string): Promise { + async findByTokenHash( + tokenHash: string, + ): Promise { return await dbRead.query.organizationInvites.findFirst({ where: eq(organizationInvites.token_hash, tokenHash), with: { @@ -44,7 +46,9 @@ export class OrganizationInvitesRepository { /** * Finds a pending invite by email address (case-insensitive). */ - async findPendingInviteByEmail(email: string): Promise { + async findPendingInviteByEmail( + email: string, + ): Promise { return await dbRead.query.organizationInvites.findFirst({ where: and( eq(organizationInvites.invited_email, email.toLowerCase()), @@ -59,7 +63,9 @@ export class OrganizationInvitesRepository { /** * Lists all invites for an organization with inviter information. */ - async listByOrganization(organizationId: string): Promise { + async listByOrganization( + organizationId: string, + ): Promise { return await dbRead.query.organizationInvites.findMany({ where: eq(organizationInvites.organization_id, organizationId), with: { @@ -78,7 +84,9 @@ export class OrganizationInvitesRepository { /** * Lists pending invites for an organization with inviter information. */ - async listPendingByOrganization(organizationId: string): Promise { + async listPendingByOrganization( + organizationId: string, + ): Promise { return await dbRead.query.organizationInvites.findMany({ where: and( eq(organizationInvites.organization_id, organizationId), @@ -105,7 +113,10 @@ export class OrganizationInvitesRepository { * Creates a new organization invite. */ async create(data: NewOrganizationInvite): Promise { - const [invite] = await dbWrite.insert(organizationInvites).values(data).returning(); + const [invite] = await dbWrite + .insert(organizationInvites) + .values(data) + .returning(); return invite; } @@ -163,11 +174,14 @@ export class OrganizationInvitesRepository { * Deletes an organization invite by ID. */ async delete(id: string): Promise { - await dbWrite.delete(organizationInvites).where(eq(organizationInvites.id, id)); + await dbWrite + .delete(organizationInvites) + .where(eq(organizationInvites.id, id)); } } /** * Singleton instance of OrganizationInvitesRepository. */ -export const organizationInvitesRepository = new OrganizationInvitesRepository(); +export const organizationInvitesRepository = + new OrganizationInvitesRepository(); diff --git a/packages/db/repositories/organizations.ts b/packages/db/repositories/organizations.ts index 8f15fcee6..fa0e11820 100644 --- a/packages/db/repositories/organizations.ts +++ b/packages/db/repositories/organizations.ts @@ -3,7 +3,11 @@ import { dbRead, dbWrite } from "../helpers"; import type { CreditTransaction } from "../schemas/credit-transactions"; import { creditTransactions } from "../schemas/credit-transactions"; import { organizationBilling } from "../schemas/organization-billing"; -import { type NewOrganization, type Organization, organizations } from "../schemas/organizations"; +import { + type NewOrganization, + type Organization, + organizations, +} from "../schemas/organizations"; export type { NewOrganization, Organization }; @@ -39,7 +43,9 @@ export class OrganizationsRepository { /** * Finds an organization by Stripe customer ID (via billing table). */ - async findByStripeCustomerId(stripeCustomerId: string): Promise { + async findByStripeCustomerId( + stripeCustomerId: string, + ): Promise { const billing = await dbRead.query.organizationBilling.findFirst({ where: eq(organizationBilling.stripe_customer_id, stripeCustomerId), }); @@ -67,14 +73,20 @@ export class OrganizationsRepository { * Creates a new organization. */ async create(data: NewOrganization): Promise { - const [organization] = await dbWrite.insert(organizations).values(data).returning(); + const [organization] = await dbWrite + .insert(organizations) + .values(data) + .returning(); return organization; } /** * Updates an existing organization. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(organizations) .set({ diff --git a/packages/db/repositories/provider-health.ts b/packages/db/repositories/provider-health.ts index 6230e43ed..39277a6b9 100644 --- a/packages/db/repositories/provider-health.ts +++ b/packages/db/repositories/provider-health.ts @@ -58,7 +58,10 @@ export class ProviderHealthRepository { return updated; } - const [created] = await dbWrite.insert(providerHealth).values(data).returning(); + const [created] = await dbWrite + .insert(providerHealth) + .values(data) + .returning(); return created; } diff --git a/packages/db/repositories/referrals.ts b/packages/db/repositories/referrals.ts index faaddae34..0e20235e6 100644 --- a/packages/db/repositories/referrals.ts +++ b/packages/db/repositories/referrals.ts @@ -64,7 +64,10 @@ class ReferralCodesRepository { * Creates a new referral code. */ async create(data: NewReferralCode): Promise { - const [result] = await dbWrite.insert(referralCodes).values(data).returning(); + const [result] = await dbWrite + .insert(referralCodes) + .values(data) + .returning(); return result; } @@ -152,7 +155,10 @@ class ReferralSignupsRepository { /** * Lists referral signups for a referrer, ordered by creation date. */ - async listByReferrerId(referrerId: string, limit = 50): Promise { + async listByReferrerId( + referrerId: string, + limit = 50, + ): Promise { return dbRead .select() .from(referralSignups) @@ -164,7 +170,9 @@ class ReferralSignupsRepository { /** * Finds an unqualified referral signup for a user. */ - async findUnqualifiedByReferredUserId(userId: string): Promise { + async findUnqualifiedByReferredUserId( + userId: string, + ): Promise { const [result] = await dbRead .select() .from(referralSignups) @@ -186,14 +194,20 @@ class ReferralSignupsRepository { * Creates a new referral signup record. */ async create(data: NewReferralSignup): Promise { - const [result] = await dbWrite.insert(referralSignups).values(data).returning(); + const [result] = await dbWrite + .insert(referralSignups) + .values(data) + .returning(); return result; } /** * Marks signup bonus as credited for a referral signup. */ - async markBonusCredited(id: string, amount: number): Promise { + async markBonusCredited( + id: string, + amount: number, + ): Promise { const [result] = await dbWrite .update(referralSignups) .set({ @@ -220,7 +234,10 @@ class ReferralSignupsRepository { /** * Marks a referral as qualified and credits the qualified bonus. */ - async markQualified(id: string, amount: number): Promise { + async markQualified( + id: string, + amount: number, + ): Promise { const [result] = await dbWrite .update(referralSignups) .set({ @@ -228,7 +245,12 @@ class ReferralSignupsRepository { qualified_bonus_credited: true, qualified_bonus_amount: String(amount), }) - .where(and(eq(referralSignups.id, id), sql`${referralSignups.qualified_at} IS NULL`)) + .where( + and( + eq(referralSignups.id, id), + sql`${referralSignups.qualified_at} IS NULL`, + ), + ) .returning(); return result || null; } diff --git a/packages/db/repositories/remote-sessions.ts b/packages/db/repositories/remote-sessions.ts index a87f887db..87ec92635 100644 --- a/packages/db/repositories/remote-sessions.ts +++ b/packages/db/repositories/remote-sessions.ts @@ -20,16 +20,27 @@ export class RemoteSessionsRepository { return row; } - async findByIdAndOrg(id: string, orgId: string): Promise { + async findByIdAndOrg( + id: string, + orgId: string, + ): Promise { const [row] = await dbRead .select() .from(remoteSessions) - .where(and(eq(remoteSessions.id, id), eq(remoteSessions.organization_id, orgId))) + .where( + and( + eq(remoteSessions.id, id), + eq(remoteSessions.organization_id, orgId), + ), + ) .limit(1); return row; } - async listActiveByAgent(agentId: string, orgId: string): Promise { + async listActiveByAgent( + agentId: string, + orgId: string, + ): Promise { return dbRead .select() .from(remoteSessions) diff --git a/packages/db/repositories/secrets.ts b/packages/db/repositories/secrets.ts index 14f964f0c..a7fc4220f 100644 --- a/packages/db/repositories/secrets.ts +++ b/packages/db/repositories/secrets.ts @@ -50,7 +50,10 @@ class SecretsRepository { projectId?: string, environment?: SecretEnvironment, ): Promise { - const conditions = [eq(secrets.organization_id, organizationId), eq(secrets.name, name)]; + const conditions = [ + eq(secrets.organization_id, organizationId), + eq(secrets.name, name), + ]; if (projectId) { conditions.push(eq(secrets.project_id, projectId)); @@ -102,7 +105,9 @@ class SecretsRepository { .where( and( eq(secretBindings.project_id, projectId), - projectType ? eq(secretBindings.project_type, projectType) : sql`1=1`, + projectType + ? eq(secretBindings.project_type, projectType) + : sql`1=1`, ), ); @@ -114,13 +119,20 @@ class SecretsRepository { )!, ); } else if (projectId) { - conditions.push(sql`(${secrets.project_id} = ${projectId} OR ${secrets.project_id} IS NULL)`); + conditions.push( + sql`(${secrets.project_id} = ${projectId} OR ${secrets.project_id} IS NULL)`, + ); } else { conditions.push(isNull(secrets.project_id)); } if (projectType && !includeBindings) { - conditions.push(or(eq(secrets.project_type, projectType), isNull(secrets.project_type))!); + conditions.push( + or( + eq(secrets.project_type, projectType), + isNull(secrets.project_type), + )!, + ); } if (environment) { @@ -215,10 +227,17 @@ class SecretsRepository { } async listByProject(projectId: string): Promise { - return db.select().from(secrets).where(eq(secrets.project_id, projectId)).orderBy(secrets.name); + return db + .select() + .from(secrets) + .where(eq(secrets.project_id, projectId)) + .orderBy(secrets.name); } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [secret] = await db .update(secrets) .set({ ...data, updated_at: new Date() }) @@ -228,7 +247,10 @@ class SecretsRepository { } async delete(id: string): Promise { - const result = await db.delete(secrets).where(eq(secrets.id, id)).returning({ id: secrets.id }); + const result = await db + .delete(secrets) + .where(eq(secrets.id, id)) + .returning({ id: secrets.id }); return result.length > 0; } @@ -249,7 +271,9 @@ class SecretsRepository { return db .select() .from(secrets) - .where(and(gte(secrets.expires_at, now), lte(secrets.expires_at, deadline))) + .where( + and(gte(secrets.expires_at, now), lte(secrets.expires_at, deadline)), + ) .orderBy(secrets.expires_at); } } @@ -261,7 +285,10 @@ class OAuthSessionsRepository { } async findById(id: string): Promise { - const [session] = await db.select().from(oauthSessions).where(eq(oauthSessions.id, id)); + const [session] = await db + .select() + .from(oauthSessions) + .where(eq(oauthSessions.id, id)); return session; } @@ -296,7 +323,10 @@ class OAuthSessionsRepository { .orderBy(oauthSessions.provider); } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [session] = await db .update(oauthSessions) .set({ ...data, updated_at: new Date() }) @@ -376,7 +406,10 @@ class SecretAuditLogRepository { .limit(limit); } - async findByOrganization(organizationId: string, limit = 100): Promise { + async findByOrganization( + organizationId: string, + limit = 100, + ): Promise { return db .select() .from(secretAuditLog) @@ -405,7 +438,11 @@ class SecretAuditLogRepository { .limit(limit); } - async findByActor(actorType: string, actorId: string, limit = 100): Promise { + async findByActor( + actorType: string, + actorId: string, + limit = 100, + ): Promise { return db .select() .from(secretAuditLog) @@ -413,7 +450,12 @@ class SecretAuditLogRepository { and( eq( secretAuditLog.actor_type, - actorType as "user" | "api_key" | "system" | "deployment" | "workflow", + actorType as + | "user" + | "api_key" + | "system" + | "deployment" + | "workflow", ), eq(secretAuditLog.actor_id, actorId), ), @@ -435,20 +477,34 @@ class SecretBindingsRepository { } async findById(id: string): Promise { - const [binding] = await db.select().from(secretBindings).where(eq(secretBindings.id, id)); + const [binding] = await db + .select() + .from(secretBindings) + .where(eq(secretBindings.id, id)); return binding; } - async findByIdAndOrg(id: string, organizationId: string): Promise { + async findByIdAndOrg( + id: string, + organizationId: string, + ): Promise { const [binding] = await db .select() .from(secretBindings) - .where(and(eq(secretBindings.id, id), eq(secretBindings.organization_id, organizationId))); + .where( + and( + eq(secretBindings.id, id), + eq(secretBindings.organization_id, organizationId), + ), + ); return binding; } async findBySecret(secretId: string): Promise { - return db.select().from(secretBindings).where(eq(secretBindings.secret_id, secretId)); + return db + .select() + .from(secretBindings) + .where(eq(secretBindings.secret_id, secretId)); } async findByProject( @@ -529,7 +585,10 @@ class SecretBindingsRepository { return result.length; } - async deleteByProject(projectId: string, projectType?: SecretProjectType): Promise { + async deleteByProject( + projectId: string, + projectType?: SecretProjectType, + ): Promise { const conditions = [eq(secretBindings.project_id, projectId)]; if (projectType) { conditions.push(eq(secretBindings.project_type, projectType)); @@ -544,11 +603,16 @@ class SecretBindingsRepository { class AppSecretRequirementsRepository { async create(data: NewAppSecretRequirement): Promise { - const [req] = await db.insert(appSecretRequirements).values(data).returning(); + const [req] = await db + .insert(appSecretRequirements) + .values(data) + .returning(); return req; } - async createMany(data: NewAppSecretRequirement[]): Promise { + async createMany( + data: NewAppSecretRequirement[], + ): Promise { if (data.length === 0) return []; return db.insert(appSecretRequirements).values(data).returning(); } @@ -573,7 +637,12 @@ class AppSecretRequirementsRepository { return db .select() .from(appSecretRequirements) - .where(and(eq(appSecretRequirements.app_id, appId), eq(appSecretRequirements.approved, true))) + .where( + and( + eq(appSecretRequirements.app_id, appId), + eq(appSecretRequirements.approved, true), + ), + ) .orderBy(appSecretRequirements.secret_name); } @@ -593,7 +662,10 @@ class AppSecretRequirementsRepository { return req; } - async approve(id: string, approvedBy: string): Promise { + async approve( + id: string, + approvedBy: string, + ): Promise { const [req] = await db .update(appSecretRequirements) .set({ @@ -683,7 +755,8 @@ export const secretsRepository = new SecretsRepository(); export const oauthSessionsRepository = new OAuthSessionsRepository(); export const secretAuditLogRepository = new SecretAuditLogRepository(); export const secretBindingsRepository = new SecretBindingsRepository(); -export const appSecretRequirementsRepository = new AppSecretRequirementsRepository(); +export const appSecretRequirementsRepository = + new AppSecretRequirementsRepository(); export type { AppSecretRequirement, diff --git a/packages/db/repositories/seo-artifacts.ts b/packages/db/repositories/seo-artifacts.ts index 2ce4ec95c..313f92966 100644 --- a/packages/db/repositories/seo-artifacts.ts +++ b/packages/db/repositories/seo-artifacts.ts @@ -16,7 +16,10 @@ export class SeoArtifactsRepository { ): Promise { return await db.query.seoArtifacts.findMany({ where: options?.type - ? and(eq(seoArtifacts.request_id, requestId), eq(seoArtifacts.type, options.type)) + ? and( + eq(seoArtifacts.request_id, requestId), + eq(seoArtifacts.type, options.type), + ) : eq(seoArtifacts.request_id, requestId), orderBy: desc(seoArtifacts.created_at), }); diff --git a/packages/db/repositories/seo-provider-calls.ts b/packages/db/repositories/seo-provider-calls.ts index 7eb618e4d..282d962ef 100644 --- a/packages/db/repositories/seo-provider-calls.ts +++ b/packages/db/repositories/seo-provider-calls.ts @@ -1,6 +1,10 @@ import { desc, eq } from "drizzle-orm"; import { db } from "../client"; -import { type NewSeoProviderCall, type SeoProviderCall, seoProviderCalls } from "../schemas/seo"; +import { + type NewSeoProviderCall, + type SeoProviderCall, + seoProviderCalls, +} from "../schemas/seo"; export type { NewSeoProviderCall, SeoProviderCall }; diff --git a/packages/db/repositories/service-pricing.ts b/packages/db/repositories/service-pricing.ts index 1ebde6006..c22516f21 100644 --- a/packages/db/repositories/service-pricing.ts +++ b/packages/db/repositories/service-pricing.ts @@ -9,11 +9,18 @@ import { servicePricingAudit, } from "../schemas/service-pricing"; -export type { NewServicePricing, NewServicePricingAudit, ServicePricing, ServicePricingAudit }; +export type { + NewServicePricing, + NewServicePricingAudit, + ServicePricing, + ServicePricingAudit, +}; type PricingMetadata = NewServicePricing["metadata"]; -function normalizeMetadata(metadata?: Record): PricingMetadata { +function normalizeMetadata( + metadata?: Record, +): PricingMetadata { if (!metadata) { return {}; } @@ -31,7 +38,9 @@ function normalizeMetadata(metadata?: Record): PricingMetadata continue; } - throw new Error(`Metadata value for key '${key}' must be a string, number, boolean, or null`); + throw new Error( + `Metadata value for key '${key}' must be a string, number, boolean, or null`, + ); } return normalized; @@ -58,7 +67,10 @@ export class ServicePricingRepository { * @param activeOnly - If true, only return active methods (default: true) * @returns Array of service pricing records */ - async listByService(serviceId: string, activeOnly: boolean = true): Promise { + async listByService( + serviceId: string, + activeOnly: boolean = true, + ): Promise { const conditions = [eq(servicePricing.service_id, serviceId)]; if (activeOnly) { @@ -89,11 +101,15 @@ export class ServicePricingRepository { } for (const key of keys) { if (key.length > 100) { - throw new Error(`Metadata key exceeds 100 character limit: ${key.substring(0, 20)}...`); + throw new Error( + `Metadata key exceeds 100 character limit: ${key.substring(0, 20)}...`, + ); } const val = metadata[key]; if (typeof val === "string" && val.length > 1000) { - throw new Error(`Metadata value for key '${key}' exceeds 1000 character limit`); + throw new Error( + `Metadata value for key '${key}' exceeds 1000 character limit`, + ); } } } diff --git a/packages/db/repositories/telegram-chats.ts b/packages/db/repositories/telegram-chats.ts index 96c749f31..41d501e5d 100644 --- a/packages/db/repositories/telegram-chats.ts +++ b/packages/db/repositories/telegram-chats.ts @@ -15,19 +15,28 @@ class TelegramChatsRepository { .orderBy(desc(telegramChats.created_at)); } - async findByChatId(organizationId: string, chatId: number): Promise { + async findByChatId( + organizationId: string, + chatId: number, + ): Promise { const results = await dbRead .select() .from(telegramChats) .where( - and(eq(telegramChats.organization_id, organizationId), eq(telegramChats.chat_id, chatId)), + and( + eq(telegramChats.organization_id, organizationId), + eq(telegramChats.chat_id, chatId), + ), ) .limit(1); return results[0]; } async upsert(data: NewTelegramChat): Promise { - const existing = await this.findByChatId(data.organization_id, data.chat_id); + const existing = await this.findByChatId( + data.organization_id, + data.chat_id, + ); if (existing) { const [updated] = await dbWrite @@ -45,7 +54,10 @@ class TelegramChatsRepository { return updated; } - const [created] = await dbWrite.insert(telegramChats).values(data).returning(); + const [created] = await dbWrite + .insert(telegramChats) + .values(data) + .returning(); return created; } @@ -53,12 +65,17 @@ class TelegramChatsRepository { await dbWrite .delete(telegramChats) .where( - and(eq(telegramChats.organization_id, organizationId), eq(telegramChats.chat_id, chatId)), + and( + eq(telegramChats.organization_id, organizationId), + eq(telegramChats.chat_id, chatId), + ), ); } async deleteByOrganization(organizationId: string): Promise { - await dbWrite.delete(telegramChats).where(eq(telegramChats.organization_id, organizationId)); + await dbWrite + .delete(telegramChats) + .where(eq(telegramChats.organization_id, organizationId)); } } diff --git a/packages/db/repositories/token-redemptions.ts b/packages/db/repositories/token-redemptions.ts index 994f6e4c0..b2b74ae6b 100644 --- a/packages/db/repositories/token-redemptions.ts +++ b/packages/db/repositories/token-redemptions.ts @@ -44,9 +44,15 @@ export class TokenRedemptionsRepository { /** * Finds a redemption by ID and user ID (for security). */ - async findByIdAndUser(id: string, userId: string): Promise { + async findByIdAndUser( + id: string, + userId: string, + ): Promise { return await dbRead.query.tokenRedemptions.findFirst({ - where: and(eq(tokenRedemptions.id, id), eq(tokenRedemptions.user_id, userId)), + where: and( + eq(tokenRedemptions.id, id), + eq(tokenRedemptions.user_id, userId), + ), }); } @@ -66,7 +72,10 @@ export class TokenRedemptionsRepository { */ async hasPendingRedemption(userId: string): Promise { const pending = await dbRead.query.tokenRedemptions.findFirst({ - where: and(eq(tokenRedemptions.user_id, userId), eq(tokenRedemptions.status, "pending")), + where: and( + eq(tokenRedemptions.user_id, userId), + eq(tokenRedemptions.status, "pending"), + ), }); return !!pending; } @@ -120,7 +129,10 @@ export class TokenRedemptionsRepository { * Creates a new token redemption request. */ async create(data: NewTokenRedemption): Promise { - const [redemption] = await dbWrite.insert(tokenRedemptions).values(data).returning(); + const [redemption] = await dbWrite + .insert(tokenRedemptions) + .values(data) + .returning(); return redemption; } @@ -128,7 +140,10 @@ export class TokenRedemptionsRepository { * Acquires processing lock on a redemption. * Returns true if lock was acquired, false if already locked. */ - async acquireProcessingLock(redemptionId: string, workerId: string): Promise { + async acquireProcessingLock( + redemptionId: string, + workerId: string, + ): Promise { const [updated] = await dbWrite .update(tokenRedemptions) .set({ @@ -137,7 +152,12 @@ export class TokenRedemptionsRepository { processing_worker_id: workerId, updated_at: new Date(), }) - .where(and(eq(tokenRedemptions.id, redemptionId), eq(tokenRedemptions.status, "approved"))) + .where( + and( + eq(tokenRedemptions.id, redemptionId), + eq(tokenRedemptions.status, "approved"), + ), + ) .returning(); return !!updated; @@ -162,7 +182,11 @@ export class TokenRedemptionsRepository { * Marks a redemption as failed with reason. * If retryable, resets to approved for retry. */ - async markFailed(redemptionId: string, reason: string, retryable: boolean): Promise { + async markFailed( + redemptionId: string, + reason: string, + retryable: boolean, + ): Promise { if (retryable) { await dbWrite .update(tokenRedemptions) @@ -190,7 +214,11 @@ export class TokenRedemptionsRepository { /** * Admin: Approves a pending redemption. */ - async approve(redemptionId: string, reviewerId: string, notes?: string): Promise { + async approve( + redemptionId: string, + reviewerId: string, + notes?: string, + ): Promise { const [updated] = await dbWrite .update(tokenRedemptions) .set({ @@ -200,7 +228,12 @@ export class TokenRedemptionsRepository { review_notes: notes, updated_at: new Date(), }) - .where(and(eq(tokenRedemptions.id, redemptionId), eq(tokenRedemptions.status, "pending"))) + .where( + and( + eq(tokenRedemptions.id, redemptionId), + eq(tokenRedemptions.status, "pending"), + ), + ) .returning(); return !!updated; @@ -209,7 +242,11 @@ export class TokenRedemptionsRepository { /** * Admin: Rejects a pending redemption. */ - async reject(redemptionId: string, reviewerId: string, reason: string): Promise { + async reject( + redemptionId: string, + reviewerId: string, + reason: string, + ): Promise { const [updated] = await dbWrite .update(tokenRedemptions) .set({ @@ -220,7 +257,12 @@ export class TokenRedemptionsRepository { review_notes: reason, updated_at: new Date(), }) - .where(and(eq(tokenRedemptions.id, redemptionId), eq(tokenRedemptions.status, "pending"))) + .where( + and( + eq(tokenRedemptions.id, redemptionId), + eq(tokenRedemptions.status, "pending"), + ), + ) .returning(); return !!updated; @@ -242,7 +284,10 @@ export class RedemptionLimitsRepository { today.setHours(0, 0, 0, 0); const existing = await dbRead.query.redemptionLimits.findFirst({ - where: and(eq(redemptionLimits.user_id, userId), gte(redemptionLimits.date, today)), + where: and( + eq(redemptionLimits.user_id, userId), + gte(redemptionLimits.date, today), + ), }); if (existing) { @@ -261,7 +306,10 @@ export class RedemptionLimitsRepository { if (!created) { // Race condition - another request created it, use write DB to avoid replication lag const refetched = await dbWrite.query.redemptionLimits.findFirst({ - where: and(eq(redemptionLimits.user_id, userId), gte(redemptionLimits.date, today)), + where: and( + eq(redemptionLimits.user_id, userId), + gte(redemptionLimits.date, today), + ), }); if (!refetched) { throw new Error("Failed to create or find redemption limits"); @@ -312,7 +360,10 @@ export class ElizaTokenPricesRepository { /** * Gets the most recent cached price for a network. */ - async getLatest(network: string, maxAgeMs: number): Promise { + async getLatest( + network: string, + maxAgeMs: number, + ): Promise { const minFetchedAt = new Date(Date.now() - maxAgeMs); return await dbRead.query.elizaTokenPrices.findFirst({ @@ -332,7 +383,10 @@ export class ElizaTokenPricesRepository { * Caches a new price. */ async cache(data: NewElizaTokenPrice): Promise { - const [price] = await dbWrite.insert(elizaTokenPrices).values(data).returning(); + const [price] = await dbWrite + .insert(elizaTokenPrices) + .values(data) + .returning(); return price; } diff --git a/packages/db/repositories/usage-quotas.ts b/packages/db/repositories/usage-quotas.ts index 9df216a4d..c83954942 100644 --- a/packages/db/repositories/usage-quotas.ts +++ b/packages/db/repositories/usage-quotas.ts @@ -1,6 +1,10 @@ import { and, eq, lte, sql } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; -import { type NewUsageQuota, type UsageQuota, usageQuotas } from "../schemas/usage-quotas"; +import { + type NewUsageQuota, + type UsageQuota, + usageQuotas, +} from "../schemas/usage-quotas"; export type { NewUsageQuota, UsageQuota }; @@ -33,9 +37,14 @@ export class UsageQuotasRepository { /** * Lists active usage quotas for an organization. */ - async findActiveByOrganization(organizationId: string): Promise { + async findActiveByOrganization( + organizationId: string, + ): Promise { return await dbRead.query.usageQuotas.findMany({ - where: and(eq(usageQuotas.organization_id, organizationId), eq(usageQuotas.is_active, true)), + where: and( + eq(usageQuotas.organization_id, organizationId), + eq(usageQuotas.is_active, true), + ), }); } @@ -85,7 +94,10 @@ export class UsageQuotasRepository { async listExpiredQuotas(): Promise { const now = new Date(); return await dbRead.query.usageQuotas.findMany({ - where: and(eq(usageQuotas.is_active, true), lte(usageQuotas.period_end, now)), + where: and( + eq(usageQuotas.is_active, true), + lte(usageQuotas.period_end, now), + ), }); } @@ -135,7 +147,10 @@ export class UsageQuotasRepository { /** * Updates an existing usage quota. */ - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const [updated] = await dbWrite .update(usageQuotas) .set({ @@ -165,7 +180,10 @@ export class UsageQuotasRepository { /** * Atomically increments usage count for a quota. */ - async incrementUsage(id: string, amount: number): Promise { + async incrementUsage( + id: string, + amount: number, + ): Promise { const [updated] = await dbWrite .update(usageQuotas) .set({ diff --git a/packages/db/repositories/usage-records.ts b/packages/db/repositories/usage-records.ts index 8bcd001b8..c6e0607d2 100644 --- a/packages/db/repositories/usage-records.ts +++ b/packages/db/repositories/usage-records.ts @@ -1,7 +1,11 @@ import { and, desc, eq, gte, lte, type SQL, sql } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; import { organizations } from "../schemas/organizations"; -import { type NewUsageRecord, type UsageRecord, usageRecords } from "../schemas/usage-records"; +import { + type NewUsageRecord, + type UsageRecord, + usageRecords, +} from "../schemas/usage-records"; import { users } from "../schemas/users"; export type { NewUsageRecord, UsageRecord }; @@ -128,7 +132,10 @@ export class UsageRecordsRepository { /** * Lists usage records for an organization, ordered by creation date. */ - async listByOrganization(organizationId: string, limit?: number): Promise { + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { return await dbRead.query.usageRecords.findMany({ where: eq(usageRecords.organization_id, organizationId), orderBy: desc(usageRecords.created_at), @@ -162,7 +169,9 @@ export class UsageRecordsRepository { startDate?: Date, endDate?: Date, ): Promise { - const conditions: SQL[] = [eq(usageRecords.organization_id, organizationId)]; + const conditions: SQL[] = [ + eq(usageRecords.organization_id, organizationId), + ]; if (startDate) { conditions.push(sql`${usageRecords.created_at} >= ${startDate}`); @@ -306,7 +315,9 @@ export class UsageRecordsRepository { ): Promise { const { startDate, endDate, limit = 50 } = options || {}; - const conditions: SQL[] = [eq(usageRecords.organization_id, organizationId)]; + const conditions: SQL[] = [ + eq(usageRecords.organization_id, organizationId), + ]; if (startDate) { conditions.push(sql`${usageRecords.created_at} >= ${startDate}`); @@ -330,7 +341,11 @@ export class UsageRecordsRepository { .leftJoin(users, eq(usageRecords.user_id, users.id)) .where(and(...conditions)) .groupBy(usageRecords.user_id, users.name, users.email) - .orderBy(desc(sql`sum(${usageRecords.input_cost} + ${usageRecords.output_cost})`)) + .orderBy( + desc( + sql`sum(${usageRecords.input_cost} + ${usageRecords.output_cost})`, + ), + ) .limit(limit); return result @@ -374,7 +389,9 @@ export class UsageRecordsRepository { const projectedMonthlyBurn = currentDailyBurn * 30; const creditBalance = orgData?.credit_balance || 0; const daysUntilBalanceZero = - currentDailyBurn > 0 ? Math.floor(Number(creditBalance) / currentDailyBurn) : null; + currentDailyBurn > 0 + ? Math.floor(Number(creditBalance) / currentDailyBurn) + : null; return { currentDailyBurn, @@ -392,7 +409,9 @@ export class UsageRecordsRepository { organizationId: string, options?: { startDate?: Date; endDate?: Date }, ): Promise { - const conditions: SQL[] = [eq(usageRecords.organization_id, organizationId)]; + const conditions: SQL[] = [ + eq(usageRecords.organization_id, organizationId), + ]; if (options?.startDate) { conditions.push(sql`${usageRecords.created_at} >= ${options.startDate}`); @@ -416,9 +435,16 @@ export class UsageRecordsRepository { .from(usageRecords) .where(and(...conditions)) .groupBy(usageRecords.provider) - .orderBy(desc(sql`sum(${usageRecords.input_cost} + ${usageRecords.output_cost})`)); + .orderBy( + desc( + sql`sum(${usageRecords.input_cost} + ${usageRecords.output_cost})`, + ), + ); - const totalCost = result.reduce((sum, row) => sum + Number(row.totalCost || 0), 0); + const totalCost = result.reduce( + (sum, row) => sum + Number(row.totalCost || 0), + 0, + ); return result.map((row) => ({ provider: row.provider, @@ -426,7 +452,8 @@ export class UsageRecordsRepository { totalCost: Number(row.totalCost || 0), totalTokens: row.totalTokens, successRate: row.successRate, - percentage: totalCost > 0 ? (Number(row.totalCost || 0) / totalCost) * 100 : 0, + percentage: + totalCost > 0 ? (Number(row.totalCost || 0) / totalCost) * 100 : 0, })); } @@ -439,7 +466,9 @@ export class UsageRecordsRepository { ): Promise { const { startDate, endDate, limit = 50 } = options || {}; - const conditions: SQL[] = [eq(usageRecords.organization_id, organizationId)]; + const conditions: SQL[] = [ + eq(usageRecords.organization_id, organizationId), + ]; if (startDate) { conditions.push(sql`${usageRecords.created_at} >= ${startDate}`); @@ -464,7 +493,11 @@ export class UsageRecordsRepository { .from(usageRecords) .where(and(...conditions)) .groupBy(usageRecords.model, usageRecords.provider) - .orderBy(desc(sql`sum(${usageRecords.input_cost} + ${usageRecords.output_cost})`)) + .orderBy( + desc( + sql`sum(${usageRecords.input_cost} + ${usageRecords.output_cost})`, + ), + ) .limit(limit); return result.map((row) => ({ @@ -473,7 +506,8 @@ export class UsageRecordsRepository { totalRequests: row.totalRequests, totalCost: row.totalCost, totalTokens: row.totalTokens, - avgCostPerToken: row.totalTokens > 0 ? row.totalCost / row.totalTokens : 0, + avgCostPerToken: + row.totalTokens > 0 ? row.totalCost / row.totalTokens : 0, successRate: row.successRate, })); } @@ -487,8 +521,16 @@ export class UsageRecordsRepository { previousPeriod: { startDate: Date; endDate: Date }, ): Promise { const [currentStats, previousStats] = await Promise.all([ - this.getStatsByOrganization(organizationId, currentPeriod.startDate, currentPeriod.endDate), - this.getStatsByOrganization(organizationId, previousPeriod.startDate, previousPeriod.endDate), + this.getStatsByOrganization( + organizationId, + currentPeriod.startDate, + currentPeriod.endDate, + ), + this.getStatsByOrganization( + organizationId, + previousPeriod.startDate, + previousPeriod.endDate, + ), ]); const calculateChange = (current: number, previous: number): number => { @@ -497,17 +539,27 @@ export class UsageRecordsRepository { }; const periodDays = Math.ceil( - (currentPeriod.endDate.getTime() - currentPeriod.startDate.getTime()) / (1000 * 60 * 60 * 24), + (currentPeriod.endDate.getTime() - currentPeriod.startDate.getTime()) / + (1000 * 60 * 60 * 24), ); return { - requestsChange: calculateChange(currentStats.totalRequests, previousStats.totalRequests), - costChange: calculateChange(Number(currentStats.totalCost), Number(previousStats.totalCost)), + requestsChange: calculateChange( + currentStats.totalRequests, + previousStats.totalRequests, + ), + costChange: calculateChange( + Number(currentStats.totalCost), + Number(previousStats.totalCost), + ), tokensChange: calculateChange( currentStats.totalInputTokens + currentStats.totalOutputTokens, previousStats.totalInputTokens + previousStats.totalOutputTokens, ), - successRateChange: calculateChange(currentStats.successRate, previousStats.successRate), + successRateChange: calculateChange( + currentStats.successRate, + previousStats.successRate, + ), period: `${periodDays}d`, }; } @@ -536,7 +588,9 @@ export class UsageRecordsRepository { offset = 0, } = options || {}; - const conditions: SQL[] = [eq(usageRecords.organization_id, organizationId)]; + const conditions: SQL[] = [ + eq(usageRecords.organization_id, organizationId), + ]; if (startDate) { conditions.push(sql`${usageRecords.created_at} >= ${startDate}`); @@ -595,7 +649,10 @@ export class UsageRecordsRepository { * Creates a new usage record. */ async create(data: NewUsageRecord): Promise { - const [record] = await dbWrite.insert(usageRecords).values(data).returning(); + const [record] = await dbWrite + .insert(usageRecords) + .values(data) + .returning(); return record; } @@ -605,7 +662,10 @@ export class UsageRecordsRepository { async update( id: string, data: Partial< - Pick + Pick< + UsageRecord, + "is_successful" | "error_message" | "duration_ms" | "metadata" + > >, ): Promise { const [record] = await dbWrite diff --git a/packages/db/repositories/user-mcps.ts b/packages/db/repositories/user-mcps.ts index a4f67b783..dd07e03b6 100644 --- a/packages/db/repositories/user-mcps.ts +++ b/packages/db/repositories/user-mcps.ts @@ -23,18 +23,29 @@ export const userMcpsRepository = { * Get MCP by ID */ async getById(id: string): Promise { - const [mcp] = await dbRead.select().from(userMcps).where(eq(userMcps.id, id)); + const [mcp] = await dbRead + .select() + .from(userMcps) + .where(eq(userMcps.id, id)); return mcp ?? null; }, /** * Get MCP by slug and organization */ - async getBySlug(slug: string, organizationId: string): Promise { + async getBySlug( + slug: string, + organizationId: string, + ): Promise { const [mcp] = await dbRead .select() .from(userMcps) - .where(and(eq(userMcps.slug, slug), eq(userMcps.organization_id, organizationId))); + .where( + and( + eq(userMcps.slug, slug), + eq(userMcps.organization_id, organizationId), + ), + ); return mcp ?? null; }, @@ -63,7 +74,12 @@ export const userMcpsRepository = { query = dbRead .select() .from(userMcps) - .where(and(eq(userMcps.organization_id, organizationId), eq(userMcps.status, status))) + .where( + and( + eq(userMcps.organization_id, organizationId), + eq(userMcps.status, status), + ), + ) .orderBy(desc(userMcps.created_at)) .limit(limit) .offset(offset); @@ -84,9 +100,18 @@ export const userMcpsRepository = { offset?: number; } = {}, ): Promise { - const { category, status = "live", search, limit = 100, offset = 0 } = options; - - const conditions = [eq(userMcps.is_public, true), eq(userMcps.status, status)]; + const { + category, + status = "live", + search, + limit = 100, + offset = 0, + } = options; + + const conditions = [ + eq(userMcps.is_public, true), + eq(userMcps.status, status), + ]; if (category) { conditions.push(eq(userMcps.category, category)); @@ -94,7 +119,10 @@ export const userMcpsRepository = { if (search) { conditions.push( - or(ilike(userMcps.name, `%${search}%`), ilike(userMcps.description, `%${search}%`))!, + or( + ilike(userMcps.name, `%${search}%`), + ilike(userMcps.description, `%${search}%`), + )!, ); } @@ -111,7 +139,10 @@ export const userMcpsRepository = { * Get MCPs by container ID */ async getByContainerId(containerId: string): Promise { - return dbRead.select().from(userMcps).where(eq(userMcps.container_id, containerId)); + return dbRead + .select() + .from(userMcps) + .where(eq(userMcps.container_id, containerId)); }, /** @@ -183,7 +214,10 @@ export const userMcpsRepository = { /** * Update status */ - async updateStatus(id: string, status: UserMcp["status"]): Promise { + async updateStatus( + id: string, + status: UserMcp["status"], + ): Promise { const updateData: Partial = { status, updated_at: new Date(), diff --git a/packages/db/repositories/user-sessions.ts b/packages/db/repositories/user-sessions.ts index 5fcf842a8..91c34d16c 100644 --- a/packages/db/repositories/user-sessions.ts +++ b/packages/db/repositories/user-sessions.ts @@ -1,6 +1,10 @@ import { and, desc, eq, isNull, sql } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; -import { type NewUserSession, type UserSession, userSessions } from "../schemas/user-sessions"; +import { + type NewUserSession, + type UserSession, + userSessions, +} from "../schemas/user-sessions"; import { jsonbParam } from "../utils/jsonb"; export type { NewUserSession, UserSession }; @@ -28,9 +32,14 @@ export class UserSessionsRepository { /** * Finds an active session by token (not ended). */ - async findActiveByToken(sessionToken: string): Promise { + async findActiveByToken( + sessionToken: string, + ): Promise { return await dbRead.query.userSessions.findFirst({ - where: and(eq(userSessions.session_token, sessionToken), isNull(userSessions.ended_at)), + where: and( + eq(userSessions.session_token, sessionToken), + isNull(userSessions.ended_at), + ), }); } @@ -39,7 +48,10 @@ export class UserSessionsRepository { */ async listActiveByUser(userId: string): Promise { return await dbRead.query.userSessions.findMany({ - where: and(eq(userSessions.user_id, userId), isNull(userSessions.ended_at)), + where: and( + eq(userSessions.user_id, userId), + isNull(userSessions.ended_at), + ), orderBy: desc(userSessions.last_activity_at), }); } @@ -47,7 +59,10 @@ export class UserSessionsRepository { /** * Lists sessions for an organization, ordered by start time. */ - async listByOrganization(organizationId: string, limit?: number): Promise { + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { return await dbRead.query.userSessions.findMany({ where: eq(userSessions.organization_id, organizationId), orderBy: desc(userSessions.started_at), @@ -66,7 +81,10 @@ export class UserSessionsRepository { tokens_consumed: number; } | null> { const activeSessions = await dbRead.query.userSessions.findMany({ - where: and(eq(userSessions.user_id, userId), isNull(userSessions.ended_at)), + where: and( + eq(userSessions.user_id, userId), + isNull(userSessions.ended_at), + ), }); if (activeSessions.length === 0) { @@ -100,7 +118,10 @@ export class UserSessionsRepository { device_info: jsonbParam(data.device_info), } as unknown as NewUserSession; - const [session] = await dbWrite.insert(userSessions).values(values).returning(); + const [session] = await dbWrite + .insert(userSessions) + .values(values) + .returning(); return session; } @@ -200,7 +221,12 @@ export class UserSessionsRepository { const [updated] = await dbWrite .update(userSessions) .set(updateFields) - .where(and(eq(userSessions.session_token, sessionToken), isNull(userSessions.ended_at))) + .where( + and( + eq(userSessions.session_token, sessionToken), + isNull(userSessions.ended_at), + ), + ) .returning(); return updated; @@ -233,7 +259,9 @@ export class UserSessionsRepository { ended_at: new Date(), updated_at: new Date(), }) - .where(and(eq(userSessions.user_id, userId), isNull(userSessions.ended_at))); + .where( + and(eq(userSessions.user_id, userId), isNull(userSessions.ended_at)), + ); return result.rowCount || 0; } diff --git a/packages/db/repositories/users.ts b/packages/db/repositories/users.ts index 6042fc60f..0692c02dc 100644 --- a/packages/db/repositories/users.ts +++ b/packages/db/repositories/users.ts @@ -13,7 +13,10 @@ export interface UserWithOrganization extends User { organization: Organization | null; } -type CompatibleUserWithoutWhatsApp = Omit; +type CompatibleUserWithoutWhatsApp = Omit< + User, + "whatsapp_id" | "whatsapp_name" +>; type CompatibleUserRow = CompatibleUserWithoutWhatsApp & { whatsapp_id?: User["whatsapp_id"]; whatsapp_name?: User["whatsapp_name"]; @@ -76,8 +79,12 @@ type WhatsAppColumnSupport = { * Write operations → dbWrite (primary) */ export class UsersRepository { - private static readWhatsAppColumnSupportPromise: Promise | undefined; - private static writeWhatsAppColumnSupportPromise: Promise | undefined; + private static readWhatsAppColumnSupportPromise: + | Promise + | undefined; + private static writeWhatsAppColumnSupportPromise: + | Promise + | undefined; static resetWhatsAppColumnSupportCacheForTests(): void { UsersRepository.readWhatsAppColumnSupportPromise = undefined; @@ -113,7 +120,11 @@ export class UsersRepository { async findByStewardIdWithOrganization( stewardUserId: string, ): Promise { - return this.findByStewardIdWithOrganizationUsingDb(dbRead, "read", stewardUserId); + return this.findByStewardIdWithOrganizationUsingDb( + dbRead, + "read", + stewardUserId, + ); } /** @@ -125,7 +136,11 @@ export class UsersRepository { async findByPrivyIdWithOrganization( privyUserId: string, ): Promise { - return this.findByPrivyIdWithOrganizationUsingDb(dbRead, "read", privyUserId); + return this.findByPrivyIdWithOrganizationUsingDb( + dbRead, + "read", + privyUserId, + ); } /** @@ -152,13 +167,20 @@ export class UsersRepository { } // Fallback: look up via identity projection (two-query approach for safety) - const identityUserId = await this.findIdentityUserIdByPrivyId(dbWrite, privyUserId); + const identityUserId = await this.findIdentityUserIdByPrivyId( + dbWrite, + privyUserId, + ); if (!identityUserId) { return undefined; } - return await this.findCompatibleUserWithOrganizationById(dbWrite, "write", identityUserId); + return await this.findCompatibleUserWithOrganizationById( + dbWrite, + "write", + identityUserId, + ); } /** @@ -178,19 +200,28 @@ export class UsersRepository { return user; } - const identityUserId = await this.findIdentityUserIdByStewardId(dbWrite, stewardUserId); + const identityUserId = await this.findIdentityUserIdByStewardId( + dbWrite, + stewardUserId, + ); if (!identityUserId) { return undefined; } - return await this.findCompatibleUserWithOrganizationById(dbWrite, "write", identityUserId); + return await this.findCompatibleUserWithOrganizationById( + dbWrite, + "write", + identityUserId, + ); } /** * Finds a user by ID with organization data. */ - async findWithOrganization(userId: string): Promise { + async findWithOrganization( + userId: string, + ): Promise { const user = await dbRead.query.users.findFirst({ where: eq(users.id, userId), with: { @@ -204,7 +235,9 @@ export class UsersRepository { /** * Finds a user by email with organization data. */ - async findByEmailWithOrganization(email: string): Promise { + async findByEmailWithOrganization( + email: string, + ): Promise { const user = await dbRead.query.users.findFirst({ where: eq(users.email, email), with: { @@ -376,7 +409,9 @@ export class UsersRepository { * Finds the identity projection row for a user from primary. * Use after writes when replica lag could return a stale identity row. */ - async findIdentityByUserIdForWrite(userId: string): Promise { + async findIdentityByUserIdForWrite( + userId: string, + ): Promise { return await dbWrite.query.userIdentities.findFirst({ where: eq(userIdentities.user_id, userId), }); @@ -408,12 +443,13 @@ export class UsersRepository { } if (canonicalIdentity.whatsapp_id) { - const conflictingProjection = await dbWrite.query.userIdentities.findFirst({ - where: and( - eq(userIdentities.whatsapp_id, canonicalIdentity.whatsapp_id), - ne(userIdentities.user_id, userId), - ), - }); + const conflictingProjection = + await dbWrite.query.userIdentities.findFirst({ + where: and( + eq(userIdentities.whatsapp_id, canonicalIdentity.whatsapp_id), + ne(userIdentities.user_id, userId), + ), + }); if (conflictingProjection) { return; @@ -436,7 +472,9 @@ export class UsersRepository { * Finds the identity projection row for a Privy user ID from primary. * Use when recovery must verify the projection row ownership directly. */ - async findIdentityByPrivyIdForWrite(privyUserId: string): Promise { + async findIdentityByPrivyIdForWrite( + privyUserId: string, + ): Promise { return await dbWrite.query.userIdentities.findFirst({ where: eq(userIdentities.privy_user_id, privyUserId), }); @@ -446,7 +484,9 @@ export class UsersRepository { * Finds the identity projection row for a Steward user ID from primary. * Use when recovery or auth linking must verify projection row ownership directly. */ - async findIdentityByStewardIdForWrite(stewardUserId: string): Promise { + async findIdentityByStewardIdForWrite( + stewardUserId: string, + ): Promise { return await dbWrite.query.userIdentities.findFirst({ where: eq(userIdentities.steward_user_id, stewardUserId), }); @@ -461,7 +501,10 @@ export class UsersRepository { // Some deployed environments still lag older identity columns on users, // so auth lookups only select the columns they actually need and fill any // missing legacy fields from schema detection. - const identityUserId = await this.findIdentityUserIdByPrivyId(database, privyUserId); + const identityUserId = await this.findIdentityUserIdByPrivyId( + database, + privyUserId, + ); if (identityUserId) { return await this.findCompatibleUserWithOrganizationById( @@ -483,7 +526,10 @@ export class UsersRepository { databaseRole: "read" | "write", stewardUserId: string, ): Promise { - const identityUserId = await this.findIdentityUserIdByStewardId(database, stewardUserId); + const identityUserId = await this.findIdentityUserIdByStewardId( + database, + stewardUserId, + ); if (identityUserId) { return await this.findCompatibleUserWithOrganizationById( @@ -539,9 +585,11 @@ export class UsersRepository { } return { - users: usersColumns.has("whatsapp_id") && usersColumns.has("whatsapp_name"), + users: + usersColumns.has("whatsapp_id") && usersColumns.has("whatsapp_name"), userIdentities: - userIdentityColumns.has("whatsapp_id") && userIdentityColumns.has("whatsapp_name"), + userIdentityColumns.has("whatsapp_id") && + userIdentityColumns.has("whatsapp_name"), }; })(); @@ -590,11 +638,16 @@ export class UsersRepository { return identity?.user_id; } - private normalizeCompatibleUser(user: CompatibleUserRow, hasUsersWhatsAppColumns: boolean): User { + private normalizeCompatibleUser( + user: CompatibleUserRow, + hasUsersWhatsAppColumns: boolean, + ): User { return { ...user, whatsapp_id: hasUsersWhatsAppColumns ? (user.whatsapp_id ?? null) : null, - whatsapp_name: hasUsersWhatsAppColumns ? (user.whatsapp_name ?? null) : null, + whatsapp_name: hasUsersWhatsAppColumns + ? (user.whatsapp_name ?? null) + : null, }; } @@ -717,7 +770,10 @@ export class UsersRepository { /** * Upserts the Privy identity projection for a user. */ - async upsertPrivyIdentity(userId: string, privyUserId: string): Promise { + async upsertPrivyIdentity( + userId: string, + privyUserId: string, + ): Promise { const support = await this.getWhatsAppColumnSupport(dbWrite, "write"); const whatsappInsertColumns = support.userIdentities ? sql`, @@ -791,7 +847,9 @@ export class UsersRepository { const [identity] = result.rows; if (!identity) { - throw new Error(`User ${userId} not found while upserting Privy identity ${privyUserId}`); + throw new Error( + `User ${userId} not found while upserting Privy identity ${privyUserId}`, + ); } return identity; @@ -800,7 +858,10 @@ export class UsersRepository { /** * Upserts the Steward identity projection for a user. */ - async upsertStewardIdentity(userId: string, stewardUserId: string): Promise { + async upsertStewardIdentity( + userId: string, + stewardUserId: string, + ): Promise { const support = await this.getWhatsAppColumnSupport(dbWrite, "write"); const whatsappInsertColumns = support.userIdentities ? sql`, @@ -865,7 +926,9 @@ export class UsersRepository { const [identity] = result.rows; if (!identity) { - throw new Error(`User ${userId} not found while upserting Steward identity ${stewardUserId}`); + throw new Error( + `User ${userId} not found while upserting Steward identity ${stewardUserId}`, + ); } return identity; @@ -877,7 +940,11 @@ export class UsersRepository { */ async listPendingStewardProvisioning( limit: number, - ): Promise>> { + ): Promise< + Array< + Pick + > + > { return await dbWrite .select({ id: users.id, diff --git a/packages/db/repositories/webhook-events.ts b/packages/db/repositories/webhook-events.ts index e3d1591f8..3361bc28e 100644 --- a/packages/db/repositories/webhook-events.ts +++ b/packages/db/repositories/webhook-events.ts @@ -1,6 +1,10 @@ import { and, eq, lt } from "drizzle-orm"; import { dbRead, dbWrite } from "../helpers"; -import { type NewWebhookEvent, type WebhookEvent, webhookEvents } from "../schemas/webhook-events"; +import { + type NewWebhookEvent, + type WebhookEvent, + webhookEvents, +} from "../schemas/webhook-events"; export type { NewWebhookEvent, WebhookEvent }; @@ -94,13 +98,21 @@ export class WebhookEventsRepository { /** * Delete old webhook events for a specific provider. */ - async cleanupOldEventsForProvider(provider: string, retentionDays = 30): Promise { + async cleanupOldEventsForProvider( + provider: string, + retentionDays = 30, + ): Promise { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); const result = await dbWrite .delete(webhookEvents) - .where(and(eq(webhookEvents.provider, provider), lt(webhookEvents.processed_at, cutoffDate))) + .where( + and( + eq(webhookEvents.provider, provider), + lt(webhookEvents.processed_at, cutoffDate), + ), + ) .returning(); return result.length; diff --git a/packages/db/schemas/ad-accounts.ts b/packages/db/schemas/ad-accounts.ts index 6250c8694..3758aa22f 100644 --- a/packages/db/schemas/ad-accounts.ts +++ b/packages/db/schemas/ad-accounts.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; import { secrets } from "./secrets"; import { users } from "./users"; @@ -12,7 +19,11 @@ export type AdPlatform = "meta" | "google" | "tiktok"; /** * Ad account status. */ -export type AdAccountStatus = "active" | "suspended" | "disconnected" | "pending"; +export type AdAccountStatus = + | "active" + | "suspended" + | "disconnected" + | "pending"; /** * Ad accounts table schema. @@ -40,12 +51,18 @@ export const adAccounts = pgTable( account_name: text("account_name").notNull(), // Encrypted tokens stored in secrets table - access_token_secret_id: uuid("access_token_secret_id").references(() => secrets.id, { - onDelete: "set null", - }), - refresh_token_secret_id: uuid("refresh_token_secret_id").references(() => secrets.id, { - onDelete: "set null", - }), + access_token_secret_id: uuid("access_token_secret_id").references( + () => secrets.id, + { + onDelete: "set null", + }, + ), + refresh_token_secret_id: uuid("refresh_token_secret_id").references( + () => secrets.id, + { + onDelete: "set null", + }, + ), // Token expiration tracking token_expires_at: timestamp("token_expires_at"), @@ -76,13 +93,17 @@ export const adAccounts = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("ad_accounts_organization_idx").on(table.organization_id), + organization_idx: index("ad_accounts_organization_idx").on( + table.organization_id, + ), platform_idx: index("ad_accounts_platform_idx").on(table.platform), org_platform_idx: index("ad_accounts_org_platform_idx").on( table.organization_id, table.platform, ), - external_id_idx: index("ad_accounts_external_id_idx").on(table.external_account_id), + external_id_idx: index("ad_accounts_external_id_idx").on( + table.external_account_id, + ), status_idx: index("ad_accounts_status_idx").on(table.status), }), ); diff --git a/packages/db/schemas/ad-campaigns.ts b/packages/db/schemas/ad-campaigns.ts index 9395728c9..61e4a3fd4 100644 --- a/packages/db/schemas/ad-campaigns.ts +++ b/packages/db/schemas/ad-campaigns.ts @@ -73,14 +73,18 @@ export const adCampaigns = pgTable( status: text("status").$type().notNull().default("draft"), budget_type: text("budget_type").$type().notNull(), - budget_amount: numeric("budget_amount", { precision: 12, scale: 2 }).notNull().default("0.00"), + budget_amount: numeric("budget_amount", { precision: 12, scale: 2 }) + .notNull() + .default("0.00"), budget_currency: text("budget_currency").notNull().default("USD"), // Credits allocated for this campaign (in our system) credits_allocated: numeric("credits_allocated", { precision: 12, scale: 2 }) .notNull() .default("0.00"), - credits_spent: numeric("credits_spent", { precision: 12, scale: 2 }).notNull().default("0.00"), + credits_spent: numeric("credits_spent", { precision: 12, scale: 2 }) + .notNull() + .default("0.00"), // Schedule start_date: timestamp("start_date"), @@ -104,7 +108,9 @@ export const adCampaigns = pgTable( .default({}), // Performance metrics (synced from platform) - total_spend: numeric("total_spend", { precision: 12, scale: 2 }).notNull().default("0.00"), + total_spend: numeric("total_spend", { precision: 12, scale: 2 }) + .notNull() + .default("0.00"), total_impressions: integer("total_impressions").notNull().default(0), total_clicks: integer("total_clicks").notNull().default(0), total_conversions: integer("total_conversions").notNull().default(0), @@ -128,14 +134,23 @@ export const adCampaigns = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("ad_campaigns_organization_idx").on(table.organization_id), - ad_account_idx: index("ad_campaigns_ad_account_idx").on(table.ad_account_id), + organization_idx: index("ad_campaigns_organization_idx").on( + table.organization_id, + ), + ad_account_idx: index("ad_campaigns_ad_account_idx").on( + table.ad_account_id, + ), platform_idx: index("ad_campaigns_platform_idx").on(table.platform), status_idx: index("ad_campaigns_status_idx").on(table.status), - external_id_idx: index("ad_campaigns_external_id_idx").on(table.external_campaign_id), + external_id_idx: index("ad_campaigns_external_id_idx").on( + table.external_campaign_id, + ), app_idx: index("ad_campaigns_app_idx").on(table.app_id), created_at_idx: index("ad_campaigns_created_at_idx").on(table.created_at), - org_status_idx: index("ad_campaigns_org_status_idx").on(table.organization_id, table.status), + org_status_idx: index("ad_campaigns_org_status_idx").on( + table.organization_id, + table.status, + ), }), ); diff --git a/packages/db/schemas/ad-creatives.ts b/packages/db/schemas/ad-creatives.ts index dc31195c8..0127352fa 100644 --- a/packages/db/schemas/ad-creatives.ts +++ b/packages/db/schemas/ad-creatives.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { adCampaigns } from "./ad-campaigns"; /** @@ -106,7 +113,9 @@ export const adCreatives = pgTable( campaign_idx: index("ad_creatives_campaign_idx").on(table.campaign_id), type_idx: index("ad_creatives_type_idx").on(table.type), status_idx: index("ad_creatives_status_idx").on(table.status), - external_id_idx: index("ad_creatives_external_id_idx").on(table.external_creative_id), + external_id_idx: index("ad_creatives_external_id_idx").on( + table.external_creative_id, + ), created_at_idx: index("ad_creatives_created_at_idx").on(table.created_at), }), ); diff --git a/packages/db/schemas/ad-transactions.ts b/packages/db/schemas/ad-transactions.ts index d3db0415d..474dafea8 100644 --- a/packages/db/schemas/ad-transactions.ts +++ b/packages/db/schemas/ad-transactions.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, numeric, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + numeric, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { adCampaigns } from "./ad-campaigns"; import { creditTransactions } from "./credit-transactions"; import { organizations } from "./organizations"; @@ -7,7 +15,11 @@ import { organizations } from "./organizations"; /** * Ad transaction type. */ -export type AdTransactionType = "budget_allocation" | "spend" | "refund" | "adjustment"; +export type AdTransactionType = + | "budget_allocation" + | "spend" + | "refund" + | "adjustment"; /** * Ad transactions table schema. @@ -29,9 +41,12 @@ export const adTransactions = pgTable( }), // Link to credit transaction for unified tracking - credit_transaction_id: uuid("credit_transaction_id").references(() => creditTransactions.id, { - onDelete: "set null", - }), + credit_transaction_id: uuid("credit_transaction_id").references( + () => creditTransactions.id, + { + onDelete: "set null", + }, + ), type: text("type").$type().notNull(), @@ -62,12 +77,21 @@ export const adTransactions = pgTable( created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("ad_transactions_organization_idx").on(table.organization_id), + organization_idx: index("ad_transactions_organization_idx").on( + table.organization_id, + ), campaign_idx: index("ad_transactions_campaign_idx").on(table.campaign_id), - credit_tx_idx: index("ad_transactions_credit_tx_idx").on(table.credit_transaction_id), + credit_tx_idx: index("ad_transactions_credit_tx_idx").on( + table.credit_transaction_id, + ), type_idx: index("ad_transactions_type_idx").on(table.type), - created_at_idx: index("ad_transactions_created_at_idx").on(table.created_at), - org_type_idx: index("ad_transactions_org_type_idx").on(table.organization_id, table.type), + created_at_idx: index("ad_transactions_created_at_idx").on( + table.created_at, + ), + org_type_idx: index("ad_transactions_org_type_idx").on( + table.organization_id, + table.type, + ), }), ); diff --git a/packages/db/schemas/admin-users.ts b/packages/db/schemas/admin-users.ts index 42620b1b4..5ad6bc52e 100644 --- a/packages/db/schemas/admin-users.ts +++ b/packages/db/schemas/admin-users.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + pgEnum, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { users } from "./users"; /** @@ -47,7 +55,9 @@ export const adminUsers = pgTable( revokedAt: timestamp("revoked_at"), }, (table) => ({ - walletAddressIdx: index("admin_users_wallet_address_idx").on(table.walletAddress), + walletAddressIdx: index("admin_users_wallet_address_idx").on( + table.walletAddress, + ), userIdIdx: index("admin_users_user_id_idx").on(table.userId), roleIdx: index("admin_users_role_idx").on(table.role), isActiveIdx: index("admin_users_is_active_idx").on(table.isActive), diff --git a/packages/db/schemas/advanced-memory.ts b/packages/db/schemas/advanced-memory.ts index dcaa555fc..5efe0ae99 100644 --- a/packages/db/schemas/advanced-memory.ts +++ b/packages/db/schemas/advanced-memory.ts @@ -28,10 +28,17 @@ export const longTermMemories = pgTable( accessCount: integer("access_count").default(0), }, (table) => ({ - agentEntityIdx: index("long_term_memories_agent_entity_idx").on(table.agentId, table.entityId), + agentEntityIdx: index("long_term_memories_agent_entity_idx").on( + table.agentId, + table.entityId, + ), categoryIdx: index("long_term_memories_category_idx").on(table.category), - confidenceIdx: index("long_term_memories_confidence_idx").on(table.confidence), - createdAtIdx: index("long_term_memories_created_at_idx").on(table.createdAt), + confidenceIdx: index("long_term_memories_confidence_idx").on( + table.confidence, + ), + createdAtIdx: index("long_term_memories_created_at_idx").on( + table.createdAt, + ), }), ); @@ -54,7 +61,10 @@ export const sessionSummaries = pgTable( updatedAt: timestamp("updated_at").default(sql`now()`).notNull(), }, (table) => ({ - agentRoomIdx: index("session_summaries_agent_room_idx").on(table.agentId, table.roomId), + agentRoomIdx: index("session_summaries_agent_room_idx").on( + table.agentId, + table.roomId, + ), entityIdx: index("session_summaries_entity_idx").on(table.entityId), startTimeIdx: index("session_summaries_start_time_idx").on(table.startTime), }), @@ -75,6 +85,8 @@ export const memoryAccessLogs = pgTable( (table) => ({ memoryIdx: index("memory_access_logs_memory_idx").on(table.memoryId), agentIdx: index("memory_access_logs_agent_idx").on(table.agentId), - accessedAtIdx: index("memory_access_logs_accessed_at_idx").on(table.accessedAt), + accessedAtIdx: index("memory_access_logs_accessed_at_idx").on( + table.accessedAt, + ), }), ); diff --git a/packages/db/schemas/affiliates.ts b/packages/db/schemas/affiliates.ts index 30aa91263..61f38a91e 100644 --- a/packages/db/schemas/affiliates.ts +++ b/packages/db/schemas/affiliates.ts @@ -68,7 +68,9 @@ export const userAffiliates = pgTable( }, (table) => ({ user_unique: uniqueIndex("user_affiliates_user_idx").on(table.user_id), - affiliate_idx: index("user_affiliates_affiliate_idx").on(table.affiliate_code_id), + affiliate_idx: index("user_affiliates_affiliate_idx").on( + table.affiliate_code_id, + ), }), ); diff --git a/packages/db/schemas/agent-budgets.ts b/packages/db/schemas/agent-budgets.ts index ed0f90b49..c5c845404 100644 --- a/packages/db/schemas/agent-budgets.ts +++ b/packages/db/schemas/agent-budgets.ts @@ -59,15 +59,21 @@ export const agentBudgets = pgTable( allocated_budget: numeric("allocated_budget", { precision: 12, scale: 4 }) .notNull() .default("0.0000"), - spent_budget: numeric("spent_budget", { precision: 12, scale: 4 }).notNull().default("0.0000"), + spent_budget: numeric("spent_budget", { precision: 12, scale: 4 }) + .notNull() + .default("0.0000"), // Daily limits (in USD) daily_limit: numeric("daily_limit", { precision: 10, scale: 4 }), - daily_spent: numeric("daily_spent", { precision: 10, scale: 4 }).notNull().default("0.0000"), + daily_spent: numeric("daily_spent", { precision: 10, scale: 4 }) + .notNull() + .default("0.0000"), daily_reset_at: timestamp("daily_reset_at"), // Auto-refill settings - auto_refill_enabled: boolean("auto_refill_enabled").notNull().default(false), + auto_refill_enabled: boolean("auto_refill_enabled") + .notNull() + .default(false), auto_refill_amount: numeric("auto_refill_amount", { precision: 10, scale: 4, @@ -89,10 +95,15 @@ export const agentBudgets = pgTable( precision: 10, scale: 4, }).default("5.0000"), - low_budget_alert_sent: boolean("low_budget_alert_sent").notNull().default(false), + low_budget_alert_sent: boolean("low_budget_alert_sent") + .notNull() + .default(false), // Metadata - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), @@ -153,7 +164,10 @@ export const agentBudgetTransactions = pgTable( source_type: text("source_type"), // "org_transfer", "auto_refill", "usage", etc. source_id: text("source_id"), // Reference to source transaction/operation - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), }, @@ -161,7 +175,9 @@ export const agentBudgetTransactions = pgTable( budget_idx: index("agent_budget_txns_budget_idx").on(table.budget_id), agent_idx: index("agent_budget_txns_agent_idx").on(table.agent_id), type_idx: index("agent_budget_txns_type_idx").on(table.type), - created_at_idx: index("agent_budget_txns_created_at_idx").on(table.created_at), + created_at_idx: index("agent_budget_txns_created_at_idx").on( + table.created_at, + ), }), ); @@ -171,5 +187,9 @@ export const agentBudgetTransactions = pgTable( export type AgentBudget = InferSelectModel; export type NewAgentBudget = InferInsertModel; -export type AgentBudgetTransaction = InferSelectModel; -export type NewAgentBudgetTransaction = InferInsertModel; +export type AgentBudgetTransaction = InferSelectModel< + typeof agentBudgetTransactions +>; +export type NewAgentBudgetTransaction = InferInsertModel< + typeof agentBudgetTransactions +>; diff --git a/packages/db/schemas/agent-events.ts b/packages/db/schemas/agent-events.ts index 6eb8698fd..4fd59e4fb 100644 --- a/packages/db/schemas/agent-events.ts +++ b/packages/db/schemas/agent-events.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; import { userCharacters } from "./user-characters"; @@ -31,18 +38,26 @@ export const agentEvents = pgTable( event_type: text("event_type").$type().notNull(), level: text("level").$type().notNull().default("info"), message: text("message").notNull(), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), duration_ms: text("duration_ms"), container_id: uuid("container_id"), created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ agent_idx: index("agent_events_agent_idx").on(table.agent_id), - organization_idx: index("agent_events_organization_idx").on(table.organization_id), + organization_idx: index("agent_events_organization_idx").on( + table.organization_id, + ), event_type_idx: index("agent_events_event_type_idx").on(table.event_type), level_idx: index("agent_events_level_idx").on(table.level), created_at_idx: index("agent_events_created_at_idx").on(table.created_at), - agent_created_idx: index("agent_events_agent_created_idx").on(table.agent_id, table.created_at), + agent_created_idx: index("agent_events_agent_created_idx").on( + table.agent_id, + table.created_at, + ), }), ); diff --git a/packages/db/schemas/agent-phone-numbers.ts b/packages/db/schemas/agent-phone-numbers.ts index 2bcd4f196..0ecec8604 100644 --- a/packages/db/schemas/agent-phone-numbers.ts +++ b/packages/db/schemas/agent-phone-numbers.ts @@ -25,7 +25,13 @@ export const phoneProviderEnum = pgEnum("phone_provider", [ /** * Phone number type enum */ -export const phoneTypeEnum = pgEnum("phone_type", ["sms", "voice", "both", "imessage", "whatsapp"]); +export const phoneTypeEnum = pgEnum("phone_type", [ + "sms", + "voice", + "both", + "imessage", + "whatsapp", +]); /** * Agent phone numbers table schema. @@ -91,10 +97,14 @@ export const agentPhoneNumbers = pgTable( table.phone_number, table.organization_id, ), - organization_idx: index("agent_phone_numbers_organization_idx").on(table.organization_id), + organization_idx: index("agent_phone_numbers_organization_idx").on( + table.organization_id, + ), agent_idx: index("agent_phone_numbers_agent_idx").on(table.agent_id), provider_idx: index("agent_phone_numbers_provider_idx").on(table.provider), - is_active_idx: index("agent_phone_numbers_is_active_idx").on(table.is_active), + is_active_idx: index("agent_phone_numbers_is_active_idx").on( + table.is_active, + ), }), ); @@ -140,11 +150,17 @@ export const phoneMessageLog = pgTable( responded_at: timestamp("responded_at"), }, (table) => ({ - phone_number_idx: index("phone_message_log_phone_number_idx").on(table.phone_number_id), + phone_number_idx: index("phone_message_log_phone_number_idx").on( + table.phone_number_id, + ), direction_idx: index("phone_message_log_direction_idx").on(table.direction), status_idx: index("phone_message_log_status_idx").on(table.status), - created_at_idx: index("phone_message_log_created_at_idx").on(table.created_at), - from_number_idx: index("phone_message_log_from_number_idx").on(table.from_number), + created_at_idx: index("phone_message_log_created_at_idx").on( + table.created_at, + ), + from_number_idx: index("phone_message_log_from_number_idx").on( + table.from_number, + ), // Composite index for conversation grouping queries // Used by GET /api/v1/messages which groups by (from_number, to_number, phone_number_id) conversation_composite_idx: index("phone_message_log_conversation_idx").on( diff --git a/packages/db/schemas/agent-server-wallets.ts b/packages/db/schemas/agent-server-wallets.ts index 875ae7966..940065020 100644 --- a/packages/db/schemas/agent-server-wallets.ts +++ b/packages/db/schemas/agent-server-wallets.ts @@ -51,13 +51,23 @@ export const agentServerWallets = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("agent_server_wallets_organization_idx").on(table.organization_id), + organization_idx: index("agent_server_wallets_organization_idx").on( + table.organization_id, + ), user_idx: index("agent_server_wallets_user_idx").on(table.user_id), - character_idx: index("agent_server_wallets_character_idx").on(table.character_id), - privy_wallet_idx: index("agent_server_wallets_privy_wallet_idx").on(table.privy_wallet_id), + character_idx: index("agent_server_wallets_character_idx").on( + table.character_id, + ), + privy_wallet_idx: index("agent_server_wallets_privy_wallet_idx").on( + table.privy_wallet_id, + ), address_idx: index("agent_server_wallets_address_idx").on(table.address), - client_address_idx: index("agent_server_wallets_client_address_idx").on(table.client_address), - steward_agent_idx: index("agent_server_wallets_steward_agent_idx").on(table.steward_agent_id), + client_address_idx: index("agent_server_wallets_client_address_idx").on( + table.client_address, + ), + steward_agent_idx: index("agent_server_wallets_steward_agent_idx").on( + table.steward_agent_id, + ), wallet_provider_idx: index("agent_server_wallets_wallet_provider_idx").on( table.wallet_provider, ), diff --git a/packages/db/schemas/ai-pricing.ts b/packages/db/schemas/ai-pricing.ts index af0b63274..a3a16bb7b 100644 --- a/packages/db/schemas/ai-pricing.ts +++ b/packages/db/schemas/ai-pricing.ts @@ -34,7 +34,10 @@ export const aiPricingEntries = pgTable( unit_price: numeric("unit_price", { precision: 20, scale: 10 }).notNull(), currency: text("currency").notNull().default("USD"), dimension_key: text("dimension_key").notNull().default("*"), - dimensions: jsonb("dimensions").$type().notNull().default({}), + dimensions: jsonb("dimensions") + .$type() + .notNull() + .default({}), source_kind: text("source_kind").notNull(), source_url: text("source_url").notNull(), source_hash: text("source_hash"), @@ -46,7 +49,10 @@ export const aiPricingEntries = pgTable( is_active: boolean("is_active").notNull().default(true), is_override: boolean("is_override").notNull().default(false), updated_by: text("updated_by"), - metadata: jsonb("metadata").$type>().notNull().default({}), + metadata: jsonb("metadata") + .$type>() + .notNull() + .default({}), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, @@ -85,7 +91,10 @@ export const aiPricingRefreshRuns = pgTable( upserted_entries: integer("upserted_entries").notNull().default(0), deactivated_entries: integer("deactivated_entries").notNull().default(0), error: text("error"), - metadata: jsonb("metadata").$type>().notNull().default({}), + metadata: jsonb("metadata") + .$type>() + .notNull() + .default({}), started_at: timestamp("started_at").notNull().defaultNow(), completed_at: timestamp("completed_at"), created_at: timestamp("created_at").notNull().defaultNow(), @@ -95,11 +104,15 @@ export const aiPricingRefreshRuns = pgTable( table.source, table.status, ), - started_idx: index("ai_pricing_refresh_runs_started_idx").on(table.started_at), + started_idx: index("ai_pricing_refresh_runs_started_idx").on( + table.started_at, + ), }), ); export type AiPricingEntry = InferSelectModel; export type NewAiPricingEntry = InferInsertModel; export type AiPricingRefreshRun = InferSelectModel; -export type NewAiPricingRefreshRun = InferInsertModel; +export type NewAiPricingRefreshRun = InferInsertModel< + typeof aiPricingRefreshRuns +>; diff --git a/packages/db/schemas/alb-priorities.ts b/packages/db/schemas/alb-priorities.ts index c08dd773b..b84e7963e 100644 --- a/packages/db/schemas/alb-priorities.ts +++ b/packages/db/schemas/alb-priorities.ts @@ -8,7 +8,14 @@ * The unique key is (userId, projectName). */ -import { integer, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { + integer, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; export const albPriorities = pgTable( "alb_priorities", @@ -17,10 +24,17 @@ export const albPriorities = pgTable( userId: text("user_id").notNull(), projectName: text("project_name").notNull().default("default"), priority: integer("priority").notNull().unique(), - createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), expiresAt: timestamp("expires_at", { withTimezone: true }), }, - (table) => [uniqueIndex("alb_priorities_user_project_idx").on(table.userId, table.projectName)], + (table) => [ + uniqueIndex("alb_priorities_user_project_idx").on( + table.userId, + table.projectName, + ), + ], ); export type AlbPriority = typeof albPriorities.$inferSelect; diff --git a/packages/db/schemas/anonymous-sessions.ts b/packages/db/schemas/anonymous-sessions.ts index aa17029ef..34e0db07c 100644 --- a/packages/db/schemas/anonymous-sessions.ts +++ b/packages/db/schemas/anonymous-sessions.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + integer, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { users } from "./users"; /** diff --git a/packages/db/schemas/api-keys.ts b/packages/db/schemas/api-keys.ts index ca0df4ee2..af8403f5c 100644 --- a/packages/db/schemas/api-keys.ts +++ b/packages/db/schemas/api-keys.ts @@ -47,7 +47,9 @@ export const apiKeys = pgTable( key_idx: index("api_keys_key_idx").on(table.key), key_hash_idx: uniqueIndex("api_keys_key_hash_idx").on(table.key_hash), key_prefix_idx: index("api_keys_key_prefix_idx").on(table.key_prefix), - organization_idx: index("api_keys_organization_idx").on(table.organization_id), + organization_idx: index("api_keys_organization_idx").on( + table.organization_id, + ), user_idx: index("api_keys_user_idx").on(table.user_id), }), ); diff --git a/packages/db/schemas/app-billing.ts b/packages/db/schemas/app-billing.ts index 8860dd440..7181178bd 100644 --- a/packages/db/schemas/app-billing.ts +++ b/packages/db/schemas/app-billing.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, integer, numeric, pgTable, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + integer, + numeric, + pgTable, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { apps } from "./apps"; /** @@ -18,10 +26,14 @@ export const appBilling = pgTable( .references(() => apps.id, { onDelete: "cascade" }), // Pricing overrides - custom_pricing_enabled: boolean("custom_pricing_enabled").default(false).notNull(), + custom_pricing_enabled: boolean("custom_pricing_enabled") + .default(false) + .notNull(), // Monetization settings - monetization_enabled: boolean("monetization_enabled").default(false).notNull(), + monetization_enabled: boolean("monetization_enabled") + .default(false) + .notNull(), inference_markup_percentage: numeric("inference_markup_percentage", { precision: 7, scale: 2, diff --git a/packages/db/schemas/app-config.ts b/packages/db/schemas/app-config.ts index d9d72550c..9e58d4fbc 100644 --- a/packages/db/schemas/app-config.ts +++ b/packages/db/schemas/app-config.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { apps } from "./apps"; /** @@ -120,7 +127,10 @@ export const appConfig = pgTable( .default([]), // Linked characters (max 4 AI agents) - linked_character_ids: jsonb("linked_character_ids").$type().default([]).notNull(), + linked_character_ids: jsonb("linked_character_ids") + .$type() + .default([]) + .notNull(), // GitHub repository github_repo: text("github_repo"), diff --git a/packages/db/schemas/app-credit-balances.ts b/packages/db/schemas/app-credit-balances.ts index c0771357f..c3fa467c5 100644 --- a/packages/db/schemas/app-credit-balances.ts +++ b/packages/db/schemas/app-credit-balances.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, numeric, pgTable, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { + index, + numeric, + pgTable, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; import { apps } from "./apps"; import { organizations } from "./organizations"; import { users } from "./users"; @@ -28,7 +35,9 @@ export const appCreditBalances = pgTable( total_purchased: numeric("total_purchased", { precision: 10, scale: 2 }) .notNull() .default("0.00"), - total_spent: numeric("total_spent", { precision: 10, scale: 2 }).notNull().default("0.00"), + total_spent: numeric("total_spent", { precision: 10, scale: 2 }) + .notNull() + .default("0.00"), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, diff --git a/packages/db/schemas/app-databases.ts b/packages/db/schemas/app-databases.ts index 389918e07..12a4ab8bb 100644 --- a/packages/db/schemas/app-databases.ts +++ b/packages/db/schemas/app-databases.ts @@ -33,7 +33,9 @@ export const appDatabases = pgTable( user_database_region: text("user_database_region").default("aws-us-east-1"), /** Current provisioning status. State machine: none → provisioning → ready | error. */ - user_database_status: userDatabaseStatusEnum("user_database_status").notNull().default("none"), + user_database_status: userDatabaseStatusEnum("user_database_status") + .notNull() + .default("none"), /** Error message if provisioning failed. */ user_database_error: text("user_database_error"), @@ -44,7 +46,9 @@ export const appDatabases = pgTable( }, (table) => ({ app_idx: index("app_databases_app_idx").on(table.app_id), - status_idx: index("app_databases_status_idx").on(table.user_database_status), + status_idx: index("app_databases_status_idx").on( + table.user_database_status, + ), }), ); diff --git a/packages/db/schemas/app-domains.ts b/packages/db/schemas/app-domains.ts index c5839b1d0..861ca4908 100644 --- a/packages/db/schemas/app-domains.ts +++ b/packages/db/schemas/app-domains.ts @@ -37,7 +37,9 @@ export const appDomains = pgTable( // Custom domain (optional) custom_domain: text("custom_domain"), - custom_domain_verified: boolean("custom_domain_verified").default(false).notNull(), + custom_domain_verified: boolean("custom_domain_verified") + .default(false) + .notNull(), verification_records: jsonb("verification_records") .$type() .default([]), @@ -64,8 +66,12 @@ export const appDomains = pgTable( (table) => ({ app_id_idx: index("app_domains_app_id_idx").on(table.app_id), subdomain_idx: uniqueIndex("app_domains_subdomain_idx").on(table.subdomain), - custom_domain_idx: uniqueIndex("app_domains_custom_domain_idx").on(table.custom_domain), - vercel_domain_idx: index("app_domains_vercel_domain_idx").on(table.vercel_domain_id), + custom_domain_idx: uniqueIndex("app_domains_custom_domain_idx").on( + table.custom_domain, + ), + vercel_domain_idx: index("app_domains_vercel_domain_idx").on( + table.vercel_domain_id, + ), }), ); diff --git a/packages/db/schemas/app-earnings.ts b/packages/db/schemas/app-earnings.ts index 7e942c6cd..b6f80feb7 100644 --- a/packages/db/schemas/app-earnings.ts +++ b/packages/db/schemas/app-earnings.ts @@ -84,7 +84,10 @@ export const appEarningsTransactions = pgTable( type: text("type").notNull(), amount: numeric("amount", { precision: 10, scale: 6 }).notNull(), description: text("description"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ @@ -100,5 +103,9 @@ export const appEarningsTransactions = pgTable( export type AppEarnings = InferSelectModel; export type NewAppEarnings = InferInsertModel; -export type AppEarningsTransaction = InferSelectModel; -export type NewAppEarningsTransaction = InferInsertModel; +export type AppEarningsTransaction = InferSelectModel< + typeof appEarningsTransactions +>; +export type NewAppEarningsTransaction = InferInsertModel< + typeof appEarningsTransactions +>; diff --git a/packages/db/schemas/app-sandboxes.ts b/packages/db/schemas/app-sandboxes.ts index ae3f4c1f9..36e48bdab 100644 --- a/packages/db/schemas/app-sandboxes.ts +++ b/packages/db/schemas/app-sandboxes.ts @@ -54,7 +54,14 @@ export const appSandboxSessions = pgTable( // Session status status: text("status") - .$type<"initializing" | "ready" | "generating" | "error" | "stopped" | "timeout">() + .$type< + | "initializing" + | "ready" + | "generating" + | "error" + | "stopped" + | "timeout" + >() .notNull() .default("initializing"), status_message: text("status_message"), @@ -128,11 +135,17 @@ export const appSandboxSessions = pgTable( }, (table) => ({ user_id_idx: index("app_sandbox_sessions_user_id_idx").on(table.user_id), - organization_id_idx: index("app_sandbox_sessions_org_id_idx").on(table.organization_id), + organization_id_idx: index("app_sandbox_sessions_org_id_idx").on( + table.organization_id, + ), app_id_idx: index("app_sandbox_sessions_app_id_idx").on(table.app_id), - sandbox_id_idx: index("app_sandbox_sessions_sandbox_id_idx").on(table.sandbox_id), + sandbox_id_idx: index("app_sandbox_sessions_sandbox_id_idx").on( + table.sandbox_id, + ), status_idx: index("app_sandbox_sessions_status_idx").on(table.status), - created_at_idx: index("app_sandbox_sessions_created_at_idx").on(table.created_at), + created_at_idx: index("app_sandbox_sessions_created_at_idx").on( + table.created_at, + ), }), ); @@ -173,8 +186,12 @@ export const appBuilderPrompts = pgTable( duration_ms: integer("duration_ms"), }, (table) => ({ - session_idx: index("app_builder_prompts_session_idx").on(table.sandbox_session_id), - created_at_idx: index("app_builder_prompts_created_at_idx").on(table.created_at), + session_idx: index("app_builder_prompts_session_idx").on( + table.sandbox_session_id, + ), + created_at_idx: index("app_builder_prompts_created_at_idx").on( + table.created_at, + ), }), ); @@ -193,7 +210,9 @@ export const appTemplates = pgTable( slug: text("slug").notNull().unique(), description: text("description"), category: text("category") - .$type<"chat" | "agent" | "dashboard" | "landing" | "analytics" | "utility">() + .$type< + "chat" | "agent" | "dashboard" | "landing" | "analytics" | "utility" + >() .notNull(), // Template content - now points to GitHub repo @@ -206,7 +225,10 @@ export const appTemplates = pgTable( // AI prompts for this template system_prompt: text("system_prompt"), - example_prompts: jsonb("example_prompts").$type().default([]).notNull(), + example_prompts: jsonb("example_prompts") + .$type() + .default([]) + .notNull(), // Usage tracking usage_count: integer("usage_count").default(0).notNull(), @@ -223,7 +245,9 @@ export const appTemplates = pgTable( slug_idx: index("app_templates_slug_idx").on(table.slug), category_idx: index("app_templates_category_idx").on(table.category), is_active_idx: index("app_templates_is_active_idx").on(table.is_active), - is_featured_idx: index("app_templates_is_featured_idx").on(table.is_featured), + is_featured_idx: index("app_templates_is_featured_idx").on( + table.is_featured, + ), }), ); @@ -259,12 +283,16 @@ export const sessionFileSnapshots = pgTable( created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ - session_idx: index("session_file_snapshots_session_idx").on(table.sandbox_session_id), + session_idx: index("session_file_snapshots_session_idx").on( + table.sandbox_session_id, + ), session_path_idx: index("session_file_snapshots_session_path_idx").on( table.sandbox_session_id, table.file_path, ), - created_at_idx: index("session_file_snapshots_created_at_idx").on(table.created_at), + created_at_idx: index("session_file_snapshots_created_at_idx").on( + table.created_at, + ), }), ); @@ -301,7 +329,9 @@ export const sessionRestoreHistory = pgTable( completed_at: timestamp("completed_at"), }, (table) => ({ - session_idx: index("session_restore_history_session_idx").on(table.sandbox_session_id), + session_idx: index("session_restore_history_session_idx").on( + table.sandbox_session_id, + ), }), ); @@ -313,9 +343,15 @@ export type NewAppBuilderPrompt = InferInsertModel; export type AppTemplate = InferSelectModel; export type NewAppTemplate = InferInsertModel; export type SessionFileSnapshot = InferSelectModel; -export type NewSessionFileSnapshot = InferInsertModel; -export type SessionRestoreHistory = InferSelectModel; -export type NewSessionRestoreHistory = InferInsertModel; +export type NewSessionFileSnapshot = InferInsertModel< + typeof sessionFileSnapshots +>; +export type SessionRestoreHistory = InferSelectModel< + typeof sessionRestoreHistory +>; +export type NewSessionRestoreHistory = InferInsertModel< + typeof sessionRestoreHistory +>; /** * Sandbox template snapshots table schema. * @@ -361,12 +397,22 @@ export const sandboxTemplateSnapshots = pgTable( usage_count: integer("usage_count").default(0).notNull(), }, (table) => ({ - template_key_idx: index("sandbox_snapshots_template_key_idx").on(table.template_key), + template_key_idx: index("sandbox_snapshots_template_key_idx").on( + table.template_key, + ), status_idx: index("sandbox_snapshots_status_idx").on(table.status), - expires_at_idx: index("sandbox_snapshots_expires_at_idx").on(table.expires_at), - snapshot_id_idx: index("sandbox_snapshots_snapshot_id_idx").on(table.snapshot_id), + expires_at_idx: index("sandbox_snapshots_expires_at_idx").on( + table.expires_at, + ), + snapshot_id_idx: index("sandbox_snapshots_snapshot_id_idx").on( + table.snapshot_id, + ), }), ); -export type SandboxTemplateSnapshot = InferSelectModel; -export type NewSandboxTemplateSnapshot = InferInsertModel; +export type SandboxTemplateSnapshot = InferSelectModel< + typeof sandboxTemplateSnapshots +>; +export type NewSandboxTemplateSnapshot = InferInsertModel< + typeof sandboxTemplateSnapshots +>; diff --git a/packages/db/schemas/apps.ts b/packages/db/schemas/apps.ts index 8fe0c2a84..97c510567 100644 --- a/packages/db/schemas/apps.ts +++ b/packages/db/schemas/apps.ts @@ -27,7 +27,12 @@ export const appDeploymentStatusEnum = pgEnum("app_deployment_status", [ "failed", ]); -export type AppDeploymentStatus = "draft" | "building" | "deploying" | "deployed" | "failed"; +export type AppDeploymentStatus = + | "draft" + | "building" + | "deploying" + | "deployed" + | "failed"; /** * User database provisioning status enum. @@ -75,7 +80,10 @@ export const apps = pgTable( // App URL app_url: text("app_url").notNull(), - allowed_origins: jsonb("allowed_origins").$type().notNull().default([]), + allowed_origins: jsonb("allowed_origins") + .$type() + .notNull() + .default([]), // API Key api_key_id: uuid("api_key_id").unique(), @@ -99,10 +107,15 @@ export const apps = pgTable( logo_url: text("logo_url"), website_url: text("website_url"), contact_email: text("contact_email"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), // Deployment status - deployment_status: appDeploymentStatusEnum("deployment_status").notNull().default("draft"), + deployment_status: appDeploymentStatusEnum("deployment_status") + .notNull() + .default("draft"), production_url: text("production_url"), last_deployed_at: timestamp("last_deployed_at"), @@ -110,20 +123,26 @@ export const apps = pgTable( github_repo: text("github_repo"), // Character linking (kept for characters API) - linked_character_ids: jsonb("linked_character_ids").$type().default([]), + linked_character_ids: jsonb("linked_character_ids") + .$type() + .default([]), // Monetization (kept for chat system) - monetization_enabled: boolean("monetization_enabled").default(false).notNull(), + monetization_enabled: boolean("monetization_enabled") + .default(false) + .notNull(), inference_markup_percentage: real("inference_markup_percentage").default(0), purchase_share_percentage: real("purchase_share_percentage").default(0), platform_offset_amount: real("platform_offset_amount").default(0), custom_pricing_enabled: boolean("custom_pricing_enabled").default(false), - total_creator_earnings: numeric("total_creator_earnings", { precision: 12, scale: 6 }).default( - "0.000000", - ), - total_platform_revenue: numeric("total_platform_revenue", { precision: 12, scale: 6 }).default( - "0.000000", - ), + total_creator_earnings: numeric("total_creator_earnings", { + precision: 12, + scale: 6, + }).default("0.000000"), + total_platform_revenue: numeric("total_platform_revenue", { + precision: 12, + scale: 6, + }).default("0.000000"), // Social automation configs (kept for automation services) discord_automation: jsonb("discord_automation") @@ -204,7 +223,9 @@ export const apps = pgTable( .default([]), // User database provisioning (kept for database service) - user_database_status: userDatabaseStatusEnum("user_database_status").notNull().default("none"), + user_database_status: userDatabaseStatusEnum("user_database_status") + .notNull() + .default("none"), user_database_uri: text("user_database_uri"), user_database_project_id: text("user_database_project_id"), user_database_branch_id: text("user_database_branch_id"), @@ -228,7 +249,9 @@ export const apps = pgTable( slug_idx: index("apps_slug_idx").on(table.slug), organization_idx: index("apps_organization_idx").on(table.organization_id), created_by_idx: index("apps_created_by_idx").on(table.created_by_user_id), - affiliate_code_idx: index("apps_affiliate_code_idx").on(table.affiliate_code), + affiliate_code_idx: index("apps_affiliate_code_idx").on( + table.affiliate_code, + ), is_active_idx: index("apps_is_active_idx").on(table.is_active), created_at_idx: index("apps_created_at_idx").on(table.created_at), }), @@ -263,10 +286,16 @@ export const appUsers = pgTable( first_seen_at: timestamp("first_seen_at").notNull().defaultNow(), last_seen_at: timestamp("last_seen_at").notNull().defaultNow(), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), }, (table) => ({ - app_user_unique_idx: uniqueIndex("app_users_app_user_idx").on(table.app_id, table.user_id), + app_user_unique_idx: uniqueIndex("app_users_app_user_idx").on( + table.app_id, + table.user_id, + ), app_id_idx: index("app_users_app_id_idx").on(table.app_id), user_id_idx: index("app_users_user_id_idx").on(table.user_id), first_seen_idx: index("app_users_first_seen_idx").on(table.first_seen_at), @@ -297,7 +326,9 @@ export const appAnalytics = pgTable( total_input_tokens: integer("total_input_tokens").default(0).notNull(), total_output_tokens: integer("total_output_tokens").default(0).notNull(), - total_cost: numeric("total_cost", { precision: 10, scale: 2 }).default("0.00"), + total_cost: numeric("total_cost", { precision: 10, scale: 2 }).default( + "0.00", + ), total_credits_used: numeric("total_credits_used", { precision: 10, scale: 2, @@ -315,9 +346,17 @@ export const appAnalytics = pgTable( }, (table) => ({ app_id_idx: index("app_analytics_app_id_idx").on(table.app_id), - period_idx: index("app_analytics_period_idx").on(table.period_start, table.period_end), - period_type_idx: index("app_analytics_period_type_idx").on(table.period_type), - app_period_idx: index("app_analytics_app_period_idx").on(table.app_id, table.period_start), + period_idx: index("app_analytics_period_idx").on( + table.period_start, + table.period_end, + ), + period_type_idx: index("app_analytics_period_type_idx").on( + table.period_type, + ), + app_period_idx: index("app_analytics_app_period_idx").on( + table.app_id, + table.period_start, + ), }), ); @@ -348,7 +387,9 @@ export const appRequests = pgTable( model: text("model"), input_tokens: integer("input_tokens").default(0), output_tokens: integer("output_tokens").default(0), - credits_used: numeric("credits_used", { precision: 10, scale: 6 }).default("0.00"), + credits_used: numeric("credits_used", { precision: 10, scale: 6 }).default( + "0.00", + ), response_time_ms: integer("response_time_ms"), status: text("status").notNull().default("success"), @@ -364,7 +405,10 @@ export const appRequests = pgTable( request_type_idx: index("app_requests_type_idx").on(table.request_type), source_idx: index("app_requests_source_idx").on(table.source), ip_idx: index("app_requests_ip_idx").on(table.ip_address), - app_created_idx: index("app_requests_app_created_idx").on(table.app_id, table.created_at), + app_created_idx: index("app_requests_app_created_idx").on( + table.app_id, + table.created_at, + ), }), ); diff --git a/packages/db/schemas/cli-auth-sessions.ts b/packages/db/schemas/cli-auth-sessions.ts index a552f37a0..b00a1c9cf 100644 --- a/packages/db/schemas/cli-auth-sessions.ts +++ b/packages/db/schemas/cli-auth-sessions.ts @@ -27,10 +27,14 @@ export const cliAuthSessions = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - session_id_idx: index("cli_auth_sessions_session_id_idx").on(table.session_id), + session_id_idx: index("cli_auth_sessions_session_id_idx").on( + table.session_id, + ), status_idx: index("cli_auth_sessions_status_idx").on(table.status), user_id_idx: index("cli_auth_sessions_user_id_idx").on(table.user_id), - expires_at_idx: index("cli_auth_sessions_expires_at_idx").on(table.expires_at), + expires_at_idx: index("cli_auth_sessions_expires_at_idx").on( + table.expires_at, + ), }), ); diff --git a/packages/db/schemas/containers.ts b/packages/db/schemas/containers.ts index f46a4d403..61fcb0b3d 100644 --- a/packages/db/schemas/containers.ts +++ b/packages/db/schemas/containers.ts @@ -67,28 +67,48 @@ export const containers = pgTable( last_health_check: timestamp("last_health_check"), deployment_log: text("deployment_log"), error_message: text("error_message"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), // Billing tracking fields last_billed_at: timestamp("last_billed_at"), next_billing_at: timestamp("next_billing_at"), billing_status: text("billing_status").default("active").notNull(), // active, warning, suspended, shutdown_pending shutdown_warning_sent_at: timestamp("shutdown_warning_sent_at"), scheduled_shutdown_at: timestamp("scheduled_shutdown_at"), - total_billed: numeric("total_billed", { precision: 10, scale: 2 }).default("0.00").notNull(), + total_billed: numeric("total_billed", { precision: 10, scale: 2 }) + .default("0.00") + .notNull(), created_at: timestamp("created_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(), }, (table) => ({ - organization_idx: index("containers_organization_idx").on(table.organization_id), + organization_idx: index("containers_organization_idx").on( + table.organization_id, + ), user_idx: index("containers_user_idx").on(table.user_id), status_idx: index("containers_status_idx").on(table.status), character_idx: index("containers_character_idx").on(table.character_id), - ecs_service_idx: index("containers_ecs_service_idx").on(table.ecs_service_arn), - ecr_repository_idx: index("containers_ecr_repository_idx").on(table.ecr_repository_uri), - project_name_idx: index("containers_project_name_idx").on(table.project_name), - user_project_idx: index("containers_user_project_idx").on(table.user_id, table.project_name), - billing_status_idx: index("containers_billing_status_idx").on(table.billing_status), - next_billing_idx: index("containers_next_billing_idx").on(table.next_billing_at), + ecs_service_idx: index("containers_ecs_service_idx").on( + table.ecs_service_arn, + ), + ecr_repository_idx: index("containers_ecr_repository_idx").on( + table.ecr_repository_uri, + ), + project_name_idx: index("containers_project_name_idx").on( + table.project_name, + ), + user_project_idx: index("containers_user_project_idx").on( + table.user_id, + table.project_name, + ), + billing_status_idx: index("containers_billing_status_idx").on( + table.billing_status, + ), + next_billing_idx: index("containers_next_billing_idx").on( + table.next_billing_at, + ), scheduled_shutdown_idx: index("containers_scheduled_shutdown_idx").on( table.scheduled_shutdown_at, ), @@ -114,16 +134,25 @@ export const containerBillingRecords = pgTable( billing_period_start: timestamp("billing_period_start").notNull(), billing_period_end: timestamp("billing_period_end").notNull(), status: text("status").default("success").notNull(), // success, failed, insufficient_credits - credit_transaction_id: uuid("credit_transaction_id").references(() => creditTransactions.id, { - onDelete: "set null", - }), + credit_transaction_id: uuid("credit_transaction_id").references( + () => creditTransactions.id, + { + onDelete: "set null", + }, + ), error_message: text("error_message"), created_at: timestamp("created_at").defaultNow().notNull(), }, (table) => ({ - container_idx: index("container_billing_records_container_idx").on(table.container_id), - org_idx: index("container_billing_records_org_idx").on(table.organization_id), - created_idx: index("container_billing_records_created_idx").on(table.created_at), + container_idx: index("container_billing_records_container_idx").on( + table.container_id, + ), + org_idx: index("container_billing_records_org_idx").on( + table.organization_id, + ), + created_idx: index("container_billing_records_created_idx").on( + table.created_at, + ), status_idx: index("container_billing_records_status_idx").on(table.status), }), ); @@ -131,5 +160,9 @@ export const containerBillingRecords = pgTable( // Type inference export type Container = InferSelectModel; export type NewContainer = InferInsertModel; -export type ContainerBillingRecord = InferSelectModel; -export type NewContainerBillingRecord = InferInsertModel; +export type ContainerBillingRecord = InferSelectModel< + typeof containerBillingRecords +>; +export type NewContainerBillingRecord = InferInsertModel< + typeof containerBillingRecords +>; diff --git a/packages/db/schemas/conversations.ts b/packages/db/schemas/conversations.ts index 234142771..75395466d 100644 --- a/packages/db/schemas/conversations.ts +++ b/packages/db/schemas/conversations.ts @@ -24,9 +24,12 @@ export const conversations = pgTable( { id: uuid("id").defaultRandom().primaryKey(), // Allow NULL for anonymous users who don't have organizations - organization_id: uuid("organization_id").references(() => organizations.id, { - onDelete: "cascade", - }), + organization_id: uuid("organization_id").references( + () => organizations.id, + { + onDelete: "cascade", + }, + ), user_id: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), @@ -52,13 +55,17 @@ export const conversations = pgTable( }), status: text("status").notNull().default("active"), message_count: integer("message_count").notNull().default(0), - total_cost: numeric("total_cost", { precision: 10, scale: 2 }).notNull().default("0.00"), + total_cost: numeric("total_cost", { precision: 10, scale: 2 }) + .notNull() + .default("0.00"), last_message_at: timestamp("last_message_at"), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("conversations_organization_idx").on(table.organization_id), + organization_idx: index("conversations_organization_idx").on( + table.organization_id, + ), user_idx: index("conversations_user_idx").on(table.user_id), updated_idx: index("conversations_updated_idx").on(table.updated_at), status_idx: index("conversations_status_idx").on(table.status), @@ -93,7 +100,9 @@ export const conversationMessages = pgTable( created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ - conversation_idx: index("conv_messages_conversation_idx").on(table.conversation_id), + conversation_idx: index("conv_messages_conversation_idx").on( + table.conversation_id, + ), sequence_idx: index("conv_messages_sequence_idx").on( table.conversation_id, table.sequence_number, @@ -106,4 +115,6 @@ export const conversationMessages = pgTable( export type Conversation = InferSelectModel; export type NewConversation = InferInsertModel; export type ConversationMessage = InferSelectModel; -export type NewConversationMessage = InferInsertModel; +export type NewConversationMessage = InferInsertModel< + typeof conversationMessages +>; diff --git a/packages/db/schemas/credit-packs.ts b/packages/db/schemas/credit-packs.ts index 948b992b6..47f72cafd 100644 --- a/packages/db/schemas/credit-packs.ts +++ b/packages/db/schemas/credit-packs.ts @@ -28,12 +28,17 @@ export const creditPacks = pgTable( stripe_product_id: text("stripe_product_id").notNull(), is_active: boolean("is_active").notNull().default(true), sort_order: integer("sort_order").notNull().default(0), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - stripe_price_idx: index("credit_packs_stripe_price_idx").on(table.stripe_price_id), + stripe_price_idx: index("credit_packs_stripe_price_idx").on( + table.stripe_price_id, + ), active_idx: index("credit_packs_active_idx").on(table.is_active), sort_idx: index("credit_packs_sort_idx").on(table.sort_order), }), diff --git a/packages/db/schemas/credit-transactions.ts b/packages/db/schemas/credit-transactions.ts index 004320a0a..0311976ef 100644 --- a/packages/db/schemas/credit-transactions.ts +++ b/packages/db/schemas/credit-transactions.ts @@ -30,18 +30,25 @@ export const creditTransactions = pgTable( amount: numeric("amount", { precision: 12, scale: 6 }).notNull(), type: text("type").notNull(), description: text("description"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), stripe_payment_intent_id: text("stripe_payment_intent_id"), created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("credit_transactions_organization_idx").on(table.organization_id), + organization_idx: index("credit_transactions_organization_idx").on( + table.organization_id, + ), user_idx: index("credit_transactions_user_idx").on(table.user_id), type_idx: index("credit_transactions_type_idx").on(table.type), - created_at_idx: index("credit_transactions_created_at_idx").on(table.created_at), - stripe_payment_intent_idx: uniqueIndex("credit_transactions_stripe_payment_intent_idx").on( - table.stripe_payment_intent_id, + created_at_idx: index("credit_transactions_created_at_idx").on( + table.created_at, ), + stripe_payment_intent_idx: uniqueIndex( + "credit_transactions_stripe_payment_intent_idx", + ).on(table.stripe_payment_intent_id), }), ); diff --git a/packages/db/schemas/crypto-payments.ts b/packages/db/schemas/crypto-payments.ts index 8b4c1985a..d5f944a3f 100644 --- a/packages/db/schemas/crypto-payments.ts +++ b/packages/db/schemas/crypto-payments.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; import { users } from "./users"; @@ -49,19 +57,26 @@ export const cryptoPayments = pgTable( metadata: jsonb("metadata").$type>().default({}), }, (table) => ({ - org_idx: index("crypto_payments_organization_id_idx").on(table.organization_id), + org_idx: index("crypto_payments_organization_id_idx").on( + table.organization_id, + ), user_idx: index("crypto_payments_user_id_idx").on(table.user_id), - payment_address_idx: index("crypto_payments_payment_address_idx").on(table.payment_address), + payment_address_idx: index("crypto_payments_payment_address_idx").on( + table.payment_address, + ), status_idx: index("crypto_payments_status_idx").on(table.status), // Unique constraint to prevent duplicate transaction processing - tx_hash_unique_idx: uniqueIndex("crypto_payments_transaction_hash_unique_idx").on( - table.transaction_hash, - ), + tx_hash_unique_idx: uniqueIndex( + "crypto_payments_transaction_hash_unique_idx", + ).on(table.transaction_hash), network_idx: index("crypto_payments_network_idx").on(table.network), created_idx: index("crypto_payments_created_at_idx").on(table.created_at), expires_idx: index("crypto_payments_expires_at_idx").on(table.expires_at), // GIN index for efficient JSONB queries on oxapay_track_id - metadata_gin_idx: index("crypto_payments_metadata_gin_idx").using("gin", table.metadata), + metadata_gin_idx: index("crypto_payments_metadata_gin_idx").using( + "gin", + table.metadata, + ), }), ); diff --git a/packages/db/schemas/daily-metrics.ts b/packages/db/schemas/daily-metrics.ts index 2d1305a98..f5dd01221 100644 --- a/packages/db/schemas/daily-metrics.ts +++ b/packages/db/schemas/daily-metrics.ts @@ -10,7 +10,12 @@ import { uuid, } from "drizzle-orm/pg-core"; -export type MetricsPlatform = "web" | "telegram" | "discord" | "imessage" | "sms"; +export type MetricsPlatform = + | "web" + | "telegram" + | "discord" + | "imessage" + | "sms"; /** * Pre-computed daily engagement metrics. diff --git a/packages/db/schemas/device-bus.ts b/packages/db/schemas/device-bus.ts index 9019fb492..c083f7825 100644 --- a/packages/db/schemas/device-bus.ts +++ b/packages/db/schemas/device-bus.ts @@ -8,7 +8,15 @@ */ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { users } from "./users"; export const devices = pgTable( @@ -22,12 +30,18 @@ export const devices = pgTable( push_token: text("push_token"), label: text("label"), online: boolean("online").notNull().default(false), - last_seen_at: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + last_seen_at: timestamp("last_seen_at", { withTimezone: true }) + .notNull() + .defaultNow(), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ userIdx: index("device_bus_devices_user_id_idx").on(table.user_id), - lastSeenIdx: index("device_bus_devices_last_seen_idx").on(table.last_seen_at), + lastSeenIdx: index("device_bus_devices_last_seen_idx").on( + table.last_seen_at, + ), }), ); @@ -44,7 +58,9 @@ export const deviceIntents = pgTable( kind: text("kind").notNull(), payload: jsonb("payload").notNull().default({}), delivered_to: jsonb("delivered_to").notNull().default([]), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ userCreatedIdx: index("device_bus_intents_user_created_idx").on( diff --git a/packages/db/schemas/discord-channels.ts b/packages/db/schemas/discord-channels.ts index cf22ae16d..a8c546dc8 100644 --- a/packages/db/schemas/discord-channels.ts +++ b/packages/db/schemas/discord-channels.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + integer, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -29,14 +37,21 @@ export const discordChannels = pgTable( can_attach_files: boolean("can_attach_files").default(true).notNull(), is_nsfw: boolean("is_nsfw").default(false).notNull(), - created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + created_at: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("discord_channels_organization_id_idx").on(table.organization_id), index("discord_channels_guild_id_idx").on(table.guild_id), index("discord_channels_channel_id_idx").on(table.channel_id), - index("discord_channels_guild_channel_idx").on(table.guild_id, table.channel_id), + index("discord_channels_guild_channel_idx").on( + table.guild_id, + table.channel_id, + ), ], ); diff --git a/packages/db/schemas/discord-connections.ts b/packages/db/schemas/discord-connections.ts index 6516567f6..e2497f2ef 100644 --- a/packages/db/schemas/discord-connections.ts +++ b/packages/db/schemas/discord-connections.ts @@ -40,7 +40,9 @@ export const DiscordConnectionMetadataSchema = z ) .optional(); -export type DiscordConnectionMetadata = z.infer; +export type DiscordConnectionMetadata = z.infer< + typeof DiscordConnectionMetadataSchema +>; /** * Discord Gateway Intents @@ -142,8 +144,12 @@ export const discordConnections = pgTable( */ metadata: jsonb("metadata").$type(), - created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + created_at: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("discord_connections_organization_id_idx").on(table.organization_id), diff --git a/packages/db/schemas/discord-guilds.ts b/packages/db/schemas/discord-guilds.ts index 2de9c3674..372026a16 100644 --- a/packages/db/schemas/discord-guilds.ts +++ b/packages/db/schemas/discord-guilds.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -23,16 +30,25 @@ export const discordGuilds = pgTable( owner_id: text("owner_id"), // Discord user ID who added the bot bot_permissions: text("bot_permissions"), // Bitfield of granted permissions - bot_joined_at: timestamp("bot_joined_at", { withTimezone: true }).defaultNow().notNull(), + bot_joined_at: timestamp("bot_joined_at", { withTimezone: true }) + .defaultNow() + .notNull(), is_active: boolean("is_active").default(true).notNull(), - created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + created_at: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("discord_guilds_organization_id_idx").on(table.organization_id), index("discord_guilds_guild_id_idx").on(table.guild_id), - index("discord_guilds_org_guild_idx").on(table.organization_id, table.guild_id), + index("discord_guilds_org_guild_idx").on( + table.organization_id, + table.guild_id, + ), ], ); diff --git a/packages/db/schemas/docker-nodes.ts b/packages/db/schemas/docker-nodes.ts index a7c9a6f61..86e78c345 100644 --- a/packages/db/schemas/docker-nodes.ts +++ b/packages/db/schemas/docker-nodes.ts @@ -21,14 +21,24 @@ export const dockerNodes = pgTable( ssh_port: integer("ssh_port").notNull().default(22), capacity: integer("capacity").notNull().default(8), enabled: boolean("enabled").notNull().default(true), - status: text("status").$type().notNull().default("unknown"), + status: text("status") + .$type() + .notNull() + .default("unknown"), allocated_count: integer("allocated_count").notNull().default(0), last_health_check: timestamp("last_health_check", { withTimezone: true }), ssh_user: text("ssh_user").notNull().default("root"), host_key_fingerprint: text("host_key_fingerprint"), - metadata: jsonb("metadata").$type>().notNull().default({}), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), - updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + metadata: jsonb("metadata") + .$type>() + .notNull() + .default({}), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ node_id_idx: index("docker_nodes_node_id_idx").on(table.node_id), diff --git a/packages/db/schemas/eliza-room-characters.ts b/packages/db/schemas/eliza-room-characters.ts index 1ed1823fd..8f7c656fe 100644 --- a/packages/db/schemas/eliza-room-characters.ts +++ b/packages/db/schemas/eliza-room-characters.ts @@ -18,4 +18,5 @@ export const elizaRoomCharactersTable = pgTable("eliza_room_characters", { }); export type ElizaRoomCharacter = typeof elizaRoomCharactersTable.$inferSelect; -export type NewElizaRoomCharacter = typeof elizaRoomCharactersTable.$inferInsert; +export type NewElizaRoomCharacter = + typeof elizaRoomCharactersTable.$inferInsert; diff --git a/packages/db/schemas/eliza.ts b/packages/db/schemas/eliza.ts index 84788dd09..8198884db 100644 --- a/packages/db/schemas/eliza.ts +++ b/packages/db/schemas/eliza.ts @@ -6,7 +6,11 @@ */ import plugin from "@elizaos/plugin-sql/node"; -import { longTermMemories, memoryAccessLogs, sessionSummaries } from "./advanced-memory"; +import { + longTermMemories, + memoryAccessLogs, + sessionSummaries, +} from "./advanced-memory"; /** * Re-exported elizaOS plugin-sql tables. @@ -29,7 +33,10 @@ export const { messageServerTable, channelTable, channelParticipantsTable, -} = (plugin as unknown as { schema: Record }).schema as Record; +} = (plugin as unknown as { schema: Record }).schema as Record< + string, + any +>; /** * Re-exported elizaOS memory plugin tables. diff --git a/packages/db/schemas/entity-settings.ts b/packages/db/schemas/entity-settings.ts index 186e1d6f9..ad501993e 100644 --- a/packages/db/schemas/entity-settings.ts +++ b/packages/db/schemas/entity-settings.ts @@ -22,7 +22,14 @@ */ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { + index, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; import { users } from "./users"; /** @@ -95,12 +102,16 @@ export const entitySettings = pgTable( /** * Timestamp when this setting was created. */ - created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + created_at: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), /** * Timestamp when this setting was last updated. */ - updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => ({ /** @@ -125,7 +136,10 @@ export const entitySettings = pgTable( /** * Composite index for agent-specific lookups. */ - user_agent_idx: index("entity_settings_user_agent_idx").on(table.user_id, table.agent_id), + user_agent_idx: index("entity_settings_user_agent_idx").on( + table.user_id, + table.agent_id, + ), /** * Index for key lookups across users (for admin/audit purposes). diff --git a/packages/db/schemas/generations.ts b/packages/db/schemas/generations.ts index 6167bdd8c..dfad4dcb9 100644 --- a/packages/db/schemas/generations.ts +++ b/packages/db/schemas/generations.ts @@ -47,16 +47,26 @@ export const generations = pgTable( file_size: bigint("file_size", { mode: "bigint" }), mime_type: text("mime_type"), parameters: jsonb("parameters").$type>(), - settings: jsonb("settings").$type>().notNull().default({}), - metadata: jsonb("metadata").$type>().default({}).notNull(), + settings: jsonb("settings") + .$type>() + .notNull() + .default({}), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), dimensions: jsonb("dimensions").$type<{ width?: number; height?: number; duration?: number; }>(), tokens: integer("tokens"), - cost: numeric("cost", { precision: 10, scale: 2 }).notNull().default("0.00"), - credits: numeric("credits", { precision: 10, scale: 2 }).notNull().default("0.00"), + cost: numeric("cost", { precision: 10, scale: 2 }) + .notNull() + .default("0.00"), + credits: numeric("credits", { precision: 10, scale: 2 }) + .notNull() + .default("0.00"), usage_record_id: uuid("usage_record_id").references(() => usageRecords.id, { onDelete: "set null", }), @@ -66,7 +76,9 @@ export const generations = pgTable( completed_at: timestamp("completed_at"), }, (table) => ({ - organization_idx: index("generations_organization_idx").on(table.organization_id), + organization_idx: index("generations_organization_idx").on( + table.organization_id, + ), user_idx: index("generations_user_idx").on(table.user_id), api_key_idx: index("generations_api_key_idx").on(table.api_key_id), type_idx: index("generations_type_idx").on(table.type), @@ -78,12 +90,9 @@ export const generations = pgTable( table.status, ), // Composite index for gallery queries: org + status + user + created_at ordering - org_status_user_created_idx: index("generations_org_status_user_created_idx").on( - table.organization_id, - table.status, - table.user_id, - table.created_at, - ), + org_status_user_created_idx: index( + "generations_org_status_user_created_idx", + ).on(table.organization_id, table.status, table.user_id, table.created_at), }), ); diff --git a/packages/db/schemas/invoices.ts b/packages/db/schemas/invoices.ts index c9a859b2b..37574957f 100644 --- a/packages/db/schemas/invoices.ts +++ b/packages/db/schemas/invoices.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, numeric, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + numeric, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; /** * Invoices table schema. @@ -38,7 +46,9 @@ export const invoices = pgTable( }, (table) => ({ org_idx: index("invoices_organization_idx").on(table.organization_id), - stripe_invoice_idx: index("invoices_stripe_invoice_idx").on(table.stripe_invoice_id), + stripe_invoice_idx: index("invoices_stripe_invoice_idx").on( + table.stripe_invoice_id, + ), status_idx: index("invoices_status_idx").on(table.status), }), ); diff --git a/packages/db/schemas/jobs.ts b/packages/db/schemas/jobs.ts index 4ef68ab47..e4c410d0f 100644 --- a/packages/db/schemas/jobs.ts +++ b/packages/db/schemas/jobs.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + integer, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { apiKeys } from "./api-keys"; import { generations } from "./generations"; import { organizations } from "./organizations"; diff --git a/packages/db/schemas/llm-trajectories.ts b/packages/db/schemas/llm-trajectories.ts index 4aa52bc5f..a57a5af76 100644 --- a/packages/db/schemas/llm-trajectories.ts +++ b/packages/db/schemas/llm-trajectories.ts @@ -58,9 +58,15 @@ export const llmTrajectories = pgTable( total_tokens: integer("total_tokens").notNull().default(0), // Cost tracking - input_cost: numeric("input_cost", { precision: 12, scale: 6 }).default("0.000000"), - output_cost: numeric("output_cost", { precision: 12, scale: 6 }).default("0.000000"), - total_cost: numeric("total_cost", { precision: 12, scale: 6 }).default("0.000000"), + input_cost: numeric("input_cost", { precision: 12, scale: 6 }).default( + "0.000000", + ), + output_cost: numeric("output_cost", { precision: 12, scale: 6 }).default( + "0.000000", + ), + total_cost: numeric("total_cost", { precision: 12, scale: 6 }).default( + "0.000000", + ), // Performance latency_ms: integer("latency_ms"), @@ -70,7 +76,10 @@ export const llmTrajectories = pgTable( error_message: text("error_message"), // Extensibility - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), }, @@ -79,13 +88,14 @@ export const llmTrajectories = pgTable( table.organization_id, table.created_at, ), - org_model_idx: index("llm_trajectories_org_model_idx").on(table.organization_id, table.model), - purpose_idx: index("llm_trajectories_purpose_idx").on(table.purpose), - org_purpose_created_idx: index("llm_trajectories_org_purpose_created_idx").on( + org_model_idx: index("llm_trajectories_org_model_idx").on( table.organization_id, - table.purpose, - table.created_at, + table.model, ), + purpose_idx: index("llm_trajectories_purpose_idx").on(table.purpose), + org_purpose_created_idx: index( + "llm_trajectories_org_purpose_created_idx", + ).on(table.organization_id, table.purpose, table.created_at), }), ); diff --git a/packages/db/schemas/managed-domains.ts b/packages/db/schemas/managed-domains.ts index fb23b1890..d4ed8163a 100644 --- a/packages/db/schemas/managed-domains.ts +++ b/packages/db/schemas/managed-domains.ts @@ -25,9 +25,15 @@ import { userCharacters } from "./user-characters"; import { userMcps } from "./user-mcps"; // Enums -export const domainRegistrarEnum = pgEnum("domain_registrar", ["vercel", "external"]); +export const domainRegistrarEnum = pgEnum("domain_registrar", [ + "vercel", + "external", +]); -export const domainNameserverModeEnum = pgEnum("domain_nameserver_mode", ["vercel", "external"]); +export const domainNameserverModeEnum = pgEnum("domain_nameserver_mode", [ + "vercel", + "external", +]); export const domainResourceTypeEnum = pgEnum("domain_resource_type", [ "app", @@ -155,7 +161,9 @@ export const managedDomains = pgTable( }), // DNS configuration - nameserverMode: domainNameserverModeEnum("nameserver_mode").notNull().default("vercel"), + nameserverMode: domainNameserverModeEnum("nameserver_mode") + .notNull() + .default("vercel"), dnsRecords: jsonb("dns_records").$type().default([]), sslStatus: text("ssl_status") .$type<"pending" | "provisioning" | "active" | "error">() @@ -168,8 +176,12 @@ export const managedDomains = pgTable( verifiedAt: timestamp("verified_at"), // Moderation - moderationStatus: domainModerationStatusEnum("moderation_status").notNull().default("clean"), - moderationFlags: jsonb("moderation_flags").$type().default([]), + moderationStatus: domainModerationStatusEnum("moderation_status") + .notNull() + .default("clean"), + moderationFlags: jsonb("moderation_flags") + .$type() + .default([]), // Health monitoring lastHealthCheck: timestamp("last_health_check"), @@ -187,7 +199,9 @@ export const managedDomains = pgTable( // Suspension tracking suspendedAt: timestamp("suspended_at"), suspensionReason: text("suspension_reason"), - suspensionNotification: jsonb("suspension_notification").$type(), + suspensionNotification: jsonb( + "suspension_notification", + ).$type(), ownerNotifiedAt: timestamp("owner_notified_at"), // Pricing (for purchased domains) @@ -208,9 +222,13 @@ export const managedDomains = pgTable( agentIdx: index("managed_domains_agent_idx").on(table.agentId), mcpIdx: index("managed_domains_mcp_idx").on(table.mcpId), statusIdx: index("managed_domains_status_idx").on(table.status), - moderationIdx: index("managed_domains_moderation_idx").on(table.moderationStatus), + moderationIdx: index("managed_domains_moderation_idx").on( + table.moderationStatus, + ), expiresIdx: index("managed_domains_expires_idx").on(table.expiresAt), - contentScanIdx: index("managed_domains_content_scan_idx").on(table.lastContentScanAt), + contentScanIdx: index("managed_domains_content_scan_idx").on( + table.lastContentScanAt, + ), suspendedIdx: index("managed_domains_suspended_idx").on(table.suspendedAt), }), ); diff --git a/packages/db/schemas/milady-pairing-tokens.ts b/packages/db/schemas/milady-pairing-tokens.ts index fecead381..1faea4901 100644 --- a/packages/db/schemas/milady-pairing-tokens.ts +++ b/packages/db/schemas/milady-pairing-tokens.ts @@ -22,14 +22,22 @@ export const miladyPairingTokens = pgTable( expected_origin: text("expected_origin").notNull(), expires_at: timestamp("expires_at", { withTimezone: true }).notNull(), used_at: timestamp("used_at", { withTimezone: true }), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ - tokenHashIdx: index("milady_pairing_tokens_token_hash_idx").on(table.token_hash), - expiresAtIdx: index("milady_pairing_tokens_expires_at_idx").on(table.expires_at), + tokenHashIdx: index("milady_pairing_tokens_token_hash_idx").on( + table.token_hash, + ), + expiresAtIdx: index("milady_pairing_tokens_expires_at_idx").on( + table.expires_at, + ), agentIdx: index("milady_pairing_tokens_agent_id_idx").on(table.agent_id), }), ); export type MiladyPairingToken = InferSelectModel; -export type NewMiladyPairingToken = InferInsertModel; +export type NewMiladyPairingToken = InferInsertModel< + typeof miladyPairingTokens +>; diff --git a/packages/db/schemas/milady-sandboxes.ts b/packages/db/schemas/milady-sandboxes.ts index 1e9f695c6..e989d2db6 100644 --- a/packages/db/schemas/milady-sandboxes.ts +++ b/packages/db/schemas/milady-sandboxes.ts @@ -43,7 +43,10 @@ export const miladySandboxes = pgTable( onDelete: "set null", }), sandbox_id: text("sandbox_id"), - status: text("status").$type().notNull().default("pending"), + status: text("status") + .$type() + .notNull() + .default("pending"), bridge_url: text("bridge_url"), health_url: text("health_url"), agent_name: text("agent_name"), @@ -73,22 +76,45 @@ export const miladySandboxes = pgTable( headscale_ip: text("headscale_ip"), docker_image: text("docker_image"), // Billing tracking fields (mirrors containers table pattern) - billing_status: text("billing_status").$type().notNull().default("active"), + billing_status: text("billing_status") + .$type() + .notNull() + .default("active"), last_billed_at: timestamp("last_billed_at", { withTimezone: true }), - hourly_rate: numeric("hourly_rate", { precision: 10, scale: 4 }).default("0.0100"), - total_billed: numeric("total_billed", { precision: 10, scale: 2 }).default("0.00").notNull(), - shutdown_warning_sent_at: timestamp("shutdown_warning_sent_at", { withTimezone: true }), - scheduled_shutdown_at: timestamp("scheduled_shutdown_at", { withTimezone: true }), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), - updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + hourly_rate: numeric("hourly_rate", { precision: 10, scale: 4 }).default( + "0.0100", + ), + total_billed: numeric("total_billed", { precision: 10, scale: 2 }) + .default("0.00") + .notNull(), + shutdown_warning_sent_at: timestamp("shutdown_warning_sent_at", { + withTimezone: true, + }), + scheduled_shutdown_at: timestamp("scheduled_shutdown_at", { + withTimezone: true, + }), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ - organization_idx: index("milady_sandboxes_organization_idx").on(table.organization_id), + organization_idx: index("milady_sandboxes_organization_idx").on( + table.organization_id, + ), user_idx: index("milady_sandboxes_user_idx").on(table.user_id), status_idx: index("milady_sandboxes_status_idx").on(table.status), - character_idx: index("milady_sandboxes_character_idx").on(table.character_id), - sandbox_id_idx: index("milady_sandboxes_sandbox_id_idx").on(table.sandbox_id), - billing_status_idx: index("milady_sandboxes_billing_status_idx").on(table.billing_status), + character_idx: index("milady_sandboxes_character_idx").on( + table.character_id, + ), + sandbox_id_idx: index("milady_sandboxes_sandbox_id_idx").on( + table.sandbox_id, + ), + billing_status_idx: index("milady_sandboxes_billing_status_idx").on( + table.billing_status, + ), }), ); @@ -107,19 +133,29 @@ export const miladySandboxBackups = pgTable( sandbox_record_id: uuid("sandbox_record_id") .notNull() .references(() => miladySandboxes.id, { onDelete: "cascade" }), - snapshot_type: text("snapshot_type").$type().notNull(), + snapshot_type: text("snapshot_type") + .$type() + .notNull(), state_data: jsonb("state_data").$type().notNull(), vercel_snapshot_id: text("vercel_snapshot_id"), size_bytes: bigint("size_bytes", { mode: "number" }), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ - sandbox_record_idx: index("milady_sandbox_backups_sandbox_idx").on(table.sandbox_record_id), - created_at_idx: index("milady_sandbox_backups_created_at_idx").on(table.created_at), + sandbox_record_idx: index("milady_sandbox_backups_sandbox_idx").on( + table.sandbox_record_id, + ), + created_at_idx: index("milady_sandbox_backups_created_at_idx").on( + table.created_at, + ), }), ); export type MiladySandbox = InferSelectModel; export type NewMiladySandbox = InferInsertModel; export type MiladySandboxBackup = InferSelectModel; -export type NewMiladySandboxBackup = InferInsertModel; +export type NewMiladySandboxBackup = InferInsertModel< + typeof miladySandboxBackups +>; diff --git a/packages/db/schemas/model-pricing.ts b/packages/db/schemas/model-pricing.ts index 5f0cf2a42..8843a7350 100644 --- a/packages/db/schemas/model-pricing.ts +++ b/packages/db/schemas/model-pricing.ts @@ -40,12 +40,18 @@ export const modelPricing = pgTable( is_active: boolean("is_active").notNull().default(true), effective_from: timestamp("effective_from").notNull().defaultNow(), effective_until: timestamp("effective_until"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - provider_model_idx: index("model_pricing_provider_model_idx").on(table.provider, table.model), + provider_model_idx: index("model_pricing_provider_model_idx").on( + table.provider, + table.model, + ), active_idx: index("model_pricing_active_idx").on(table.is_active), }), ); diff --git a/packages/db/schemas/moderation-violations.ts b/packages/db/schemas/moderation-violations.ts index f4028cb06..fa02574cb 100644 --- a/packages/db/schemas/moderation-violations.ts +++ b/packages/db/schemas/moderation-violations.ts @@ -68,7 +68,9 @@ export const moderationViolations = pgTable( (table) => ({ userIdIdx: index("moderation_violations_user_id_idx").on(table.userId), actionIdx: index("moderation_violations_action_idx").on(table.action), - createdAtIdx: index("moderation_violations_created_at_idx").on(table.createdAt), + createdAtIdx: index("moderation_violations_created_at_idx").on( + table.createdAt, + ), roomIdIdx: index("moderation_violations_room_id_idx").on(table.roomId), }), ); @@ -114,7 +116,9 @@ export const userModerationStatus = pgTable( (table) => ({ userIdIdx: index("user_moderation_status_user_id_idx").on(table.userId), statusIdx: index("user_moderation_status_status_idx").on(table.status), - riskScoreIdx: index("user_moderation_status_risk_score_idx").on(table.riskScore), + riskScoreIdx: index("user_moderation_status_risk_score_idx").on( + table.riskScore, + ), totalViolationsIdx: index("user_moderation_status_total_violations_idx").on( table.totalViolations, ), @@ -122,7 +126,13 @@ export const userModerationStatus = pgTable( ); export type ModerationViolation = InferSelectModel; -export type NewModerationViolation = InferInsertModel; - -export type UserModerationStatus = InferSelectModel; -export type NewUserModerationStatus = InferInsertModel; +export type NewModerationViolation = InferInsertModel< + typeof moderationViolations +>; + +export type UserModerationStatus = InferSelectModel< + typeof userModerationStatus +>; +export type NewUserModerationStatus = InferInsertModel< + typeof userModerationStatus +>; diff --git a/packages/db/schemas/org-rate-limit-overrides.ts b/packages/db/schemas/org-rate-limit-overrides.ts index a79a22278..75c940bf6 100644 --- a/packages/db/schemas/org-rate-limit-overrides.ts +++ b/packages/db/schemas/org-rate-limit-overrides.ts @@ -26,5 +26,9 @@ export const orgRateLimitOverrides = pgTable("org_rate_limit_overrides", { updated_at: timestamp("updated_at").notNull().defaultNow(), }); -export type OrgRateLimitOverride = InferSelectModel; -export type NewOrgRateLimitOverride = InferInsertModel; +export type OrgRateLimitOverride = InferSelectModel< + typeof orgRateLimitOverrides +>; +export type NewOrgRateLimitOverride = InferInsertModel< + typeof orgRateLimitOverrides +>; diff --git a/packages/db/schemas/organization-billing.ts b/packages/db/schemas/organization-billing.ts index caa7a770e..7389c336e 100644 --- a/packages/db/schemas/organization-billing.ts +++ b/packages/db/schemas/organization-billing.ts @@ -36,7 +36,9 @@ export const organizationBilling = pgTable( stripe_default_payment_method: text("stripe_default_payment_method"), // Auto top-up - auto_top_up_enabled: boolean("auto_top_up_enabled").default(false).notNull(), + auto_top_up_enabled: boolean("auto_top_up_enabled") + .default(false) + .notNull(), auto_top_up_amount: numeric("auto_top_up_amount", { precision: 12, scale: 6, @@ -52,8 +54,12 @@ export const organizationBilling = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("org_billing_organization_idx").on(table.organization_id), - stripe_customer_idx: index("org_billing_stripe_customer_idx").on(table.stripe_customer_id), + organization_idx: index("org_billing_organization_idx").on( + table.organization_id, + ), + stripe_customer_idx: index("org_billing_stripe_customer_idx").on( + table.stripe_customer_id, + ), auto_top_up_enabled_idx: index("org_billing_auto_top_up_enabled_idx").on( table.auto_top_up_enabled, ), @@ -62,4 +68,6 @@ export const organizationBilling = pgTable( // Type inference export type OrganizationBilling = InferSelectModel; -export type NewOrganizationBilling = InferInsertModel; +export type NewOrganizationBilling = InferInsertModel< + typeof organizationBilling +>; diff --git a/packages/db/schemas/organization-config.ts b/packages/db/schemas/organization-config.ts index 8335abdd6..a0142c530 100644 --- a/packages/db/schemas/organization-config.ts +++ b/packages/db/schemas/organization-config.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + integer, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -26,18 +34,29 @@ export const organizationConfig = pgTable( max_tokens_per_request: integer("max_tokens_per_request"), // Model/provider restrictions - allowed_models: jsonb("allowed_models").$type().notNull().default([]), - allowed_providers: jsonb("allowed_providers").$type().notNull().default([]), + allowed_models: jsonb("allowed_models") + .$type() + .notNull() + .default([]), + allowed_providers: jsonb("allowed_providers") + .$type() + .notNull() + .default([]), // General settings - settings: jsonb("settings").$type>().default({}).notNull(), + settings: jsonb("settings") + .$type>() + .default({}) + .notNull(), // Lifecycle created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("org_config_organization_idx").on(table.organization_id), + organization_idx: index("org_config_organization_idx").on( + table.organization_id, + ), }), ); diff --git a/packages/db/schemas/organization-encryption-keys.ts b/packages/db/schemas/organization-encryption-keys.ts index 729510488..023aec176 100644 --- a/packages/db/schemas/organization-encryption-keys.ts +++ b/packages/db/schemas/organization-encryption-keys.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, integer, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core"; +import { + index, + integer, + pgTable, + text, + timestamp, + unique, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -31,11 +39,17 @@ export const organizationEncryptionKeys = pgTable( rotated_at: timestamp("rotated_at"), }, (table) => ({ - orgUnique: unique("organization_encryption_keys_org_unique").on(table.organization_id), + orgUnique: unique("organization_encryption_keys_org_unique").on( + table.organization_id, + ), orgIdx: index("org_encryption_keys_org_idx").on(table.organization_id), }), ); // Type inference -export type OrganizationEncryptionKey = InferSelectModel; -export type NewOrganizationEncryptionKey = InferInsertModel; +export type OrganizationEncryptionKey = InferSelectModel< + typeof organizationEncryptionKeys +>; +export type NewOrganizationEncryptionKey = InferInsertModel< + typeof organizationEncryptionKeys +>; diff --git a/packages/db/schemas/organization-invites.ts b/packages/db/schemas/organization-invites.ts index 5da97c94a..9d2456abb 100644 --- a/packages/db/schemas/organization-invites.ts +++ b/packages/db/schemas/organization-invites.ts @@ -27,9 +27,12 @@ export const organizationInvites = pgTable( status: text("status").notNull().default("pending"), accepted_at: timestamp("accepted_at"), - accepted_by_user_id: uuid("accepted_by_user_id").references(() => users.id, { - onDelete: "set null", - }), + accepted_by_user_id: uuid("accepted_by_user_id").references( + () => users.id, + { + onDelete: "set null", + }, + ), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), @@ -43,4 +46,6 @@ export const organizationInvites = pgTable( ); export type OrganizationInvite = InferSelectModel; -export type NewOrganizationInvite = InferInsertModel; +export type NewOrganizationInvite = InferInsertModel< + typeof organizationInvites +>; diff --git a/packages/db/schemas/organizations.ts b/packages/db/schemas/organizations.ts index 30d1280f6..6468b5a6b 100644 --- a/packages/db/schemas/organizations.ts +++ b/packages/db/schemas/organizations.ts @@ -43,8 +43,14 @@ export const organizations = pgTable( stripe_payment_method_id: text("stripe_payment_method_id"), stripe_default_payment_method: text("stripe_default_payment_method"), auto_top_up_enabled: boolean("auto_top_up_enabled").default(false), - auto_top_up_threshold: numeric("auto_top_up_threshold", { precision: 10, scale: 2 }), - auto_top_up_amount: numeric("auto_top_up_amount", { precision: 10, scale: 2 }), + auto_top_up_threshold: numeric("auto_top_up_threshold", { + precision: 10, + scale: 2, + }), + auto_top_up_amount: numeric("auto_top_up_amount", { + precision: 10, + scale: 2, + }), // Steward auth tenant credentials for this organization. // Populated when an org is onboarded onto Steward-backed auth. diff --git a/packages/db/schemas/platform-credentials.ts b/packages/db/schemas/platform-credentials.ts index 74eb21e07..41af644db 100644 --- a/packages/db/schemas/platform-credentials.ts +++ b/packages/db/schemas/platform-credentials.ts @@ -64,13 +64,10 @@ export const platformCredentialTypeEnum = pgEnum("platform_credential_type", [ "microsoft", ]); -export const platformCredentialStatusEnum = pgEnum("platform_credential_status", [ - "pending", - "active", - "expired", - "revoked", - "error", -]); +export const platformCredentialStatusEnum = pgEnum( + "platform_credential_status", + ["pending", "active", "expired", "revoked", "error"], +); // ============================================================================= // PLATFORM CREDENTIALS @@ -116,7 +113,9 @@ export const platformCredentials = pgTable( api_key_secret_id: uuid("api_key_secret_id"), // Permissions granted - granted_permissions: jsonb("granted_permissions").$type().default([]), + granted_permissions: jsonb("granted_permissions") + .$type() + .default([]), // Source context - where did the link come from? source_type: text("source_type"), // "discord" | "telegram" | "web" | "api" @@ -202,9 +201,12 @@ export const platformCredentialSessions = pgTable( status: text("status").notNull().default("pending"), // pending | completed | expired | failed // Result - credential_id: uuid("credential_id").references(() => platformCredentials.id, { - onDelete: "set null", - }), + credential_id: uuid("credential_id").references( + () => platformCredentials.id, + { + onDelete: "set null", + }, + ), error_code: text("error_code"), error_message: text("error_message"), @@ -214,13 +216,21 @@ export const platformCredentialSessions = pgTable( completed_at: timestamp("completed_at"), }, (table) => ({ - session_id_idx: index("platform_credential_sessions_session_idx").on(table.session_id), - org_idx: index("platform_credential_sessions_org_idx").on(table.organization_id), - oauth_state_idx: uniqueIndex("platform_credential_sessions_oauth_state_idx").on( - table.oauth_state, + session_id_idx: index("platform_credential_sessions_session_idx").on( + table.session_id, + ), + org_idx: index("platform_credential_sessions_org_idx").on( + table.organization_id, + ), + oauth_state_idx: uniqueIndex( + "platform_credential_sessions_oauth_state_idx", + ).on(table.oauth_state), + status_idx: index("platform_credential_sessions_status_idx").on( + table.status, + ), + expires_idx: index("platform_credential_sessions_expires_idx").on( + table.expires_at, ), - status_idx: index("platform_credential_sessions_status_idx").on(table.status), - expires_idx: index("platform_credential_sessions_expires_idx").on(table.expires_at), }), ); @@ -231,7 +241,9 @@ export const platformCredentialSessions = pgTable( export type PlatformCredential = typeof platformCredentials.$inferSelect; export type NewPlatformCredential = typeof platformCredentials.$inferInsert; -export type PlatformCredentialSession = typeof platformCredentialSessions.$inferSelect; -export type NewPlatformCredentialSession = typeof platformCredentialSessions.$inferInsert; +export type PlatformCredentialSession = + typeof platformCredentialSessions.$inferSelect; +export type NewPlatformCredentialSession = + typeof platformCredentialSessions.$inferInsert; export type PlatformType = PlatformCredential["platform"]; diff --git a/packages/db/schemas/provider-health.ts b/packages/db/schemas/provider-health.ts index 34b3922f8..7c9978b5d 100644 --- a/packages/db/schemas/provider-health.ts +++ b/packages/db/schemas/provider-health.ts @@ -24,7 +24,10 @@ export const providerHealth = pgTable( last_checked: timestamp("last_checked").notNull().defaultNow(), response_time: integer("response_time"), error_rate: decimal("error_rate", { precision: 5, scale: 4 }).default("0"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), }, diff --git a/packages/db/schemas/redeemable-earnings.ts b/packages/db/schemas/redeemable-earnings.ts index be2031214..32779a30a 100644 --- a/packages/db/schemas/redeemable-earnings.ts +++ b/packages/db/schemas/redeemable-earnings.ts @@ -77,7 +77,9 @@ export const redeemableEarnings = pgTable( // Total earned from ALL sources (apps + agents + mcps + affiliates) // This ONLY increases, never decreases - total_earned: numeric("total_earned", { precision: 18, scale: 4 }).notNull().default("0.0000"), + total_earned: numeric("total_earned", { precision: 18, scale: 4 }) + .notNull() + .default("0.0000"), // Total successfully redeemed // This ONLY increases, never decreases @@ -137,7 +139,9 @@ export const redeemableEarnings = pgTable( last_redemption_at: timestamp("last_redemption_at"), // Version for optimistic locking (prevents race conditions) - version: numeric("version", { precision: 10, scale: 0 }).notNull().default("0"), + version: numeric("version", { precision: 10, scale: 0 }) + .notNull() + .default("0"), created_at: timestamp("created_at").notNull().defaultNow(), updated_at: timestamp("updated_at").notNull().defaultNow(), @@ -239,8 +243,12 @@ export const redeemableEarningsLedger = pgTable( table.user_id, table.created_at, ), - entry_type_idx: index("redeemable_earnings_ledger_type_idx").on(table.entry_type), - redemption_idx: index("redeemable_earnings_ledger_redemption_idx").on(table.redemption_id), + entry_type_idx: index("redeemable_earnings_ledger_type_idx").on( + table.entry_type, + ), + redemption_idx: index("redeemable_earnings_ledger_redemption_idx").on( + table.redemption_id, + ), source_idx: index("redeemable_earnings_ledger_source_idx").on( table.earnings_source, table.source_id, @@ -275,15 +283,27 @@ export const redeemedEarningsTracking = pgTable( }, (table) => ({ // CRITICAL: Unique constraint ensures each earning is redeemed only once - ledger_unique: uniqueIndex("redeemed_tracking_ledger_idx").on(table.ledger_entry_id), - redemption_idx: index("redeemed_tracking_redemption_idx").on(table.redemption_id), + ledger_unique: uniqueIndex("redeemed_tracking_ledger_idx").on( + table.ledger_entry_id, + ), + redemption_idx: index("redeemed_tracking_redemption_idx").on( + table.redemption_id, + ), }), ); // Type exports export type RedeemableEarnings = InferSelectModel; export type NewRedeemableEarnings = InferInsertModel; -export type RedeemableEarningsLedger = InferSelectModel; -export type NewRedeemableEarningsLedger = InferInsertModel; -export type RedeemedEarningsTracking = InferSelectModel; -export type NewRedeemedEarningsTracking = InferInsertModel; +export type RedeemableEarningsLedger = InferSelectModel< + typeof redeemableEarningsLedger +>; +export type NewRedeemableEarningsLedger = InferInsertModel< + typeof redeemableEarningsLedger +>; +export type RedeemedEarningsTracking = InferSelectModel< + typeof redeemedEarningsTracking +>; +export type NewRedeemedEarningsTracking = InferInsertModel< + typeof redeemedEarningsTracking +>; diff --git a/packages/db/schemas/referrals.ts b/packages/db/schemas/referrals.ts index 6e76e5968..84f3ecfc5 100644 --- a/packages/db/schemas/referrals.ts +++ b/packages/db/schemas/referrals.ts @@ -26,7 +26,11 @@ export const socialPlatformEnum = pgEnum("social_platform", [ /** * Share type enum for referral sharing. */ -export const shareTypeEnum = pgEnum("share_type", ["app_share", "character_share", "invite_share"]); +export const shareTypeEnum = pgEnum("share_type", [ + "app_share", + "character_share", + "invite_share", +]); /** * Referral codes table schema. @@ -90,16 +94,24 @@ export const referralSignups = pgTable( .notNull() .references(() => users.id, { onDelete: "cascade" }), // Context of the signup for 50/40/10 revenue split - app_owner_id: uuid("app_owner_id").references(() => users.id, { onDelete: "set null" }), - creator_id: uuid("creator_id").references(() => users.id, { onDelete: "set null" }), - signup_bonus_credited: boolean("signup_bonus_credited").default(false).notNull(), + app_owner_id: uuid("app_owner_id").references(() => users.id, { + onDelete: "set null", + }), + creator_id: uuid("creator_id").references(() => users.id, { + onDelete: "set null", + }), + signup_bonus_credited: boolean("signup_bonus_credited") + .default(false) + .notNull(), signup_bonus_amount: numeric("signup_bonus_amount", { precision: 10, scale: 2, }).default("0.00"), // Qualified referral tracking - referrer gets bonus when referred user links social qualified_at: timestamp("qualified_at"), - qualified_bonus_credited: boolean("qualified_bonus_credited").default(false).notNull(), + qualified_bonus_credited: boolean("qualified_bonus_credited") + .default(false) + .notNull(), qualified_bonus_amount: numeric("qualified_bonus_amount", { precision: 10, scale: 2, @@ -116,7 +128,9 @@ export const referralSignups = pgTable( referred_user_unique: uniqueIndex("referral_signups_referred_user_idx").on( table.referred_user_id, ), - referrer_idx: index("referral_signups_referrer_idx").on(table.referrer_user_id), + referrer_idx: index("referral_signups_referrer_idx").on( + table.referrer_user_id, + ), code_idx: index("referral_signups_code_idx").on(table.referral_code_id), }), ); @@ -150,11 +164,9 @@ export const socialShareRewards = pgTable( (table) => ({ user_idx: index("social_share_rewards_user_idx").on(table.user_id), platform_idx: index("social_share_rewards_platform_idx").on(table.platform), - user_platform_date_idx: index("social_share_rewards_user_platform_date_idx").on( - table.user_id, - table.platform, - table.created_at, - ), + user_platform_date_idx: index( + "social_share_rewards_user_platform_date_idx", + ).on(table.user_id, table.platform, table.created_at), }), ); diff --git a/packages/db/schemas/relations.ts b/packages/db/schemas/relations.ts index d41407d75..c93739b7d 100644 --- a/packages/db/schemas/relations.ts +++ b/packages/db/schemas/relations.ts @@ -27,34 +27,43 @@ import { users } from "./users"; /** * Organizations table relations. */ -export const organizationsRelations = relations(organizations, ({ one, many }) => ({ - users: many(users), - invites: many(organizationInvites), - apps: many(apps), - encryptionKey: one(organizationEncryptionKeys), - billing: one(organizationBilling), - config: one(organizationConfig), -})); +export const organizationsRelations = relations( + organizations, + ({ one, many }) => ({ + users: many(users), + invites: many(organizationInvites), + apps: many(apps), + encryptionKey: one(organizationEncryptionKeys), + billing: one(organizationBilling), + config: one(organizationConfig), + }), +); /** * Organization billing table relations. */ -export const organizationBillingRelations = relations(organizationBilling, ({ one }) => ({ - organization: one(organizations, { - fields: [organizationBilling.organization_id], - references: [organizations.id], +export const organizationBillingRelations = relations( + organizationBilling, + ({ one }) => ({ + organization: one(organizations, { + fields: [organizationBilling.organization_id], + references: [organizations.id], + }), }), -})); +); /** * Organization config table relations. */ -export const organizationConfigRelations = relations(organizationConfig, ({ one }) => ({ - organization: one(organizations, { - fields: [organizationConfig.organization_id], - references: [organizations.id], +export const organizationConfigRelations = relations( + organizationConfig, + ({ one }) => ({ + organization: one(organizations, { + fields: [organizationConfig.organization_id], + references: [organizations.id], + }), }), -})); +); /** * Organization encryption keys table relations. @@ -95,37 +104,46 @@ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({ /** * User preferences table relations. */ -export const userPreferencesRelations = relations(userPreferences, ({ one }) => ({ - user: one(users, { - fields: [userPreferences.user_id], - references: [users.id], +export const userPreferencesRelations = relations( + userPreferences, + ({ one }) => ({ + user: one(users, { + fields: [userPreferences.user_id], + references: [users.id], + }), }), -})); +); /** * Conversations table relations. */ -export const conversationsRelations = relations(conversations, ({ many, one }) => ({ - messages: many(conversationMessages), - user: one(users, { - fields: [conversations.user_id], - references: [users.id], - }), - organization: one(organizations, { - fields: [conversations.organization_id], - references: [organizations.id], +export const conversationsRelations = relations( + conversations, + ({ many, one }) => ({ + messages: many(conversationMessages), + user: one(users, { + fields: [conversations.user_id], + references: [users.id], + }), + organization: one(organizations, { + fields: [conversations.organization_id], + references: [organizations.id], + }), }), -})); +); /** * Conversation messages table relations. */ -export const conversationMessagesRelations = relations(conversationMessages, ({ one }) => ({ - conversation: one(conversations, { - fields: [conversationMessages.conversation_id], - references: [conversations.id], +export const conversationMessagesRelations = relations( + conversationMessages, + ({ one }) => ({ + conversation: one(conversations, { + fields: [conversationMessages.conversation_id], + references: [conversations.id], + }), }), -})); +); /** * User characters table relations. @@ -144,20 +162,23 @@ export const userCharactersRelations = relations(userCharacters, ({ one }) => ({ /** * Organization invites table relations. */ -export const organizationInvitesRelations = relations(organizationInvites, ({ one }) => ({ - organization: one(organizations, { - fields: [organizationInvites.organization_id], - references: [organizations.id], - }), - inviter: one(users, { - fields: [organizationInvites.inviter_user_id], - references: [users.id], - }), - acceptedBy: one(users, { - fields: [organizationInvites.accepted_by_user_id], - references: [users.id], +export const organizationInvitesRelations = relations( + organizationInvites, + ({ one }) => ({ + organization: one(organizations, { + fields: [organizationInvites.organization_id], + references: [organizations.id], + }), + inviter: one(users, { + fields: [organizationInvites.inviter_user_id], + references: [users.id], + }), + acceptedBy: one(users, { + fields: [organizationInvites.accepted_by_user_id], + references: [users.id], + }), }), -})); +); /** * Apps table relations. @@ -241,20 +262,23 @@ export const appAnalyticsRelations = relations(appAnalytics, ({ one }) => ({ /** * App credit balances table relations. */ -export const appCreditBalancesRelations = relations(appCreditBalances, ({ one }) => ({ - app: one(apps, { - fields: [appCreditBalances.app_id], - references: [apps.id], - }), - user: one(users, { - fields: [appCreditBalances.user_id], - references: [users.id], - }), - organization: one(organizations, { - fields: [appCreditBalances.organization_id], - references: [organizations.id], +export const appCreditBalancesRelations = relations( + appCreditBalances, + ({ one }) => ({ + app: one(apps, { + fields: [appCreditBalances.app_id], + references: [apps.id], + }), + user: one(users, { + fields: [appCreditBalances.user_id], + references: [users.id], + }), + organization: one(organizations, { + fields: [appCreditBalances.organization_id], + references: [organizations.id], + }), }), -})); +); /** * App earnings table relations. @@ -269,44 +293,53 @@ export const appEarningsRelations = relations(appEarnings, ({ one }) => ({ /** * App earnings transactions table relations. */ -export const appEarningsTransactionsRelations = relations(appEarningsTransactions, ({ one }) => ({ - app: one(apps, { - fields: [appEarningsTransactions.app_id], - references: [apps.id], - }), - user: one(users, { - fields: [appEarningsTransactions.user_id], - references: [users.id], +export const appEarningsTransactionsRelations = relations( + appEarningsTransactions, + ({ one }) => ({ + app: one(apps, { + fields: [appEarningsTransactions.app_id], + references: [apps.id], + }), + user: one(users, { + fields: [appEarningsTransactions.user_id], + references: [users.id], + }), }), -})); +); /** * Token redemptions table relations. */ -export const tokenRedemptionsRelations = relations(tokenRedemptions, ({ one }) => ({ - user: one(users, { - fields: [tokenRedemptions.user_id], - references: [users.id], - }), - app: one(apps, { - fields: [tokenRedemptions.app_id], - references: [apps.id], - }), - reviewer: one(users, { - fields: [tokenRedemptions.reviewed_by], - references: [users.id], +export const tokenRedemptionsRelations = relations( + tokenRedemptions, + ({ one }) => ({ + user: one(users, { + fields: [tokenRedemptions.user_id], + references: [users.id], + }), + app: one(apps, { + fields: [tokenRedemptions.app_id], + references: [apps.id], + }), + reviewer: one(users, { + fields: [tokenRedemptions.reviewed_by], + references: [users.id], + }), }), -})); +); /** * Redemption limits table relations. */ -export const redemptionLimitsRelations = relations(redemptionLimits, ({ one }) => ({ - user: one(users, { - fields: [redemptionLimits.user_id], - references: [users.id], +export const redemptionLimitsRelations = relations( + redemptionLimits, + ({ one }) => ({ + user: one(users, { + fields: [redemptionLimits.user_id], + references: [users.id], + }), }), -})); +); /** * Crypto payments table relations. diff --git a/packages/db/schemas/remote-sessions.ts b/packages/db/schemas/remote-sessions.ts index 6e4051f87..428d7c21b 100644 --- a/packages/db/schemas/remote-sessions.ts +++ b/packages/db/schemas/remote-sessions.ts @@ -11,7 +11,12 @@ import { miladySandboxes } from "./milady-sandboxes"; import { organizations } from "./organizations"; import { users } from "./users"; -export const REMOTE_SESSION_STATUSES = ["pending", "active", "denied", "revoked"] as const; +export const REMOTE_SESSION_STATUSES = [ + "pending", + "active", + "denied", + "revoked", +] as const; export type RemoteSessionStatus = (typeof REMOTE_SESSION_STATUSES)[number]; @@ -33,13 +38,19 @@ export const remoteSessions = pgTable( pairing_token_hash: text("pairing_token_hash"), ingress_url: text("ingress_url"), ingress_reason: text("ingress_reason"), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), - updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), ended_at: timestamp("ended_at", { withTimezone: true }), }, (table) => ({ agentIdx: index("remote_sessions_agent_id_idx").on(table.agent_id), - orgIdx: index("remote_sessions_organization_id_idx").on(table.organization_id), + orgIdx: index("remote_sessions_organization_id_idx").on( + table.organization_id, + ), statusIdx: index("remote_sessions_status_idx").on(table.status), }), ); diff --git a/packages/db/schemas/retention-cohorts.ts b/packages/db/schemas/retention-cohorts.ts index 4f1e2766b..407f80e4d 100644 --- a/packages/db/schemas/retention-cohorts.ts +++ b/packages/db/schemas/retention-cohorts.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { integer, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { + integer, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; import type { MetricsPlatform } from "./daily-metrics"; /** @@ -22,10 +29,9 @@ export const retentionCohorts = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - cohort_platform_idx: uniqueIndex("retention_cohorts_cohort_platform_idx").on( - table.cohort_date, - table.platform, - ), + cohort_platform_idx: uniqueIndex( + "retention_cohorts_cohort_platform_idx", + ).on(table.cohort_date, table.platform), }), ); diff --git a/packages/db/schemas/secrets.ts b/packages/db/schemas/secrets.ts index d8d3acb09..f7e23f6be 100644 --- a/packages/db/schemas/secrets.ts +++ b/packages/db/schemas/secrets.ts @@ -15,7 +15,11 @@ import { apps } from "./apps"; import { organizations } from "./organizations"; import { users } from "./users"; -export const secretScopeEnum = pgEnum("secret_scope", ["organization", "project", "environment"]); +export const secretScopeEnum = pgEnum("secret_scope", [ + "organization", + "project", + "environment", +]); export const secretEnvironmentEnum = pgEnum("secret_environment", [ "development", @@ -102,7 +106,9 @@ export const secrets = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - org_name_project_env_idx: uniqueIndex("secrets_org_name_project_env_idx").on( + org_name_project_env_idx: uniqueIndex( + "secrets_org_name_project_env_idx", + ).on( table.organization_id, table.name, table.project_id, @@ -146,7 +152,10 @@ export const secretBindings = pgTable( table.project_type, ), org_idx: index("secret_bindings_org_idx").on(table.organization_id), - project_idx: index("secret_bindings_project_idx").on(table.project_id, table.project_type), + project_idx: index("secret_bindings_project_idx").on( + table.project_id, + table.project_type, + ), secret_idx: index("secret_bindings_secret_idx").on(table.secret_id), }), ); @@ -176,7 +185,9 @@ export const appSecretRequirements = pgTable( table.secret_name, ), app_idx: index("app_secret_requirements_app_idx").on(table.app_id), - approved_idx: index("app_secret_requirements_approved_idx").on(table.approved), + approved_idx: index("app_secret_requirements_approved_idx").on( + table.approved, + ), }), ); @@ -223,9 +234,14 @@ export const oauthSessions = pgTable( table.provider, table.user_id, ), - user_provider_idx: index("oauth_sessions_user_provider_idx").on(table.user_id, table.provider), + user_provider_idx: index("oauth_sessions_user_provider_idx").on( + table.user_id, + table.provider, + ), provider_idx: index("oauth_sessions_provider_idx").on(table.provider), - expires_idx: index("oauth_sessions_expires_idx").on(table.access_token_expires_at), + expires_idx: index("oauth_sessions_expires_idx").on( + table.access_token_expires_at, + ), valid_idx: index("oauth_sessions_valid_idx").on(table.is_valid), }), ); @@ -247,7 +263,10 @@ export const secretAuditLog = pgTable( source: text("source"), request_id: text("request_id"), endpoint: text("endpoint"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ @@ -255,8 +274,13 @@ export const secretAuditLog = pgTable( oauth_idx: index("secret_audit_log_oauth_idx").on(table.oauth_session_id), org_idx: index("secret_audit_log_org_idx").on(table.organization_id), action_idx: index("secret_audit_log_action_idx").on(table.action), - actor_idx: index("secret_audit_log_actor_idx").on(table.actor_type, table.actor_id), - created_at_idx: index("secret_audit_log_created_at_idx").on(table.created_at), + actor_idx: index("secret_audit_log_actor_idx").on( + table.actor_type, + table.actor_id, + ), + created_at_idx: index("secret_audit_log_created_at_idx").on( + table.created_at, + ), org_action_time_idx: index("secret_audit_log_org_action_time_idx").on( table.organization_id, table.action, @@ -277,13 +301,27 @@ export type NewSecretAuditLog = InferInsertModel; export type SecretBinding = InferSelectModel; export type NewSecretBinding = InferInsertModel; -export type AppSecretRequirement = InferSelectModel; -export type NewAppSecretRequirement = InferInsertModel; +export type AppSecretRequirement = InferSelectModel< + typeof appSecretRequirements +>; +export type NewAppSecretRequirement = InferInsertModel< + typeof appSecretRequirements +>; export type SecretScope = "organization" | "project" | "environment"; export type SecretEnvironment = "development" | "preview" | "production"; -export type SecretAuditAction = "created" | "read" | "updated" | "deleted" | "rotated"; -export type SecretActorType = "user" | "api_key" | "system" | "deployment" | "workflow"; +export type SecretAuditAction = + | "created" + | "read" + | "updated" + | "deleted" + | "rotated"; +export type SecretActorType = + | "user" + | "api_key" + | "system" + | "deployment" + | "workflow"; export type SecretProvider = | "openai" | "anthropic" @@ -299,4 +337,9 @@ export type SecretProvider = | "aws" | "vercel" | "custom"; -export type SecretProjectType = "character" | "app" | "workflow" | "container" | "mcp"; +export type SecretProjectType = + | "character" + | "app" + | "workflow" + | "container" + | "mcp"; diff --git a/packages/db/schemas/seo.ts b/packages/db/schemas/seo.ts index 1ee9cec22..af5f3098a 100644 --- a/packages/db/schemas/seo.ts +++ b/packages/db/schemas/seo.ts @@ -80,7 +80,9 @@ export const seoRequests = pgTable( keywords: jsonb("keywords").$type().default([]), prompt_context: text("prompt_context"), idempotency_key: text("idempotency_key"), - total_cost: numeric("total_cost", { precision: 10, scale: 4 }).notNull().default("0"), + total_cost: numeric("total_cost", { precision: 10, scale: 4 }) + .notNull() + .default("0"), error: text("error"), completed_at: timestamp("completed_at"), created_at: timestamp("created_at").notNull().defaultNow(), @@ -129,7 +131,8 @@ export const seoProviderCalls = pgTable( external_id: text("external_id"), cost: numeric("cost", { precision: 10, scale: 4 }).notNull().default("0"), request_payload: jsonb("request_payload").$type>(), - response_payload: jsonb("response_payload").$type>(), + response_payload: + jsonb("response_payload").$type>(), error: text("error"), started_at: timestamp("started_at").notNull().defaultNow(), completed_at: timestamp("completed_at"), diff --git a/packages/db/schemas/service-pricing.ts b/packages/db/schemas/service-pricing.ts index f4f6a2bce..0e4881561 100644 --- a/packages/db/schemas/service-pricing.ts +++ b/packages/db/schemas/service-pricing.ts @@ -56,9 +56,12 @@ export const servicePricingAudit = pgTable( "service_pricing_audit", { id: uuid("id").defaultRandom().primaryKey(), - service_pricing_id: uuid("service_pricing_id").references(() => servicePricing.id, { - onDelete: "set null", - }), + service_pricing_id: uuid("service_pricing_id").references( + () => servicePricing.id, + { + onDelete: "set null", + }, + ), service_id: text("service_id").notNull(), method: text("method").notNull(), old_cost: numeric("old_cost", { precision: 12, scale: 6 }), @@ -71,15 +74,18 @@ export const servicePricingAudit = pgTable( created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ - service_idx: index("service_pricing_audit_service_idx").on(table.service_id), - pricing_id_created_idx: index("service_pricing_audit_pricing_created_idx").on( - table.service_pricing_id, - table.created_at, + service_idx: index("service_pricing_audit_service_idx").on( + table.service_id, ), + pricing_id_created_idx: index( + "service_pricing_audit_pricing_created_idx", + ).on(table.service_pricing_id, table.created_at), }), ); export type ServicePricing = InferSelectModel; export type NewServicePricing = InferInsertModel; export type ServicePricingAudit = InferSelectModel; -export type NewServicePricingAudit = InferInsertModel; +export type NewServicePricingAudit = InferInsertModel< + typeof servicePricingAudit +>; diff --git a/packages/db/schemas/telegram-chats.ts b/packages/db/schemas/telegram-chats.ts index a319ed297..61823b1cc 100644 --- a/packages/db/schemas/telegram-chats.ts +++ b/packages/db/schemas/telegram-chats.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { bigint, boolean, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + bigint, + boolean, + index, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -25,8 +33,12 @@ export const telegramChats = pgTable( is_admin: boolean("is_admin").default(false).notNull(), can_post_messages: boolean("can_post_messages").default(false).notNull(), - created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + created_at: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("telegram_chats_organization_id_idx").on(table.organization_id), diff --git a/packages/db/schemas/token-redemptions.ts b/packages/db/schemas/token-redemptions.ts index bbaf98834..77c328db1 100644 --- a/packages/db/schemas/token-redemptions.ts +++ b/packages/db/schemas/token-redemptions.ts @@ -110,7 +110,9 @@ export const tokenRedemptions = pgTable( // Failure details failure_reason: text("failure_reason"), - retry_count: numeric("retry_count", { precision: 3, scale: 0 }).notNull().default("0"), + retry_count: numeric("retry_count", { precision: 3, scale: 0 }) + .notNull() + .default("0"), // Admin review requires_review: boolean("requires_review").notNull().default(false), @@ -150,7 +152,9 @@ export const tokenRedemptions = pgTable( table.created_at, ), network_idx: index("token_redemptions_network_idx").on(table.network), - payout_address_idx: index("token_redemptions_payout_idx").on(table.payout_address), + payout_address_idx: index("token_redemptions_payout_idx").on( + table.payout_address, + ), // Unique constraint to prevent duplicate pending requests pending_user_unique: uniqueIndex("token_redemptions_pending_user_idx") .on(table.user_id, table.status) @@ -181,7 +185,10 @@ export const redemptionLimits = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - user_date_unique: uniqueIndex("redemption_limits_user_date_idx").on(table.user_id, table.date), + user_date_unique: uniqueIndex("redemption_limits_user_date_idx").on( + table.user_id, + table.date, + ), }), ); diff --git a/packages/db/schemas/twilio-inbound-calls.ts b/packages/db/schemas/twilio-inbound-calls.ts index 29dacc9cf..2e6be911f 100644 --- a/packages/db/schemas/twilio-inbound-calls.ts +++ b/packages/db/schemas/twilio-inbound-calls.ts @@ -7,7 +7,14 @@ */ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; export const twilioInboundCalls = pgTable( "twilio_inbound_calls", @@ -20,11 +27,15 @@ export const twilioInboundCalls = pgTable( call_status: text("call_status").notNull(), agent_id: uuid("agent_id"), raw_payload: jsonb("raw_payload").notNull().default({}), - received_at: timestamp("received_at", { withTimezone: true }).notNull().defaultNow(), + received_at: timestamp("received_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ toIdx: index("twilio_inbound_calls_to_idx").on(table.to_number), - receivedIdx: index("twilio_inbound_calls_received_idx").on(table.received_at), + receivedIdx: index("twilio_inbound_calls_received_idx").on( + table.received_at, + ), }), ); diff --git a/packages/db/schemas/usage-quotas.ts b/packages/db/schemas/usage-quotas.ts index abe88846e..38eb9ebb9 100644 --- a/packages/db/schemas/usage-quotas.ts +++ b/packages/db/schemas/usage-quotas.ts @@ -1,5 +1,13 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, numeric, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + numeric, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -27,7 +35,9 @@ export const usageQuotas = pgTable( scale: 2, }).notNull(), - current_usage: numeric("current_usage", { precision: 10, scale: 2 }).default("0.00").notNull(), + current_usage: numeric("current_usage", { precision: 10, scale: 2 }) + .default("0.00") + .notNull(), period_start: timestamp("period_start").notNull(), @@ -42,7 +52,10 @@ export const usageQuotas = pgTable( (table) => ({ org_id_idx: index("usage_quotas_org_id_idx").on(table.organization_id), quota_type_idx: index("usage_quotas_quota_type_idx").on(table.quota_type), - period_idx: index("usage_quotas_period_idx").on(table.period_start, table.period_end), + period_idx: index("usage_quotas_period_idx").on( + table.period_start, + table.period_end, + ), active_idx: index("usage_quotas_active_idx").on(table.is_active), }), ); diff --git a/packages/db/schemas/usage-records.ts b/packages/db/schemas/usage-records.ts index 219fa1e47..54983b1af 100644 --- a/packages/db/schemas/usage-records.ts +++ b/packages/db/schemas/usage-records.ts @@ -38,8 +38,12 @@ export const usageRecords = pgTable( provider: text("provider").notNull(), input_tokens: integer("input_tokens").notNull().default(0), output_tokens: integer("output_tokens").notNull().default(0), - input_cost: numeric("input_cost", { precision: 12, scale: 6 }).default("0.000000"), - output_cost: numeric("output_cost", { precision: 12, scale: 6 }).default("0.000000"), + input_cost: numeric("input_cost", { precision: 12, scale: 6 }).default( + "0.000000", + ), + output_cost: numeric("output_cost", { precision: 12, scale: 6 }).default( + "0.000000", + ), markup: numeric("markup", { precision: 12, scale: 6 }).default("0.000000"), request_id: text("request_id"), duration_ms: integer("duration_ms"), @@ -47,11 +51,16 @@ export const usageRecords = pgTable( error_message: text("error_message"), ip_address: text("ip_address"), user_agent: text("user_agent"), - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("usage_records_organization_idx").on(table.organization_id), + organization_idx: index("usage_records_organization_idx").on( + table.organization_id, + ), user_idx: index("usage_records_user_idx").on(table.user_id), api_key_idx: index("usage_records_api_key_idx").on(table.api_key_id), type_idx: index("usage_records_type_idx").on(table.type), diff --git a/packages/db/schemas/user-characters.ts b/packages/db/schemas/user-characters.ts index 32c9017ac..191add6d4 100644 --- a/packages/db/schemas/user-characters.ts +++ b/packages/db/schemas/user-characters.ts @@ -40,7 +40,9 @@ export const userCharacters = pgTable( username: text("username").unique(), system: text("system"), bio: jsonb("bio").$type().notNull(), - message_examples: jsonb("message_examples").$type[][]>().default([]), + message_examples: jsonb("message_examples") + .$type[][]>() + .default([]), post_examples: jsonb("post_examples").$type().default([]), topics: jsonb("topics").$type().default([]), adjectives: jsonb("adjectives").$type().default([]), @@ -48,8 +50,13 @@ export const userCharacters = pgTable( .$type<(string | { path: string; shared?: boolean })[]>() .default([]), plugins: jsonb("plugins").$type().default([]), - settings: jsonb("settings").$type>().default({}).notNull(), - secrets: jsonb("secrets").$type>().default({}), + settings: jsonb("settings") + .$type>() + .default({}) + .notNull(), + secrets: jsonb("secrets") + .$type>() + .default({}), style: jsonb("style") .$type<{ all?: string[]; @@ -57,7 +64,9 @@ export const userCharacters = pgTable( post?: string[]; }>() .default({}), - character_data: jsonb("character_data").$type>().notNull(), + character_data: jsonb("character_data") + .$type>() + .notNull(), is_template: boolean("is_template").default(false).notNull(), is_public: boolean("is_public").default(false).notNull(), avatar_url: text("avatar_url"), @@ -100,7 +109,9 @@ export const userCharacters = pgTable( // Monetization Settings (similar to apps) // Creators can add markup on top of base inference costs // ========================================================================= - monetization_enabled: boolean("monetization_enabled").default(false).notNull(), + monetization_enabled: boolean("monetization_enabled") + .default(false) + .notNull(), // Percentage markup on inference costs (0-1000%) // e.g., 50% markup means user pays 1.5x base cost, creator gets 0.5x inference_markup_percentage: numeric("inference_markup_percentage", { @@ -113,7 +124,9 @@ export const userCharacters = pgTable( payout_wallet_address: text("payout_wallet_address"), // Earnings tracking - total_inference_requests: integer("total_inference_requests").default(0).notNull(), + total_inference_requests: integer("total_inference_requests") + .default(0) + .notNull(), total_creator_earnings: numeric("total_creator_earnings", { precision: 12, scale: 4, @@ -140,27 +153,39 @@ export const userCharacters = pgTable( updated_at: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ - organization_idx: index("user_characters_organization_idx").on(table.organization_id), + organization_idx: index("user_characters_organization_idx").on( + table.organization_id, + ), user_idx: index("user_characters_user_idx").on(table.user_id), name_idx: index("user_characters_name_idx").on(table.name), username_idx: index("user_characters_username_idx").on(table.username), category_idx: index("user_characters_category_idx").on(table.category), featured_idx: index("user_characters_featured_idx").on(table.featured), - template_idx: index("user_characters_is_template_idx").on(table.is_template), + template_idx: index("user_characters_is_template_idx").on( + table.is_template, + ), public_idx: index("user_characters_is_public_idx").on(table.is_public), - popularity_idx: index("user_characters_popularity_idx").on(table.popularity_score), + popularity_idx: index("user_characters_popularity_idx").on( + table.popularity_score, + ), source_idx: index("user_characters_source_idx").on(table.source), // New indexes for ERC-8004 and monetization - erc8004_idx: index("user_characters_erc8004_idx").on(table.erc8004_registered), + erc8004_idx: index("user_characters_erc8004_idx").on( + table.erc8004_registered, + ), erc8004_agent_idx: index("user_characters_erc8004_agent_idx").on( table.erc8004_network, table.erc8004_agent_id, ), - monetization_idx: index("user_characters_monetization_idx").on(table.monetization_enabled), + monetization_idx: index("user_characters_monetization_idx").on( + table.monetization_enabled, + ), // Keep this plain Drizzle index in sync with db/migrations/0047_add_token_agent_linkage.sql. // The partial unique (token_address, token_chain) index is hand-managed in SQL because // Drizzle schema generation cannot represent the WHERE token_address IS NOT NULL predicate. - token_address_idx: index("user_characters_token_address_idx").on(table.token_address), + token_address_idx: index("user_characters_token_address_idx").on( + table.token_address, + ), }), ); diff --git a/packages/db/schemas/user-identities.ts b/packages/db/schemas/user-identities.ts index ff849e679..9a3347cd5 100644 --- a/packages/db/schemas/user-identities.ts +++ b/packages/db/schemas/user-identities.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { users } from "./users"; /** @@ -53,17 +60,33 @@ export const userIdentities = pgTable( }, (table) => ({ user_idx: index("user_identities_user_idx").on(table.user_id), - steward_user_id_idx: index("user_identities_steward_user_id_idx").on(table.steward_user_id), - privy_user_id_idx: index("user_identities_privy_user_id_idx").on(table.privy_user_id), - is_anonymous_idx: index("user_identities_is_anonymous_idx").on(table.is_anonymous), + steward_user_id_idx: index("user_identities_steward_user_id_idx").on( + table.steward_user_id, + ), + privy_user_id_idx: index("user_identities_privy_user_id_idx").on( + table.privy_user_id, + ), + is_anonymous_idx: index("user_identities_is_anonymous_idx").on( + table.is_anonymous, + ), anonymous_session_idx: index("user_identities_anonymous_session_idx").on( table.anonymous_session_id, ), - expires_at_idx: index("user_identities_expires_at_idx").on(table.expires_at), - telegram_id_idx: index("user_identities_telegram_id_idx").on(table.telegram_id), - phone_number_idx: index("user_identities_phone_number_idx").on(table.phone_number), - discord_id_idx: index("user_identities_discord_id_idx").on(table.discord_id), - whatsapp_id_idx: index("user_identities_whatsapp_id_idx").on(table.whatsapp_id), + expires_at_idx: index("user_identities_expires_at_idx").on( + table.expires_at, + ), + telegram_id_idx: index("user_identities_telegram_id_idx").on( + table.telegram_id, + ), + phone_number_idx: index("user_identities_phone_number_idx").on( + table.phone_number, + ), + discord_id_idx: index("user_identities_discord_id_idx").on( + table.discord_id, + ), + whatsapp_id_idx: index("user_identities_whatsapp_id_idx").on( + table.whatsapp_id, + ), }), ); diff --git a/packages/db/schemas/user-mcps.ts b/packages/db/schemas/user-mcps.ts index 4317ef3e6..924239080 100644 --- a/packages/db/schemas/user-mcps.ts +++ b/packages/db/schemas/user-mcps.ts @@ -19,7 +19,11 @@ import { users } from "./users"; /** * MCP pricing type enum */ -export const mcpPricingTypeEnum = pgEnum("mcp_pricing_type", ["free", "credits", "x402"]); +export const mcpPricingTypeEnum = pgEnum("mcp_pricing_type", [ + "free", + "credits", + "x402", +]); /** * MCP status enum @@ -89,7 +93,9 @@ export const userMcps = pgTable( color: text("color").default("#6366F1"), // Pricing configuration - pricing_type: mcpPricingTypeEnum("pricing_type").notNull().default("credits"), + pricing_type: mcpPricingTypeEnum("pricing_type") + .notNull() + .default("credits"), credits_per_request: numeric("credits_per_request", { precision: 10, scale: 4, @@ -142,7 +148,10 @@ export const userMcps = pgTable( support_email: text("support_email"), // Metadata - metadata: jsonb("metadata").$type>().default({}).notNull(), + metadata: jsonb("metadata") + .$type>() + .default({}) + .notNull(), // ========================================================================= // ERC-8004 On-Chain Registration @@ -163,15 +172,24 @@ export const userMcps = pgTable( published_at: timestamp("published_at"), }, (table) => ({ - slug_org_idx: uniqueIndex("user_mcps_slug_org_idx").on(table.slug, table.organization_id), - organization_idx: index("user_mcps_organization_idx").on(table.organization_id), - created_by_idx: index("user_mcps_created_by_idx").on(table.created_by_user_id), + slug_org_idx: uniqueIndex("user_mcps_slug_org_idx").on( + table.slug, + table.organization_id, + ), + organization_idx: index("user_mcps_organization_idx").on( + table.organization_id, + ), + created_by_idx: index("user_mcps_created_by_idx").on( + table.created_by_user_id, + ), container_idx: index("user_mcps_container_idx").on(table.container_id), category_idx: index("user_mcps_category_idx").on(table.category), status_idx: index("user_mcps_status_idx").on(table.status), is_public_idx: index("user_mcps_is_public_idx").on(table.is_public), created_at_idx: index("user_mcps_created_at_idx").on(table.created_at), - erc8004_registered_idx: index("user_mcps_erc8004_registered_idx").on(table.erc8004_registered), + erc8004_registered_idx: index("user_mcps_erc8004_registered_idx").on( + table.erc8004_registered, + ), }), ); @@ -237,10 +255,15 @@ export const mcpUsage = pgTable( }, (table) => ({ mcp_id_idx: index("mcp_usage_mcp_id_idx").on(table.mcp_id), - organization_idx: index("mcp_usage_organization_idx").on(table.organization_id), + organization_idx: index("mcp_usage_organization_idx").on( + table.organization_id, + ), user_idx: index("mcp_usage_user_idx").on(table.user_id), created_at_idx: index("mcp_usage_created_at_idx").on(table.created_at), - mcp_org_idx: index("mcp_usage_mcp_org_idx").on(table.mcp_id, table.organization_id), + mcp_org_idx: index("mcp_usage_mcp_org_idx").on( + table.mcp_id, + table.organization_id, + ), }), ); diff --git a/packages/db/schemas/user-preferences.ts b/packages/db/schemas/user-preferences.ts index 3149283e2..31cd0db2d 100644 --- a/packages/db/schemas/user-preferences.ts +++ b/packages/db/schemas/user-preferences.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { users } from "./users"; /** @@ -29,7 +36,9 @@ export const userPreferences = pgTable( }, (table) => ({ user_idx: index("user_preferences_user_idx").on(table.user_id), - work_function_idx: index("user_preferences_work_function_idx").on(table.work_function), + work_function_idx: index("user_preferences_work_function_idx").on( + table.work_function, + ), }), ); diff --git a/packages/db/schemas/user-sessions.ts b/packages/db/schemas/user-sessions.ts index dcd8db596..60a8aa353 100644 --- a/packages/db/schemas/user-sessions.ts +++ b/packages/db/schemas/user-sessions.ts @@ -33,11 +33,15 @@ export const userSessions = pgTable( session_token: text("session_token").notNull().unique(), - credits_used: numeric("credits_used", { precision: 10, scale: 2 }).default("0.00").notNull(), + credits_used: numeric("credits_used", { precision: 10, scale: 2 }) + .default("0.00") + .notNull(), requests_made: integer("requests_made").default(0).notNull(), - tokens_consumed: bigint("tokens_consumed", { mode: "number" }).default(0).notNull(), + tokens_consumed: bigint("tokens_consumed", { mode: "number" }) + .default(0) + .notNull(), started_at: timestamp("started_at").notNull().defaultNow(), @@ -49,7 +53,10 @@ export const userSessions = pgTable( user_agent: text("user_agent"), - device_info: jsonb("device_info").$type>().default({}).notNull(), + device_info: jsonb("device_info") + .$type>() + .default({}) + .notNull(), created_at: timestamp("created_at").notNull().defaultNow(), diff --git a/packages/db/schemas/user-voices.ts b/packages/db/schemas/user-voices.ts index 8cf755cba..bfd442bdd 100644 --- a/packages/db/schemas/user-voices.ts +++ b/packages/db/schemas/user-voices.ts @@ -73,11 +73,16 @@ export const userVoices = pgTable( }, (table) => ({ // Index for organization queries (listing user voices) - organization_idx: index("user_voices_organization_idx").on(table.organizationId), + organization_idx: index("user_voices_organization_idx").on( + table.organizationId, + ), // Index for user queries user_idx: index("user_voices_user_idx").on(table.userId), // Index for finding voices by organization and clone type (slot counting) - org_type_idx: index("user_voices_org_type_idx").on(table.organizationId, table.cloneType), + org_type_idx: index("user_voices_org_type_idx").on( + table.organizationId, + table.cloneType, + ), // Index for organization + usage analytics (most used voices) org_usage_idx: index("user_voices_org_usage_idx").on( table.organizationId, diff --git a/packages/db/schemas/users.ts b/packages/db/schemas/users.ts index 0d20fb26b..5f27eed4f 100644 --- a/packages/db/schemas/users.ts +++ b/packages/db/schemas/users.ts @@ -1,5 +1,12 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -import { boolean, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + index, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { organizations } from "./organizations"; /** @@ -29,9 +36,12 @@ export const users = pgTable( avatar: text("avatar"), // Organization - organization_id: uuid("organization_id").references(() => organizations.id, { - onDelete: "cascade", - }), + organization_id: uuid("organization_id").references( + () => organizations.id, + { + onDelete: "cascade", + }, + ), role: text("role").notNull().default("member"), // External identities (kept here for auth system compatibility) @@ -70,8 +80,12 @@ export const users = pgTable( }, (table) => ({ email_idx: index("users_email_idx").on(table.email), - wallet_address_idx: index("users_wallet_address_idx").on(table.wallet_address), - wallet_chain_type_idx: index("users_wallet_chain_type_idx").on(table.wallet_chain_type), + wallet_address_idx: index("users_wallet_address_idx").on( + table.wallet_address, + ), + wallet_chain_type_idx: index("users_wallet_chain_type_idx").on( + table.wallet_chain_type, + ), organization_idx: index("users_organization_idx").on(table.organization_id), is_active_idx: index("users_is_active_idx").on(table.is_active), steward_idx: index("users_steward_idx").on(table.steward_user_id), diff --git a/packages/db/schemas/vertex-model-assignments.ts b/packages/db/schemas/vertex-model-assignments.ts index a597c348c..6bc1fcbc5 100644 --- a/packages/db/schemas/vertex-model-assignments.ts +++ b/packages/db/schemas/vertex-model-assignments.ts @@ -13,7 +13,10 @@ import { import { organizations } from "./organizations"; import { users } from "./users"; import { vertexTunedModels } from "./vertex-tuned-models"; -import { vertexTuningScopeEnum, vertexTuningSlotEnum } from "./vertex-tuning-jobs"; +import { + vertexTuningScopeEnum, + vertexTuningSlotEnum, +} from "./vertex-tuning-jobs"; export const vertexModelAssignments = pgTable( "vertex_model_assignments", @@ -21,39 +24,68 @@ export const vertexModelAssignments = pgTable( id: uuid("id").defaultRandom().primaryKey(), scope: vertexTuningScopeEnum("scope").notNull(), slot: vertexTuningSlotEnum("slot").notNull(), - organization_id: uuid("organization_id").references(() => organizations.id, { - onDelete: "cascade", - }), + organization_id: uuid("organization_id").references( + () => organizations.id, + { + onDelete: "cascade", + }, + ), user_id: uuid("user_id").references(() => users.id, { onDelete: "cascade", }), tuned_model_id: uuid("tuned_model_id") .notNull() .references(() => vertexTunedModels.id, { onDelete: "cascade" }), - assigned_by_user_id: uuid("assigned_by_user_id").references(() => users.id, { - onDelete: "set null", - }), + assigned_by_user_id: uuid("assigned_by_user_id").references( + () => users.id, + { + onDelete: "set null", + }, + ), is_active: boolean("is_active").notNull().default(true), - metadata: jsonb("metadata").$type>().notNull().default({}), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), - updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), - activated_at: timestamp("activated_at", { withTimezone: true }).notNull().defaultNow(), + metadata: jsonb("metadata") + .$type>() + .notNull() + .default({}), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + activated_at: timestamp("activated_at", { withTimezone: true }) + .notNull() + .defaultNow(), deactivated_at: timestamp("deactivated_at", { withTimezone: true }), }, (table) => ({ - tuned_model_idx: index("vertex_model_assignments_tuned_model_idx").on(table.tuned_model_id), + tuned_model_idx: index("vertex_model_assignments_tuned_model_idx").on( + table.tuned_model_id, + ), scope_idx: index("vertex_model_assignments_scope_idx").on(table.scope), slot_idx: index("vertex_model_assignments_slot_idx").on(table.slot), - organization_idx: index("vertex_model_assignments_organization_idx").on(table.organization_id), + organization_idx: index("vertex_model_assignments_organization_idx").on( + table.organization_id, + ), user_idx: index("vertex_model_assignments_user_idx").on(table.user_id), - active_idx: index("vertex_model_assignments_active_idx").on(table.is_active), - global_slot_unique: uniqueIndex("vertex_model_assignments_global_slot_active_idx") + active_idx: index("vertex_model_assignments_active_idx").on( + table.is_active, + ), + global_slot_unique: uniqueIndex( + "vertex_model_assignments_global_slot_active_idx", + ) .on(table.slot) .where(sql`${table.scope} = 'global' and ${table.is_active} = true`), - organization_slot_unique: uniqueIndex("vertex_model_assignments_org_slot_active_idx") + organization_slot_unique: uniqueIndex( + "vertex_model_assignments_org_slot_active_idx", + ) .on(table.organization_id, table.slot) - .where(sql`${table.scope} = 'organization' and ${table.is_active} = true`), - user_slot_unique: uniqueIndex("vertex_model_assignments_user_slot_active_idx") + .where( + sql`${table.scope} = 'organization' and ${table.is_active} = true`, + ), + user_slot_unique: uniqueIndex( + "vertex_model_assignments_user_slot_active_idx", + ) .on(table.user_id, table.slot) .where(sql`${table.scope} = 'user' and ${table.is_active} = true`), scope_owner_check: check( @@ -67,5 +99,9 @@ export const vertexModelAssignments = pgTable( }), ); -export type VertexModelAssignmentRecord = InferSelectModel; -export type NewVertexModelAssignmentRecord = InferInsertModel; +export type VertexModelAssignmentRecord = InferSelectModel< + typeof vertexModelAssignments +>; +export type NewVertexModelAssignmentRecord = InferInsertModel< + typeof vertexModelAssignments +>; diff --git a/packages/db/schemas/vertex-tuned-models.ts b/packages/db/schemas/vertex-tuned-models.ts index cfe3f47db..bc3da80a4 100644 --- a/packages/db/schemas/vertex-tuned-models.ts +++ b/packages/db/schemas/vertex-tuned-models.ts @@ -32,9 +32,12 @@ export const vertexTunedModels = pgTable( region: text("region").notNull(), slot: vertexTuningSlotEnum("slot").notNull(), source_scope: vertexTuningScopeEnum("source_scope").notNull(), - organization_id: uuid("organization_id").references(() => organizations.id, { - onDelete: "cascade", - }), + organization_id: uuid("organization_id").references( + () => organizations.id, + { + onDelete: "cascade", + }, + ), user_id: uuid("user_id").references(() => users.id, { onDelete: "cascade", }), @@ -42,18 +45,31 @@ export const vertexTunedModels = pgTable( .$type>() .notNull() .default({}), - metadata: jsonb("metadata").$type>().notNull().default({}), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), - updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + metadata: jsonb("metadata") + .$type>() + .notNull() + .default({}), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), }, (table) => ({ - vertex_model_id_idx: uniqueIndex("vertex_tuned_models_vertex_model_id_idx").on( - table.vertex_model_id, + vertex_model_id_idx: uniqueIndex( + "vertex_tuned_models_vertex_model_id_idx", + ).on(table.vertex_model_id), + tuning_job_idx: index("vertex_tuned_models_tuning_job_idx").on( + table.tuning_job_id, ), - tuning_job_idx: index("vertex_tuned_models_tuning_job_idx").on(table.tuning_job_id), slot_idx: index("vertex_tuned_models_slot_idx").on(table.slot), - source_scope_idx: index("vertex_tuned_models_source_scope_idx").on(table.source_scope), - organization_idx: index("vertex_tuned_models_organization_idx").on(table.organization_id), + source_scope_idx: index("vertex_tuned_models_source_scope_idx").on( + table.source_scope, + ), + organization_idx: index("vertex_tuned_models_organization_idx").on( + table.organization_id, + ), user_idx: index("vertex_tuned_models_user_idx").on(table.user_id), scope_owner_check: check( "vertex_tuned_models_scope_owner_check", @@ -67,4 +83,6 @@ export const vertexTunedModels = pgTable( ); export type VertexTunedModelRecord = InferSelectModel; -export type NewVertexTunedModelRecord = InferInsertModel; +export type NewVertexTunedModelRecord = InferInsertModel< + typeof vertexTunedModels +>; diff --git a/packages/db/schemas/vertex-tuning-jobs.ts b/packages/db/schemas/vertex-tuning-jobs.ts index c639df1c3..2c06c98c1 100644 --- a/packages/db/schemas/vertex-tuning-jobs.ts +++ b/packages/db/schemas/vertex-tuning-jobs.ts @@ -49,9 +49,12 @@ export const vertexTuningJobs = pgTable( base_model: text("base_model").notNull(), slot: vertexTuningSlotEnum("slot").notNull(), scope: vertexTuningScopeEnum("scope").notNull(), - organization_id: uuid("organization_id").references(() => organizations.id, { - onDelete: "cascade", - }), + organization_id: uuid("organization_id").references( + () => organizations.id, + { + onDelete: "cascade", + }, + ), user_id: uuid("user_id").references(() => users.id, { onDelete: "cascade", }), @@ -65,7 +68,9 @@ export const vertexTuningJobs = pgTable( recommended_model_id: text("recommended_model_id"), tuned_model_display_name: text("tuned_model_display_name"), tuned_model_endpoint_name: text("tuned_model_endpoint_name"), - status: vertexTuningJobStateEnum("status").notNull().default("JOB_STATE_PENDING"), + status: vertexTuningJobStateEnum("status") + .notNull() + .default("JOB_STATE_PENDING"), error_code: integer("error_code"), error_message: text("error_message"), model_preference_patch: jsonb("model_preference_patch") @@ -76,22 +81,35 @@ export const vertexTuningJobs = pgTable( .$type>() .notNull() .default({}), - metadata: jsonb("metadata").$type>().notNull().default({}), - created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), - updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + metadata: jsonb("metadata") + .$type>() + .notNull() + .default({}), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), completed_at: timestamp("completed_at", { withTimezone: true }), }, (table) => ({ - vertex_job_name_idx: uniqueIndex("vertex_tuning_jobs_vertex_job_name_idx").on( - table.vertex_job_name, - ), + vertex_job_name_idx: uniqueIndex( + "vertex_tuning_jobs_vertex_job_name_idx", + ).on(table.vertex_job_name), status_idx: index("vertex_tuning_jobs_status_idx").on(table.status), scope_idx: index("vertex_tuning_jobs_scope_idx").on(table.scope), - organization_idx: index("vertex_tuning_jobs_organization_idx").on(table.organization_id), + organization_idx: index("vertex_tuning_jobs_organization_idx").on( + table.organization_id, + ), user_idx: index("vertex_tuning_jobs_user_idx").on(table.user_id), - created_by_idx: index("vertex_tuning_jobs_created_by_idx").on(table.created_by_user_id), + created_by_idx: index("vertex_tuning_jobs_created_by_idx").on( + table.created_by_user_id, + ), slot_idx: index("vertex_tuning_jobs_slot_idx").on(table.slot), - created_at_idx: index("vertex_tuning_jobs_created_at_idx").on(table.created_at), + created_at_idx: index("vertex_tuning_jobs_created_at_idx").on( + table.created_at, + ), scope_owner_check: check( "vertex_tuning_jobs_scope_owner_check", sql`( @@ -104,4 +122,6 @@ export const vertexTuningJobs = pgTable( ); export type VertexTuningJobRecord = InferSelectModel; -export type NewVertexTuningJobRecord = InferInsertModel; +export type NewVertexTuningJobRecord = InferInsertModel< + typeof vertexTuningJobs +>; diff --git a/packages/db/schemas/webhook-events.ts b/packages/db/schemas/webhook-events.ts index 9486896f8..f104e9434 100644 --- a/packages/db/schemas/webhook-events.ts +++ b/packages/db/schemas/webhook-events.ts @@ -30,7 +30,9 @@ export const webhookEvents = pgTable( (table) => ({ event_id_idx: index("webhook_events_event_id_idx").on(table.event_id), provider_idx: index("webhook_events_provider_idx").on(table.provider), - processed_at_idx: index("webhook_events_processed_at_idx").on(table.processed_at), + processed_at_idx: index("webhook_events_processed_at_idx").on( + table.processed_at, + ), // Composite index for cleanup queries provider_processed_idx: index("webhook_events_provider_processed_idx").on( table.provider, diff --git a/packages/lib/actions/analytics-enhanced.ts b/packages/lib/actions/analytics-enhanced.ts index 426f30cf4..ad9b2fbed 100644 --- a/packages/lib/actions/analytics-enhanced.ts +++ b/packages/lib/actions/analytics-enhanced.ts @@ -4,7 +4,10 @@ "use server"; -import { generateProjectionAlerts, generateProjections } from "@/lib/analytics/projections"; +import { + generateProjectionAlerts, + generateProjections, +} from "@/lib/analytics/projections"; import { requireAuthWithOrg } from "@/lib/auth"; import { getCostTrending, @@ -38,7 +41,9 @@ export interface EnhancedAnalyticsFilters { * @param filters - Optional filters including time range presets. * @returns Enhanced analytics data with breakdowns and trends. */ -export async function getEnhancedAnalyticsData(filters: EnhancedAnalyticsFilters = {}) { +export async function getEnhancedAnalyticsData( + filters: EnhancedAnalyticsFilters = {}, +) { const user = await requireAuthWithOrg(); const organizationId = user.organization_id!; @@ -141,7 +146,11 @@ export async function getProjectionsData(periods: number = 7) { const creditBalance = Number(org?.credit_balance || 0); const projections = generateProjections(historicalData, periods); - const alerts = generateProjectionAlerts(historicalData, projections, creditBalance); + const alerts = generateProjectionAlerts( + historicalData, + projections, + creditBalance, + ); return { historicalData, @@ -151,5 +160,7 @@ export async function getProjectionsData(periods: number = 7) { }; } -export type EnhancedAnalyticsData = Awaited>; +export type EnhancedAnalyticsData = Awaited< + ReturnType +>; export type ProjectionsData = Awaited>; diff --git a/packages/lib/actions/analytics.ts b/packages/lib/actions/analytics.ts index 30b68df11..d0bc92ee4 100644 --- a/packages/lib/actions/analytics.ts +++ b/packages/lib/actions/analytics.ts @@ -40,12 +40,13 @@ export async function getAnalyticsData(filters: AnalyticsFilters = {}) { granularity = "day" as TimeGranularity, } = filters; - const [overallStats, timeSeriesData, userBreakdown, costTrending] = await Promise.all([ - getUsageStatsSafe(organizationId, { startDate, endDate }), - getUsageTimeSeries(organizationId, { startDate, endDate, granularity }), - getUsageByUser(organizationId, { startDate, endDate, limit: 10 }), - getCostTrending(organizationId), - ]); + const [overallStats, timeSeriesData, userBreakdown, costTrending] = + await Promise.all([ + getUsageStatsSafe(organizationId, { startDate, endDate }), + getUsageTimeSeries(organizationId, { startDate, endDate, granularity }), + getUsageByUser(organizationId, { startDate, endDate, limit: 10 }), + getCostTrending(organizationId), + ]); return { filters: { diff --git a/packages/lib/actions/dashboard.ts b/packages/lib/actions/dashboard.ts index 39e59f3ec..4648ed726 100644 --- a/packages/lib/actions/dashboard.ts +++ b/packages/lib/actions/dashboard.ts @@ -110,20 +110,23 @@ async function fetchDashboardDataInternal( const organizationId = user.organization_id!; // Fetch only the data rendered on the dashboard home. - const [generationStats, userCharacters, apiKeys, userRooms, apps, org] = await Promise.all([ - generationsService.getStats(organizationId), - charactersService.listByUser(user.id), - apiKeysService.listByOrganization(organizationId), - roomsService.getRoomsForEntity(user.id), - appsService.listByOrganization(organizationId), - organizationsRepository.findById(organizationId), - ]); + const [generationStats, userCharacters, apiKeys, userRooms, apps, org] = + await Promise.all([ + generationsService.getStats(organizationId), + charactersService.listByUser(user.id), + apiKeysService.listByOrganization(organizationId), + roomsService.getRoomsForEntity(user.id), + appsService.listByOrganization(organizationId), + organizationsRepository.findById(organizationId), + ]); const chatRoomCount = userRooms.length; const totalGenerations = generationStats.totalGenerations; - const imageGenerations = generationStats.byType.find((t) => t.type === "image")?.count || 0; - const videoGenerations = generationStats.byType.find((t) => t.type === "video")?.count || 0; + const imageGenerations = + generationStats.byType.find((t) => t.type === "image")?.count || 0; + const videoGenerations = + generationStats.byType.find((t) => t.type === "video")?.count || 0; // Get actual 24h API call count from usage records const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); @@ -141,7 +144,9 @@ async function fetchDashboardDataInternal( if (characterIds.length > 0) { try { const statsMap = - await characterDeploymentDiscoveryService.getCharacterStatisticsBatch(characterIds); + await characterDeploymentDiscoveryService.getCharacterStatisticsBatch( + characterIds, + ); statsMap.forEach((stats, id) => { // Note: Both `status` (from AgentStats) and `deploymentStatus` (added by DashboardAgentStats) // are required - they have the same value but satisfy different type requirements @@ -217,8 +222,10 @@ export const getDashboardData = cache(async (): Promise => { const cacheKey = CacheKeys.org.dashboard(organizationId); // Use stale-while-revalidate pattern - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.org.dashboard, () => - fetchDashboardDataInternal(user), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.org.dashboard, + () => fetchDashboardDataInternal(user), ); // Fallback to direct fetch if cache returns null diff --git a/packages/lib/analytics/posthog-server.ts b/packages/lib/analytics/posthog-server.ts index 0287233ff..1526d5b56 100644 --- a/packages/lib/analytics/posthog-server.ts +++ b/packages/lib/analytics/posthog-server.ts @@ -7,7 +7,11 @@ import { PostHog } from "posthog-node"; import { logger } from "@/lib/utils/logger"; -import type { AdHocEventProperties, EventProperties, PostHogEvent } from "./posthog"; +import type { + AdHocEventProperties, + EventProperties, + PostHogEvent, +} from "./posthog"; let posthogClient: PostHog | null = null; @@ -84,7 +88,10 @@ export interface ServerUserProperties { [key: string]: string | number | boolean | undefined; } -export function identifyServerUser(distinctId: string, properties: ServerUserProperties): void { +export function identifyServerUser( + distinctId: string, + properties: ServerUserProperties, +): void { const client = getPostHogClient(); if (!client) return; diff --git a/packages/lib/analytics/posthog.ts b/packages/lib/analytics/posthog.ts index b70ce2db3..c3388b94c 100644 --- a/packages/lib/analytics/posthog.ts +++ b/packages/lib/analytics/posthog.ts @@ -218,7 +218,11 @@ export interface CreditsPurchasedProps { // Payment Method Types export type PaymentMethod = "stripe" | "crypto"; -export type PurchaseType = "credit_pack" | "custom_amount" | "auto_top_up" | "app_credits"; +export type PurchaseType = + | "credit_pack" + | "custom_amount" + | "auto_top_up" + | "app_credits"; export type PaymentSourcePage = "billing" | "settings" | "app"; // Payment Method Selection @@ -458,7 +462,8 @@ export function initPostHog(): void { if (!isBrowser()) return; const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; - const apiHost = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com"; + const apiHost = + process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com"; // Initialize if key is set (allows staging/preview to opt-in) if (!apiKey) { @@ -477,12 +482,18 @@ export function initPostHog(): void { }); } -export function trackEvent(event: PostHogEvent, properties?: EventProperties): void { +export function trackEvent( + event: PostHogEvent, + properties?: EventProperties, +): void { if (!isBrowser()) return; posthog.capture(event, properties); } -export function trackCustomEvent(event: string, properties?: AdHocEventProperties): void { +export function trackCustomEvent( + event: string, + properties?: AdHocEventProperties, +): void { if (!isBrowser()) return; posthog.capture(event, properties); } @@ -492,7 +503,9 @@ export function trackCustomEvent(event: string, properties?: AdHocEventPropertie * Removes stack traces, truncates to 200 chars, and takes only the first line. * This prevents leaking sensitive information like file paths or internal details. */ -export function sanitizeErrorMessage(message: string | undefined): string | undefined { +export function sanitizeErrorMessage( + message: string | undefined, +): string | undefined { if (!message) return undefined; // Take first line only (removes stack traces), truncate to 200 chars return message.split("\n")[0].substring(0, 200); @@ -508,7 +521,10 @@ export interface UserProperties { created_at?: string; } -export function identifyUser(userId: string, properties?: UserProperties): void { +export function identifyUser( + userId: string, + properties?: UserProperties, +): void { if (!isBrowser()) return; posthog.identify(userId, properties); } diff --git a/packages/lib/analytics/projections.ts b/packages/lib/analytics/projections.ts index 9ab6b8a3f..3dd561721 100644 --- a/packages/lib/analytics/projections.ts +++ b/packages/lib/analytics/projections.ts @@ -41,7 +41,11 @@ export const PROJECTION_CONSTANTS = { * @param variance - Variance factor to apply * @returns Value between (1 - variance/2) and (1 + variance/2) */ -function seededRandom(timestamp: Date, index: number, variance: number): number { +function seededRandom( + timestamp: Date, + index: number, + variance: number, +): number { const seed = timestamp.getTime() + index; // Simple Linear Congruential Generator (LCG) const a = 1664525; @@ -90,7 +94,9 @@ export interface ProjectionAlert { * @param values - Array of numeric values * @returns Slope and intercept for the linear regression line */ -export function calculateLinearRegression(values: number[]): LinearRegressionResult { +export function calculateLinearRegression( + values: number[], +): LinearRegressionResult { const n = values.length; // Handle edge case: insufficient data @@ -126,13 +132,21 @@ export function generateProjections( return historicalData.map((d) => ({ ...d, isProjected: false })); } - const requestsRegression = calculateLinearRegression(historicalData.map((d) => d.totalRequests)); - const costRegression = calculateLinearRegression(historicalData.map((d) => d.totalCost)); - const inputTokensRegression = calculateLinearRegression(historicalData.map((d) => d.inputTokens)); + const requestsRegression = calculateLinearRegression( + historicalData.map((d) => d.totalRequests), + ); + const costRegression = calculateLinearRegression( + historicalData.map((d) => d.totalCost), + ); + const inputTokensRegression = calculateLinearRegression( + historicalData.map((d) => d.inputTokens), + ); const outputTokensRegression = calculateLinearRegression( historicalData.map((d) => d.outputTokens), ); - const successRateRegression = calculateLinearRegression(historicalData.map((d) => d.successRate)); + const successRateRegression = calculateLinearRegression( + historicalData.map((d) => d.successRate), + ); const combined: ProjectionDataPoint[] = historicalData.map((d) => ({ ...d, @@ -160,29 +174,39 @@ export function generateProjections( ); const projectedInputTokens = Math.max( 0, - inputTokensRegression.intercept + inputTokensRegression.slope * futureIndex, + inputTokensRegression.intercept + + inputTokensRegression.slope * futureIndex, ); const projectedOutputTokens = Math.max( 0, - outputTokensRegression.intercept + outputTokensRegression.slope * futureIndex, + outputTokensRegression.intercept + + outputTokensRegression.slope * futureIndex, ); const projectedSuccessRate = Math.min( 1.0, - Math.max(0, successRateRegression.intercept + successRateRegression.slope * futureIndex), + Math.max( + 0, + successRateRegression.intercept + + successRateRegression.slope * futureIndex, + ), ); const futureDate = new Date(lastDate.getTime() + avgTimeDiff * i); const variance = PROJECTION_CONSTANTS.VARIANCE_FACTOR; - const requestsVariance = projectedRequests * seededRandom(futureDate, i, variance); - const costVariance = projectedCost * seededRandom(futureDate, i + 1000, variance); - const inputTokensVariance = projectedInputTokens * seededRandom(futureDate, i + 2000, variance); + const requestsVariance = + projectedRequests * seededRandom(futureDate, i, variance); + const costVariance = + projectedCost * seededRandom(futureDate, i + 1000, variance); + const inputTokensVariance = + projectedInputTokens * seededRandom(futureDate, i + 2000, variance); const outputTokensVariance = projectedOutputTokens * seededRandom(futureDate, i + 3000, variance); const confidence = Math.max( PROJECTION_CONSTANTS.MIN_CONFIDENCE, - PROJECTION_CONSTANTS.INITIAL_CONFIDENCE - i * PROJECTION_CONSTANTS.CONFIDENCE_DECAY_RATE, + PROJECTION_CONSTANTS.INITIAL_CONFIDENCE - + i * PROJECTION_CONSTANTS.CONFIDENCE_DECAY_RATE, ); combined.push({ @@ -213,8 +237,11 @@ export function generateProjectionAlerts( const recentPeriods = historicalData.slice(-3); const avgRequests = - recentPeriods.reduce((sum, d) => sum + d.totalRequests, 0) / recentPeriods.length; - const avgCost = recentPeriods.reduce((sum, d) => sum + d.totalCost, 0) / recentPeriods.length; + recentPeriods.reduce((sum, d) => sum + d.totalRequests, 0) / + recentPeriods.length; + const avgCost = + recentPeriods.reduce((sum, d) => sum + d.totalCost, 0) / + recentPeriods.length; const projectedOnly = projectedData.filter((d) => d.isProjected); if (projectedOnly.length === 0) { @@ -222,7 +249,8 @@ export function generateProjectionAlerts( } const maxProjectedCost = Math.max(...projectedOnly.map((d) => d.totalCost)); - const costIncrease = avgCost > 0 ? ((maxProjectedCost - avgCost) / avgCost) * 100 : 0; + const costIncrease = + avgCost > 0 ? ((maxProjectedCost - avgCost) / avgCost) * 100 : 0; if (costIncrease > 50) { alerts.push({ @@ -240,9 +268,13 @@ export function generateProjectionAlerts( }); } - const maxProjectedRequests = Math.max(...projectedOnly.map((d) => d.totalRequests)); + const maxProjectedRequests = Math.max( + ...projectedOnly.map((d) => d.totalRequests), + ); const requestIncrease = - avgRequests > 0 ? ((maxProjectedRequests - avgRequests) / avgRequests) * 100 : 0; + avgRequests > 0 + ? ((maxProjectedRequests - avgRequests) / avgRequests) * 100 + : 0; if (requestIncrease > 100) { alerts.push({ @@ -253,7 +285,9 @@ export function generateProjectionAlerts( }); } - const costRegression = calculateLinearRegression(historicalData.map((d) => d.totalCost)); + const costRegression = calculateLinearRegression( + historicalData.map((d) => d.totalCost), + ); if (costRegression.slope < -0.1) { alerts.push({ type: "info", @@ -262,7 +296,10 @@ export function generateProjectionAlerts( }); } - const totalProjectedCost = projectedOnly.reduce((sum, d) => sum + d.totalCost, 0); + const totalProjectedCost = projectedOnly.reduce( + (sum, d) => sum + d.totalCost, + 0, + ); if (totalProjectedCost > 100000) { alerts.push({ type: "warning", @@ -274,7 +311,8 @@ export function generateProjectionAlerts( const avgDailyCost = historicalData.length > 0 - ? historicalData.reduce((sum, d) => sum + d.totalCost, 0) / historicalData.length + ? historicalData.reduce((sum, d) => sum + d.totalCost, 0) / + historicalData.length : 0; const numericBalance = Number(creditBalance); if (avgDailyCost > 0 && numericBalance / avgDailyCost < 7) { diff --git a/packages/lib/api/a2a/handlers.ts b/packages/lib/api/a2a/handlers.ts index 661fc3d91..9408036c5 100644 --- a/packages/lib/api/a2a/handlers.ts +++ b/packages/lib/api/a2a/handlers.ts @@ -5,7 +5,10 @@ */ import { v4 as uuidv4 } from "uuid"; -import { a2aTaskStoreService, type TaskStoreEntry } from "@/lib/services/a2a-task-store"; +import { + a2aTaskStoreService, + type TaskStoreEntry, +} from "@/lib/services/a2a-task-store"; import { contentModerationService } from "@/lib/services/content-moderation"; import { logger } from "@/lib/utils/logger"; import { @@ -55,7 +58,12 @@ async function updateTaskState( state: TaskState, message?: Message, ): Promise { - return a2aTaskStoreService.updateTaskState(taskId, organizationId, state, message); + return a2aTaskStoreService.updateTaskState( + taskId, + organizationId, + state, + message, + ); } async function addArtifactToTask( @@ -71,7 +79,11 @@ async function addMessageToHistory( organizationId: string, message: Message, ): Promise { - await a2aTaskStoreService.addMessageToHistory(taskId, organizationId, message); + await a2aTaskStoreService.addMessageToHistory( + taskId, + organizationId, + message, + ); } async function storeTask( @@ -115,13 +127,18 @@ export async function handleMessageSend( .join("\n"); if (textContent) { - contentModerationService.moderateInBackground(textContent, ctx.user.id, undefined, (result) => { - logger.warn("[A2A] Moderation violation detected", { - userId: ctx.user.id, - categories: result.flaggedCategories, - action: result.action, - }); - }); + contentModerationService.moderateInBackground( + textContent, + ctx.user.id, + undefined, + (result) => { + logger.warn("[A2A] Moderation violation detected", { + userId: ctx.user.id, + categories: result.flaggedCategories, + action: result.action, + }); + }, + ); } // Create a new task @@ -155,7 +172,8 @@ async function processA2AMessage( (p): p is { type: "text"; text: string } => p.type === "text", ); const dataParts = message.parts.filter( - (p): p is { type: "data"; data: Record } => p.type === "data", + (p): p is { type: "data"; data: Record } => + p.type === "data", ); const textContent = textParts.map((p) => p.text).join("\n"); @@ -169,7 +187,11 @@ async function processA2AMessage( // Dispatch to appropriate skill if (skillId === "chat_completion" || (textContent && !skillId)) { - const result = await executeSkillChatCompletion(textContent, dataContent, ctx); + const result = await executeSkillChatCompletion( + textContent, + dataContent, + ctx, + ); responseMessage = createMessage("agent", [createTextPart(result.content)]); artifacts.push( createArtifact( @@ -185,7 +207,11 @@ async function processA2AMessage( ), ); } else if (skillId === "image_generation") { - const result = await executeSkillImageGeneration(textContent, dataContent, ctx); + const result = await executeSkillImageGeneration( + textContent, + dataContent, + ctx, + ); responseMessage = createMessage("agent", [ { type: "file", @@ -193,10 +219,18 @@ async function processA2AMessage( }, ]); artifacts.push( - createArtifact([createDataPart({ cost: result.cost })], "cost", "Generation cost"), + createArtifact( + [createDataPart({ cost: result.cost })], + "cost", + "Generation cost", + ), ); } else if (skillId === "chat_with_agent") { - const result = await executeSkillChatWithAgent(textContent, dataContent, ctx); + const result = await executeSkillChatWithAgent( + textContent, + dataContent, + ctx, + ); responseMessage = createMessage("agent", [createTextPart(result.response)]); } else if (skillId === "list_agents") { const result = await executeSkillListAgents(dataContent, ctx); @@ -211,7 +245,11 @@ async function processA2AMessage( const result = await executeSkillSaveMemory(textContent, dataContent, ctx); responseMessage = createMessage("agent", [createDataPart(result)]); } else if (skillId === "retrieve_memories") { - const result = await executeSkillRetrieveMemories(textContent, dataContent, ctx); + const result = await executeSkillRetrieveMemories( + textContent, + dataContent, + ctx, + ); responseMessage = createMessage("agent", [createDataPart(result)]); } else if (skillId === "list_containers") { const result = await executeSkillListContainers(dataContent, ctx); @@ -226,14 +264,22 @@ async function processA2AMessage( const result = await executeSkillGetConversationContext(dataContent, ctx); responseMessage = createMessage("agent", [createDataPart(result)]); } else if (skillId === "video_generation" || skillId === "generate_video") { - const result = await executeSkillVideoGeneration(textContent, dataContent, ctx); + const result = await executeSkillVideoGeneration( + textContent, + dataContent, + ctx, + ); responseMessage = createMessage("agent", [createDataPart(result)]); } else if (skillId === "get_user_profile" || skillId === "profile") { const result = await executeSkillGetUserProfile(ctx); responseMessage = createMessage("agent", [createDataPart(result)]); } else { // Default: treat as chat completion - const result = await executeSkillChatCompletion(textContent, dataContent, ctx); + const result = await executeSkillChatCompletion( + textContent, + dataContent, + ctx, + ); responseMessage = createMessage("agent", [createTextPart(result.content)]); } @@ -264,7 +310,10 @@ async function processA2AMessage( /** * tasks/get - Get task status and history */ -export async function handleTasksGet(params: TaskGetParams, ctx: A2AContext): Promise { +export async function handleTasksGet( + params: TaskGetParams, + ctx: A2AContext, +): Promise { const { id, historyLength } = params; const store = await getTaskStore(id, ctx.user.organization_id); @@ -284,7 +333,10 @@ export async function handleTasksGet(params: TaskGetParams, ctx: A2AContext): Pr /** * tasks/cancel - Cancel a running task */ -export async function handleTasksCancel(params: TaskCancelParams, ctx: A2AContext): Promise { +export async function handleTasksCancel( + params: TaskCancelParams, + ctx: A2AContext, +): Promise { const { id } = params; const store = await getTaskStore(id, ctx.user.organization_id); @@ -292,9 +344,16 @@ export async function handleTasksCancel(params: TaskCancelParams, ctx: A2AContex throw new Error(`Task not found: ${id}`); } - const terminalStates: TaskState[] = ["completed", "canceled", "failed", "rejected"]; + const terminalStates: TaskState[] = [ + "completed", + "canceled", + "failed", + "rejected", + ]; if (terminalStates.includes(store.task.status.state)) { - throw new Error(`Task ${id} is already in terminal state: ${store.task.status.state}`); + throw new Error( + `Task ${id} is already in terminal state: ${store.task.status.state}`, + ); } const task = await updateTaskState(id, ctx.user.organization_id, "canceled"); diff --git a/packages/lib/api/a2a/skills.ts b/packages/lib/api/a2a/skills.ts index 295774b6b..cd504eb8b 100644 --- a/packages/lib/api/a2a/skills.ts +++ b/packages/lib/api/a2a/skills.ts @@ -12,7 +12,11 @@ import { gateway } from "@ai-sdk/gateway"; import { streamText } from "ai"; -import { calculateCost, estimateRequestCost, getProviderFromModel } from "@/lib/pricing"; +import { + calculateCost, + estimateRequestCost, + getProviderFromModel, +} from "@/lib/pricing"; import { mergeAnthropicCotProviderOptions, mergeGoogleImageModalitiesWithAnthropicCot, @@ -70,10 +74,17 @@ export async function executeSkillChatCompletion( const cotBudget = resolveAnthropicThinkingBudgetTokens(model, process.env); const effectiveMaxTokens = cotBudget != null - ? Math.max(options.maxTokens ?? MIN_RESPONSE_TOKENS, cotBudget + MIN_RESPONSE_TOKENS) + ? Math.max( + options.maxTokens ?? MIN_RESPONSE_TOKENS, + cotBudget + MIN_RESPONSE_TOKENS, + ) : options.maxTokens; const provider = getProviderFromModel(model); - const estimatedCost = await estimateRequestCost(model, messages, effectiveMaxTokens); + const estimatedCost = await estimateRequestCost( + model, + messages, + effectiveMaxTokens, + ); // Reserve credits BEFORE the operation (TOCTOU-safe) let reservation: CreditReservation; @@ -101,8 +112,12 @@ export async function executeSkillChatCompletion( content: m.content, })), ...options, - ...(effectiveMaxTokens != null && { maxOutputTokens: effectiveMaxTokens }), - ...(cotBudget != null ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) : {}), + ...(effectiveMaxTokens != null && { + maxOutputTokens: effectiveMaxTokens, + }), + ...(cotBudget != null + ? mergeAnthropicCotProviderOptions(model, process.env, cotBudget) + : {}), }); let fullText = ""; @@ -180,7 +195,9 @@ export async function executeSkillImageGeneration( }); } catch (error) { if (error instanceof InsufficientCreditsError) { - throw new Error(`Insufficient credits: need $${imageCost.totalCost.toFixed(4)}`); + throw new Error( + `Insufficient credits: need $${imageCost.totalCost.toFixed(4)}`, + ); } throw error; } @@ -256,7 +273,9 @@ export async function executeSkillImageGeneration( /** * Check balance skill */ -export async function executeSkillCheckBalance(ctx: A2AContext): Promise { +export async function executeSkillCheckBalance( + ctx: A2AContext, +): Promise { const org = await organizationsService.getById(ctx.user.organization_id); if (!org) throw new Error("Organization not found"); return { @@ -274,7 +293,10 @@ export async function executeSkillGetUsage( ctx: A2AContext, ): Promise { const limit = Math.min(50, (dataContent.limit as number) || 10); - const records = await usageService.listByOrganization(ctx.user.organization_id, limit); + const records = await usageService.listByOrganization( + ctx.user.organization_id, + limit, + ); return { usage: records.map((r) => ({ id: r.id, @@ -297,7 +319,9 @@ export async function executeSkillListAgents( ctx: A2AContext, ): Promise { const limit = (dataContent.limit as number) || 20; - const chars = await charactersService.listByOrganization(ctx.user.organization_id); + const chars = await charactersService.listByOrganization( + ctx.user.organization_id, + ); return { agents: chars.slice(0, limit).map((c) => ({ id: c.id, @@ -327,7 +351,8 @@ export async function executeSkillChatWithAgent( if (!agentId && !roomId) throw new Error("agentId or roomId required"); // If we have a roomId, use it directly; otherwise create/get a room for the agent - const actualRoomId = roomId || (await agentService.getOrCreateRoom(entityId, agentId!)); + const actualRoomId = + roomId || (await agentService.getOrCreateRoom(entityId, agentId!)); const response = await agentService.sendMessage({ roomId: actualRoomId, @@ -354,7 +379,9 @@ export async function executeSkillSaveMemory( ctx: A2AContext, ): Promise { const content = (dataContent.content as string) || textContent; - const type = (dataContent.type as "fact" | "preference" | "context" | "document") || "fact"; + const type = + (dataContent.type as "fact" | "preference" | "context" | "document") || + "fact"; const roomId = dataContent.roomId as string; const tags = dataContent.tags as string[] | undefined; const metadata = dataContent.metadata as Record | undefined; @@ -412,7 +439,9 @@ export async function executeSkillRetrieveMemories( const type = dataContent.type as string[] | undefined; const tags = dataContent.tags as string[] | undefined; const limit = Math.min(50, (dataContent.limit as number) || 10); - const sortBy = (dataContent.sortBy as "relevance" | "recent" | "importance") || "relevance"; + const sortBy = + (dataContent.sortBy as "relevance" | "recent" | "importance") || + "relevance"; const memories = await memoryService.retrieveMemories({ organizationId: ctx.user.organization_id, @@ -428,7 +457,9 @@ export async function executeSkillRetrieveMemories( memories: memories.map((m) => ({ id: m.memory.id || "", content: - typeof m.memory.content === "string" ? m.memory.content : JSON.stringify(m.memory.content), + typeof m.memory.content === "string" + ? m.memory.content + : JSON.stringify(m.memory.content), score: m.score, createdAt: typeof m.memory.createdAt === "string" @@ -500,7 +531,9 @@ export async function executeSkillListContainers( ctx: A2AContext, ): Promise { const status = dataContent.status as string | undefined; - let containers = await containersService.listByOrganization(ctx.user.organization_id); + let containers = await containersService.listByOrganization( + ctx.user.organization_id, + ); if (status) containers = containers.filter((c) => c.status === status); return { containers: containers.map((c) => ({ @@ -542,7 +575,10 @@ export async function executeSkillGetConversationContext( if (!conversationId) throw new Error("conversationId required"); const conversation = await conversationsService.getById(conversationId); - if (!conversation || conversation.organization_id !== ctx.user.organization_id) { + if ( + !conversation || + conversation.organization_id !== ctx.user.organization_id + ) { throw new Error("Conversation not found"); } diff --git a/packages/lib/api/admin-auth.ts b/packages/lib/api/admin-auth.ts index 3582fdd7c..4be92b61a 100644 --- a/packages/lib/api/admin-auth.ts +++ b/packages/lib/api/admin-auth.ts @@ -25,7 +25,9 @@ export async function requireAdminWithResponse( return await requireAdmin(request); } catch (error) { if (error instanceof AuthenticationError) { - logger.warn(`${logPrefix} Authentication failed`, { error: error.message }); + logger.warn(`${logPrefix} Authentication failed`, { + error: error.message, + }); return NextResponse.json({ error: error.message }, { status: 401 }); } if (error instanceof ForbiddenError) { @@ -33,6 +35,9 @@ export async function requireAdminWithResponse( return NextResponse.json({ error: error.message }, { status: 403 }); } logger.error(`${logPrefix} Unexpected auth error`, { error }); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/packages/lib/api/compat-envelope.ts b/packages/lib/api/compat-envelope.ts index c593711fe..2fb601c50 100644 --- a/packages/lib/api/compat-envelope.ts +++ b/packages/lib/api/compat-envelope.ts @@ -20,7 +20,10 @@ * eliza-cloud "error" → thin-client "failed" */ -import type { MiladySandbox, MiladySandboxStatus } from "@/db/schemas/milady-sandboxes"; +import type { + MiladySandbox, + MiladySandboxStatus, +} from "@/db/schemas/milady-sandboxes"; import { getMiladyAgentPublicWebUiUrl } from "@/lib/milady-web-ui"; /** @@ -96,7 +99,9 @@ export function toCompatAgent( webUiUrl, database_status: sandbox.database_status, error_message: sandbox.error_message, - last_heartbeat_at: sandbox.last_heartbeat_at ? toISO(sandbox.last_heartbeat_at) : null, + last_heartbeat_at: sandbox.last_heartbeat_at + ? toISO(sandbox.last_heartbeat_at) + : null, // Wallet info — only expose a provider when wallet info was actually resolved wallet_address: walletInfo?.address ?? null, wallet_provider: walletInfo?.provider ?? null, @@ -123,7 +128,9 @@ export interface CompatCreateResultShape { * `jobId` intentionally equals `agentId` because compat job polling * synthesizes status from the same sandbox row. */ -export function toCompatCreateResult(sandbox: MiladySandbox): CompatCreateResultShape { +export function toCompatCreateResult( + sandbox: MiladySandbox, +): CompatCreateResultShape { return { agentId: sandbox.id, agentName: sandbox.agent_name ?? "", @@ -269,7 +276,9 @@ export function toCompatStatus(sandbox: MiladySandbox): CompatStatusShape { return { status: mapStatus(sandbox.status), - lastHeartbeat: sandbox.last_heartbeat_at ? toISO(sandbox.last_heartbeat_at) : null, + lastHeartbeat: sandbox.last_heartbeat_at + ? toISO(sandbox.last_heartbeat_at) + : null, bridgeUrl: sandbox.bridge_url ?? null, webUiUrl, currentNode: sandbox.node_id ?? null, @@ -296,7 +305,8 @@ export interface CompatUsageShape { export function toCompatUsage(sandbox: MiladySandbox): CompatUsageShape { const createdAt = new Date(sandbox.created_at); const now = new Date(); - const uptimeMs = sandbox.status === "running" ? now.getTime() - createdAt.getTime() : 0; + const uptimeMs = + sandbox.status === "running" ? now.getTime() - createdAt.getTime() : 0; const uptimeHours = Math.round((uptimeMs / (1000 * 60 * 60)) * 100) / 100; const config = (sandbox.agent_config ?? {}) as Record; @@ -353,7 +363,9 @@ export function mapStatus(status: MiladySandboxStatus): string { * is conveyed separately via `data.status` / `result.status` in the job * payload (see `toCompatJob`). */ -function mapStatusToJobStatus(status: MiladySandboxStatus): CompatJobShape["status"] { +function mapStatusToJobStatus( + status: MiladySandboxStatus, +): CompatJobShape["status"] { switch (status) { case "pending": return "queued"; diff --git a/packages/lib/api/errors.ts b/packages/lib/api/errors.ts index c78e2786e..6bcf48aad 100644 --- a/packages/lib/api/errors.ts +++ b/packages/lib/api/errors.ts @@ -172,7 +172,10 @@ export function getErrorStatusCode(error: unknown): number { if (error.name === "InsufficientCreditsError") { return 402; } - if (error.name === "AuthenticationError" || error.name === "UnauthorizedError") { + if ( + error.name === "AuthenticationError" || + error.name === "UnauthorizedError" + ) { return 401; } if (error.name === "ForbiddenError" || error.name === "AccessDeniedError") { @@ -284,7 +287,9 @@ export function getSafeErrorMessage(error: unknown): string { "getaddrinfo", "ENOTFOUND", ]; - if (denyPatterns.some((pattern) => message.includes(pattern.toLowerCase()))) { + if ( + denyPatterns.some((pattern) => message.includes(pattern.toLowerCase())) + ) { return "An unexpected error occurred"; } @@ -304,7 +309,10 @@ export function getSafeErrorMessage(error: unknown): string { } // Only apply safePatterns for client errors (< 500), not internal failures const status = getErrorStatusCode(error); - if (status < 500 && safePatterns.some((pattern) => message.includes(pattern))) { + if ( + status < 500 && + safePatterns.some((pattern) => message.includes(pattern)) + ) { return error.message; } } @@ -365,7 +373,10 @@ export function errorToResponse(error: unknown): Response { const status = getErrorStatusCode(error); const message = getSafeErrorMessage(error); - const body = error instanceof ApiError ? error.toJSON() : { success: false, error: message }; + const body = + error instanceof ApiError + ? error.toJSON() + : { success: false, error: message }; return new Response(JSON.stringify(body), { status, @@ -389,8 +400,11 @@ export function jsonError( headers.set(key, value); } } - return new Response(JSON.stringify({ success: false, error: message, code }), { - status, - headers, - }); + return new Response( + JSON.stringify({ success: false, error: message, code }), + { + status, + headers, + }, + ); } diff --git a/packages/lib/api/feature-gate.ts b/packages/lib/api/feature-gate.ts index f8726ebee..ba9d7aa59 100644 --- a/packages/lib/api/feature-gate.ts +++ b/packages/lib/api/feature-gate.ts @@ -1,9 +1,16 @@ import { NextResponse } from "next/server"; -import { type FeatureFlag, getFeatureForRoute, isFeatureEnabled } from "@/lib/config/feature-flags"; +import { + type FeatureFlag, + getFeatureForRoute, + isFeatureEnabled, +} from "@/lib/config/feature-flags"; export function requireFeature(flag: FeatureFlag): NextResponse | null { if (!isFeatureEnabled(flag)) { - return NextResponse.json({ success: false, error: "Feature not available" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Feature not available" }, + { status: 404 }, + ); } return null; } @@ -11,7 +18,10 @@ export function requireFeature(flag: FeatureFlag): NextResponse | null { export function checkRouteFeature(pathname: string): NextResponse | null { const feature = getFeatureForRoute(pathname); if (feature && !isFeatureEnabled(feature)) { - return NextResponse.json({ success: false, error: "Feature not available" }, { status: 404 }); + return NextResponse.json( + { success: false, error: "Feature not available" }, + { status: 404 }, + ); } return null; } diff --git a/packages/lib/api/stream-utils.ts b/packages/lib/api/stream-utils.ts index be462eb32..8dfad10c8 100644 --- a/packages/lib/api/stream-utils.ts +++ b/packages/lib/api/stream-utils.ts @@ -17,7 +17,9 @@ export interface StreamWriter { /** * Creates a managed SSE stream writer with heartbeat support */ -export function createStreamWriter(writer: WritableStreamDefaultWriter): StreamWriter { +export function createStreamWriter( + writer: WritableStreamDefaultWriter, +): StreamWriter { const encoder = new TextEncoder(); const state: StreamState = { isClientConnected: true, @@ -36,7 +38,8 @@ export function createStreamWriter(writer: WritableStreamDefaultWriter = { name: "ElizaAuthProvider", path: "@/components/eliza/auth/auth-provider", category: "auth", - description: "Root auth provider that wraps the app. Already included in layout.tsx.", + description: + "Root auth provider that wraps the app. Already included in layout.tsx.", props: [ { name: "children", @@ -69,7 +70,8 @@ export const ELIZA_COMPONENTS: Record = { name: "LoginButton", path: "@/components/eliza/auth/login-button", category: "auth", - description: "Pre-styled login button with multiple variants. Handles auth flow automatically.", + description: + "Pre-styled login button with multiple variants. Handles auth flow automatically.", props: [ { name: "variant", @@ -111,7 +113,8 @@ export const ELIZA_COMPONENTS: Record = { name: "UserProfile", path: "@/components/eliza/auth/user-profile", category: "auth", - description: "Displays user avatar, name, and dropdown menu with logout option.", + description: + "Displays user avatar, name, and dropdown menu with logout option.", props: [ { name: "showCredits", @@ -137,7 +140,8 @@ export const ELIZA_COMPONENTS: Record = { name: "ProtectedRoute", path: "@/components/eliza/auth/protected-route", category: "auth", - description: "Wrapper that redirects unauthenticated users. Use for protected pages.", + description: + "Wrapper that redirects unauthenticated users. Use for protected pages.", props: [ { name: "children", @@ -228,7 +232,8 @@ export default function DashboardPage() { name: "MessageList", path: "@/components/eliza/chat/message-list", category: "chat", - description: "Scrollable message list with auto-scroll. Renders user and assistant messages.", + description: + "Scrollable message list with auto-scroll. Renders user and assistant messages.", props: [ { name: "messages", @@ -294,7 +299,8 @@ export default function DashboardPage() { name: "CreditBalance", path: "@/components/eliza/billing/credit-balance", category: "billing", - description: "Displays current credit balance with auto-refresh. Optional top-up button.", + description: + "Displays current credit balance with auto-refresh. Optional top-up button.", props: [ { name: "showTopUp", @@ -377,7 +383,8 @@ export default function DashboardPage() { name: "AgentCard", path: "@/components/eliza/agents/agent-card", category: "agents", - description: "Card displaying agent info with avatar, name, bio, and action buttons.", + description: + "Card displaying agent info with avatar, name, bio, and action buttons.", props: [ { name: "agent", @@ -411,7 +418,8 @@ export default function DashboardPage() { name: "AgentList", path: "@/components/eliza/agents/agent-list", category: "agents", - description: "Grid of agent cards. Fetches agents automatically using listAgents().", + description: + "Grid of agent cards. Fetches agents automatically using listAgents().", props: [ { name: "onSelectAgent", @@ -440,7 +448,8 @@ export default function DashboardPage() { name: "AgentChat", path: "@/components/eliza/agents/agent-chat", category: "agents", - description: "Chat interface for a specific agent. Uses chatWithAgent() API.", + description: + "Chat interface for a specific agent. Uses chatWithAgent() API.", props: [ { name: "agentId", @@ -541,7 +550,8 @@ export default function DashboardPage() { name: "DashboardLayout", path: "@/components/eliza/layout/dashboard-layout", category: "layout", - description: "Complete dashboard layout with sidebar, header, and main content area.", + description: + "Complete dashboard layout with sidebar, header, and main content area.", props: [ { name: "children", @@ -644,7 +654,9 @@ const items = [ /** * Get all components by category */ -export function getComponentsByCategory(category: ComponentCategory): ComponentDefinition[] { +export function getComponentsByCategory( + category: ComponentCategory, +): ComponentDefinition[] { return Object.values(ELIZA_COMPONENTS).filter((c) => c.category === category); } @@ -652,7 +664,9 @@ export function getComponentsByCategory(category: ComponentCategory): ComponentD * Get all available categories */ export function getCategories(): ComponentCategory[] { - const categories = new Set(Object.values(ELIZA_COMPONENTS).map((c) => c.category)); + const categories = new Set( + Object.values(ELIZA_COMPONENTS).map((c) => c.category), + ); return Array.from(categories); } @@ -663,16 +677,21 @@ export function searchComponents(query: string): ComponentDefinition[] { const lowerQuery = query.toLowerCase(); return Object.values(ELIZA_COMPONENTS).filter( (c) => - c.name.toLowerCase().includes(lowerQuery) || c.description.toLowerCase().includes(lowerQuery), + c.name.toLowerCase().includes(lowerQuery) || + c.description.toLowerCase().includes(lowerQuery), ); } /** * Generate prompt-friendly component catalog */ -export function generateComponentCatalog(categories?: ComponentCategory[]): string { +export function generateComponentCatalog( + categories?: ComponentCategory[], +): string { const components = categories - ? Object.values(ELIZA_COMPONENTS).filter((c) => categories.includes(c.category)) + ? Object.values(ELIZA_COMPONENTS).filter((c) => + categories.includes(c.category), + ) : Object.values(ELIZA_COMPONENTS); const grouped = components.reduce( diff --git a/packages/lib/app-builder/knowledge-context.ts b/packages/lib/app-builder/knowledge-context.ts index dfc3a9302..eb25f2454 100644 --- a/packages/lib/app-builder/knowledge-context.ts +++ b/packages/lib/app-builder/knowledge-context.ts @@ -1427,7 +1427,9 @@ export async function buildKnowledgeContext( // SDK Reference const sdkReference = - tierConfig.includes.sdk === "compact" ? SDK_REFERENCE_COMPACT : SDK_REFERENCE_FULL; + tierConfig.includes.sdk === "compact" + ? SDK_REFERENCE_COMPACT + : SDK_REFERENCE_FULL; // Component Catalog let componentCatalog = ""; @@ -1480,7 +1482,9 @@ export async function buildKnowledgeContext( }; } -function getDefaultPatterns(tierConfig: (typeof TIER_CONFIG)[ContextTier]): PatternType[] { +function getDefaultPatterns( + tierConfig: (typeof TIER_CONFIG)[ContextTier], +): PatternType[] { if (tierConfig.includes.patterns === "none") return []; if (tierConfig.includes.patterns === "common") { return ["streaming-chat", "credits-display", "dashboard-layout"]; @@ -1587,7 +1591,10 @@ export const metadata: Metadata = { /** * Smart tier selection based on prompt analysis */ -export function selectContextTier(prompt: string, templateType?: string): ContextTier { +export function selectContextTier( + prompt: string, + templateType?: string, +): ContextTier { const lowerPrompt = prompt.toLowerCase(); // Keywords that suggest need for comprehensive context @@ -1636,7 +1643,8 @@ export function selectContextTier(prompt: string, templateType?: string): Contex "saas-starter", "ai-tool", ]; - const isComplexTemplate = templateType && complexTemplates.includes(templateType); + const isComplexTemplate = + templateType && complexTemplates.includes(templateType); if (isComplexTemplate || (hasComplex && !hasSimple)) { return "comprehensive"; diff --git a/packages/lib/app-builder/markdown-components.tsx b/packages/lib/app-builder/markdown-components.tsx index bec0a13e7..92f569a0d 100644 --- a/packages/lib/app-builder/markdown-components.tsx +++ b/packages/lib/app-builder/markdown-components.tsx @@ -107,10 +107,13 @@ const AnimatedSpinnerInline = () => ( /** * Check if content starts with a status marker */ -function getStatusMarker(children: React.ReactNode): "check" | "spinner" | null { +function getStatusMarker( + children: React.ReactNode, +): "check" | "spinner" | null { if (typeof children === "string") { if (children.startsWith("✓ ") || children.startsWith("✓")) return "check"; - if (children.startsWith("⏳ ") || children.startsWith("⏳")) return "spinner"; + if (children.startsWith("⏳ ") || children.startsWith("⏳")) + return "spinner"; } if (Array.isArray(children) && children.length > 0) { return getStatusMarker(children[0]); @@ -163,7 +166,11 @@ function transformStatusMarkers(children: React.ReactNode): React.ReactNode { if (Array.isArray(children)) { return children.map((child, i) => { if (i === 0) { - return {transformStatusMarkers(child)}; + return ( + + {transformStatusMarkers(child)} + + ); } return {child}; }); @@ -187,13 +194,19 @@ function transformStatusMarkers(children: React.ReactNode): React.ReactNode { export const markdownComponents = { h1: ({ children }: { children?: React.ReactNode }) => ( -

{children}

+

+ {children} +

), h2: ({ children }: { children?: React.ReactNode }) => ( -

{children}

+

+ {children} +

), h3: ({ children }: { children?: React.ReactNode }) => ( -

{children}

+

+ {children} +

), p: ({ children }: { children?: React.ReactNode }) => { // Check if this paragraph starts with reasoning marker @@ -211,13 +224,19 @@ export const markdownComponents = { if (marker) { return (

- {marker === "check" ? : } + {marker === "check" ? ( + + ) : ( + + )} {stripStatusMarker(children)}

); } - return

{children}

; + return ( +

{children}

+ ); }, ul: ({ children }: { children?: React.ReactNode }) => (
    {children}
@@ -228,7 +247,13 @@ export const markdownComponents = { li: ({ children }: { children?: React.ReactNode }) => (
  • {children}
  • ), - code: ({ className, children }: { className?: string; children?: React.ReactNode }) => { + code: ({ + className, + children, + }: { + className?: string; + children?: React.ReactNode; + }) => { const isInline = !className; if (isInline) { return ( @@ -249,7 +274,9 @@ export const markdownComponents = {
    ), strong: ({ children }: { children?: React.ReactNode }) => ( - {transformStatusMarkers(children)} + + {transformStatusMarkers(children)} + ), em: ({ children }: { children?: React.ReactNode }) => { // Check if this is reasoning text (starts with 💭) @@ -294,7 +321,9 @@ export const markdownComponents = { {children} ), th: ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ), td: ({ children }: { children?: React.ReactNode }) => ( {children} diff --git a/packages/lib/app-builder/sse-parser.ts b/packages/lib/app-builder/sse-parser.ts index 24a58d0e8..4a8c8c828 100644 --- a/packages/lib/app-builder/sse-parser.ts +++ b/packages/lib/app-builder/sse-parser.ts @@ -20,7 +20,10 @@ export interface SSEParserOptions { * @param response - The fetch Response with SSE stream * @param options - Parser options including event handlers */ -export async function parseSSEStream(response: Response, options: SSEParserOptions): Promise { +export async function parseSSEStream( + response: Response, + options: SSEParserOptions, +): Promise { const reader = response.body?.getReader(); if (!reader) { throw new Error("No response body"); diff --git a/packages/lib/app-builder/store.ts b/packages/lib/app-builder/store.ts index 7ca56c517..a1e7d0f7b 100644 --- a/packages/lib/app-builder/store.ts +++ b/packages/lib/app-builder/store.ts @@ -60,11 +60,14 @@ export const useChatInput = create()((set) => ({ images: [], setInput: (input) => set({ input }), addImage: (image) => set((state) => ({ images: [...state.images, image] })), - removeImage: (id) => set((state) => ({ images: state.images.filter((img) => img.id !== id) })), + removeImage: (id) => + set((state) => ({ images: state.images.filter((img) => img.id !== id) })), clearImages: () => set({ images: [] }), setImageBase64: (id, base64) => set((state) => ({ - images: state.images.map((img) => (img.id === id ? { ...img, base64 } : img)), + images: state.images.map((img) => + img.id === id ? { ...img, base64 } : img, + ), })), setImageBlobUrl: (id, blobUrl) => set((state) => ({ @@ -75,7 +78,9 @@ export const useChatInput = create()((set) => ({ setImageUploadStatus: (id, status, error) => set((state) => ({ images: state.images.map((img) => - img.id === id ? { ...img, uploadStatus: status, uploadError: error } : img, + img.id === id + ? { ...img, uploadStatus: status, uploadError: error } + : img, ), })), clearInput: () => set({ input: "", images: [] }), @@ -88,7 +93,10 @@ interface MessagesSlice { messages: Message[]; setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void; addMessage: (message: Message) => void; - updateMessage: (thinkingId: number, updater: (msg: Message) => Message) => void; + updateMessage: ( + thinkingId: number, + updater: (msg: Message) => Message, + ) => void; clearMessages: () => void; } @@ -101,10 +109,13 @@ export const useMessages = create()((set) => ({ ? messagesOrUpdater(state.messages) : messagesOrUpdater, })), - addMessage: (message) => set((state) => ({ messages: [...state.messages, message] })), + addMessage: (message) => + set((state) => ({ messages: [...state.messages, message] })), updateMessage: (thinkingId, updater) => set((state) => ({ - messages: state.messages.map((m) => (m._thinkingId === thinkingId ? updater(m) : m)), + messages: state.messages.map((m) => + m._thinkingId === thinkingId ? updater(m) : m, + ), })), clearMessages: () => set({ messages: [] }), })); @@ -207,7 +218,9 @@ export const useUI = create()((set) => ({ setConsoleLogs: (logsOrUpdater) => set((state) => { const newLogs = - typeof logsOrUpdater === "function" ? logsOrUpdater(state.consoleLogs) : logsOrUpdater; + typeof logsOrUpdater === "function" + ? logsOrUpdater(state.consoleLogs) + : logsOrUpdater; // Limit logs to prevent memory issues return { consoleLogs: newLogs.slice(-MAX_LOGS) }; }), @@ -219,9 +232,10 @@ export const useUI = create()((set) => ({ set((state) => { const timestamp = new Date().toLocaleTimeString(); return { - consoleLogs: [...state.consoleLogs, `[${timestamp}] [${level}] ${message}`].slice( - -MAX_LOGS, - ), + consoleLogs: [ + ...state.consoleLogs, + `[${timestamp}] [${level}] ${message}`, + ].slice(-MAX_LOGS), }; }), clearConsoleLogs: () => set({ consoleLogs: [] }), @@ -282,7 +296,8 @@ export const useApp = create()((set) => ({ setTemplatePage: (templatePage) => set({ templatePage }), setIncludeMonetization: (includeMonetization) => set({ includeMonetization }), setIncludeAnalytics: (includeAnalytics) => set({ includeAnalytics }), - setIsGeneratingDescription: (isGeneratingDescription) => set({ isGeneratingDescription }), + setIsGeneratingDescription: (isGeneratingDescription) => + set({ isGeneratingDescription }), resetApp: () => set({ step: "setup", diff --git a/packages/lib/app-builder/tool-display.ts b/packages/lib/app-builder/tool-display.ts index 5aa4e7ea0..a54d945a1 100644 --- a/packages/lib/app-builder/tool-display.ts +++ b/packages/lib/app-builder/tool-display.ts @@ -25,7 +25,10 @@ export interface ToolInput { * @param input - The tool input parameters * @returns Formatted display information */ -export function formatToolDisplay(toolName: string, input?: ToolInput): ToolDisplayInfo { +export function formatToolDisplay( + toolName: string, + input?: ToolInput, +): ToolDisplayInfo { switch (toolName) { case "write_file": { const path = input?.path || "file"; @@ -179,10 +182,14 @@ export function buildCompletionContent( * @param actionsLog - Array of action log entries * @returns Markdown formatted error content */ -export function buildErrorContent(error: Error | string, actionsLog: ActionLogEntry[]): string { +export function buildErrorContent( + error: Error | string, + actionsLog: ActionLogEntry[], +): string { const errorMessage = error instanceof Error ? error.message : error; let content = `**Error:** ${errorMessage}\n\n`; - content += "The operation could not be completed. Please try again or modify your request."; + content += + "The operation could not be completed. Please try again or modify your request."; if (actionsLog.length > 0) { content += "\n\n---\n\n"; diff --git a/packages/lib/app-builder/types.ts b/packages/lib/app-builder/types.ts index 08312f883..74ba4fcd9 100644 --- a/packages/lib/app-builder/types.ts +++ b/packages/lib/app-builder/types.ts @@ -28,7 +28,13 @@ export type SessionStatus = | "not_configured" | "recovering"; -export type ProgressStep = "creating" | "installing" | "starting" | "restoring" | "ready" | "error"; +export type ProgressStep = + | "creating" + | "installing" + | "starting" + | "restoring" + | "ready" + | "error"; export type SourceType = "agent" | "workflow" | "service" | "standalone"; diff --git a/packages/lib/auth-anonymous.ts b/packages/lib/auth-anonymous.ts index 23eb6fd63..f961644ed 100644 --- a/packages/lib/auth-anonymous.ts +++ b/packages/lib/auth-anonymous.ts @@ -36,16 +36,28 @@ import { logger } from "@/lib/utils/logger"; // Constants - can be overridden via environment variables const ANON_SESSION_COOKIE = "eliza-anon-session"; -const ANON_SESSION_EXPIRY_DAYS = Number.parseInt(process.env.ANON_SESSION_EXPIRY_DAYS || "7", 10); +const ANON_SESSION_EXPIRY_DAYS = Number.parseInt( + process.env.ANON_SESSION_EXPIRY_DAYS || "7", + 10, +); // Affiliate flow uses ANON_MESSAGE_LIMIT (5 messages) // Public agent chat uses PUBLIC_CHAT_MESSAGE_LIMIT (3 messages) -const PUBLIC_CHAT_MESSAGE_LIMIT = Number.parseInt(process.env.PUBLIC_CHAT_MESSAGE_LIMIT || "3", 10); -const ANON_HOURLY_LIMIT = Number.parseInt(process.env.ANON_HOURLY_LIMIT || "10", 10); +const PUBLIC_CHAT_MESSAGE_LIMIT = Number.parseInt( + process.env.PUBLIC_CHAT_MESSAGE_LIMIT || "3", + 10, +); +const ANON_HOURLY_LIMIT = Number.parseInt( + process.env.ANON_HOURLY_LIMIT || "10", + 10, +); /** * Type for anonymous user (no organization) */ -type AnonymousUserWithOrganization = Omit & { +type AnonymousUserWithOrganization = Omit< + UserWithOrganization, + "organization_id" +> & { organization_id: null; organization: null; }; @@ -70,7 +82,10 @@ async function getClientIp(): Promise { return realIp; } // Fallback to x-forwarded-for (first IP in the chain) - const forwardedFor = headersList.get("x-forwarded-for")?.split(",")[0]?.trim(); + const forwardedFor = headersList + .get("x-forwarded-for") + ?.split(",")[0] + ?.trim(); return forwardedFor || undefined; } @@ -139,7 +154,9 @@ export async function getOrCreateAnonymousUser(): Promise<{ } const newSessionToken = nanoid(32); - const expiresAt = new Date(Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000); + const expiresAt = new Date( + Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000, + ); const ipAddress = await getClientIp(); const userAgent = await getUserAgent(); // NOTE: IP-based anonymous-session abuse checks intentionally removed. @@ -200,7 +217,9 @@ export async function checkAnonymousLimit(sessionId: string): Promise<{ }; } - const rateLimitResult = await anonymousSessionsService.checkRateLimit(session.id); + const rateLimitResult = await anonymousSessionsService.checkRateLimit( + session.id, + ); if (!rateLimitResult.allowed) { return { diff --git a/packages/lib/auth.ts b/packages/lib/auth.ts index c05e075bf..67962fbee 100644 --- a/packages/lib/auth.ts +++ b/packages/lib/auth.ts @@ -24,7 +24,10 @@ import { invalidatePrivyTokenCache, verifyAuthTokenCached, } from "./auth/privy-client"; -import { invalidateStewardTokenCache, verifyStewardTokenCached } from "./auth/steward-client"; +import { + invalidateStewardTokenCache, + verifyStewardTokenCached, +} from "./auth/steward-client"; // Steward user sync (mirrors Privy JIT sync) import { syncUserFromSteward } from "./steward-sync"; @@ -36,14 +39,20 @@ export type { Organization }; * Hash a token for use as cache key (never store raw tokens) */ function hashToken(token: string): string { - return crypto.createHash("sha256").update(token).digest("hex").substring(0, 32); + return crypto + .createHash("sha256") + .update(token) + .digest("hex") + .substring(0, 32); } /** * Invalidate user session cache (call when user/org data changes) * @param sessionToken - The session token to invalidate cache for */ -export async function invalidateUserSessionCache(sessionToken: string): Promise { +export async function invalidateUserSessionCache( + sessionToken: string, +): Promise { const tokenHash = hashToken(sessionToken); const cacheKey = CacheKeys.session.user(tokenHash); await redisCache.del(cacheKey); @@ -55,19 +64,26 @@ export async function invalidateUserSessionCache(sessionToken: string): Promise< * Call this on logout to ensure immediate invalidation * @param sessionToken - The Privy auth token to invalidate */ -export async function invalidateSessionCaches(sessionToken: string): Promise { +export async function invalidateSessionCaches( + sessionToken: string, +): Promise { await Promise.all([ invalidatePrivyTokenCache(sessionToken), invalidateStewardTokenCache(sessionToken), ]); - logger.debug("[AUTH] Invalidated all session caches (Privy + Steward + user)"); + logger.debug( + "[AUTH] Invalidated all session caches (Privy + Steward + user)", + ); } /** * Ensure user has a default API key for programmatic access * Creates one if it doesn't exist (for existing users who registered before auto-generation) */ -async function ensureUserHasApiKey(userId: string, organizationId: string): Promise { +async function ensureUserHasApiKey( + userId: string, + organizationId: string, +): Promise { // Validate inputs if (!userId || userId.trim() === "") { logger.warn("[Auth] Invalid userId, skipping API key check"); @@ -75,7 +91,9 @@ async function ensureUserHasApiKey(userId: string, organizationId: string): Prom } if (!organizationId || organizationId.trim() === "") { - logger.warn(`[Auth] No organization for user ${userId}, skipping API key check`); + logger.warn( + `[Auth] No organization for user ${userId}, skipping API key check`, + ); return; } @@ -103,13 +121,14 @@ export type AuthResult = { session_token?: string; }; -let privySyncLoader: null | (() => Promise) = null; +let privySyncLoader: null | (() => Promise) = + null; async function loadPrivySyncModule(): Promise { if (!privySyncLoader) { - privySyncLoader = new Function("return import('./privy-sync');") as () => Promise< - typeof import("./privy-sync") - >; + privySyncLoader = new Function( + "return import('./privy-sync');", + ) as () => Promise; } return await privySyncLoader(); @@ -122,7 +141,9 @@ async function getPlaywrightTestUser( return null; } - const testSession = cookieStore.get(PLAYWRIGHT_TEST_SESSION_COOKIE_NAME)?.value; + const testSession = cookieStore.get( + PLAYWRIGHT_TEST_SESSION_COOKIE_NAME, + )?.value; if (!testSession) { return null; } @@ -165,158 +186,184 @@ async function getPlaywrightTestUser( * * This handles the race condition where webhooks haven't fired yet. */ -export const getCurrentUser = cache(async (): Promise => { - try { - const cookieStore = await cookies(); - const playwrightTestUser = await getPlaywrightTestUser(cookieStore); - if (playwrightTestUser) { - return playwrightTestUser; - } - - // Get the auth token from cookies (try steward-token first, then privy-token) - const stewardToken = cookieStore.get("steward-token"); - const privyToken = cookieStore.get("privy-token"); - const authToken = stewardToken || privyToken; - - if (!authToken) { - return null; - } - - const tokenHash = hashToken(authToken.value); - const cacheKey = CacheKeys.session.user(tokenHash); +export const getCurrentUser = cache( + async (): Promise => { + try { + const cookieStore = await cookies(); + const playwrightTestUser = await getPlaywrightTestUser(cookieStore); + if (playwrightTestUser) { + return playwrightTestUser; + } - // Check Redis cache first - avoids both Privy API AND DB calls - const cachedUser = await redisCache.get(cacheKey); - if (cachedUser) { - logger.debug("[AUTH] Cache hit for user session"); + // Get the auth token from cookies (try steward-token first, then privy-token) + const stewardToken = cookieStore.get("steward-token"); + const privyToken = cookieStore.get("privy-token"); + const authToken = stewardToken || privyToken; - // Update session tracking in background (non-blocking) - if (cachedUser.organization_id) { - void trackSessionActivity(cachedUser.id, cachedUser.organization_id, authToken.value); + if (!authToken) { + return null; } - return cachedUser; - } + const tokenHash = hashToken(authToken.value); + const cacheKey = CacheKeys.session.user(tokenHash); + + // Check Redis cache first - avoids both Privy API AND DB calls + const cachedUser = await redisCache.get(cacheKey); + if (cachedUser) { + logger.debug("[AUTH] Cache hit for user session"); + + // Update session tracking in background (non-blocking) + if (cachedUser.organization_id) { + void trackSessionActivity( + cachedUser.id, + cachedUser.organization_id, + authToken.value, + ); + } - // If this is a steward token, verify with steward first - if (stewardToken) { - logger.debug("[AUTH] Verifying steward session cookie"); - const stewardClaims = await verifyStewardTokenCached(stewardToken.value); + return cachedUser; + } - if (stewardClaims) { - // Look up or JIT-create user - let user = await usersService.getByStewardId(stewardClaims.userId); + // If this is a steward token, verify with steward first + if (stewardToken) { + logger.debug("[AUTH] Verifying steward session cookie"); + const stewardClaims = await verifyStewardTokenCached( + stewardToken.value, + ); - if (!user) { - try { - user = await syncUserFromSteward({ - stewardUserId: stewardClaims.userId, - email: stewardClaims.email, - walletAddress: stewardClaims.address, - }); - } catch (syncErr) { - logger.error("[AUTH] Steward JIT sync failed", { error: syncErr }); - return null; + if (stewardClaims) { + // Look up or JIT-create user + let user = await usersService.getByStewardId(stewardClaims.userId); + + if (!user) { + try { + user = await syncUserFromSteward({ + stewardUserId: stewardClaims.userId, + email: stewardClaims.email, + walletAddress: stewardClaims.address, + }); + } catch (syncErr) { + logger.error("[AUTH] Steward JIT sync failed", { + error: syncErr, + }); + return null; + } } - } - if (user && user.is_active) { - // Cache the resolved user - await redisCache.set(cacheKey, user, CacheTTL.session.user); - if (user.organization_id) { - void trackSessionActivity(user.id, user.organization_id, stewardToken.value); + if (user && user.is_active) { + // Cache the resolved user + await redisCache.set(cacheKey, user, CacheTTL.session.user); + if (user.organization_id) { + void trackSessionActivity( + user.id, + user.organization_id, + stewardToken.value, + ); + } + return user; } - return user; + return null; } - return null; + // Steward token invalid, fall through to Privy check } - // Steward token invalid, fall through to Privy check - } - logger.debug("[AUTH] Cache miss, verifying with Privy (cached)"); + logger.debug("[AUTH] Cache miss, verifying with Privy (cached)"); - // Verify the token with Privy using cached verification - // This caches the Privy API response to avoid repeated network calls - const verifiedClaims = await verifyAuthTokenCached(authToken.value); + // Verify the token with Privy using cached verification + // This caches the Privy API response to avoid repeated network calls + const verifiedClaims = await verifyAuthTokenCached(authToken.value); - if (!verifiedClaims) { - return null; - } + if (!verifiedClaims) { + return null; + } - // Get user from database by Privy ID - let user = await usersService.getByPrivyId(verifiedClaims.userId); - - // Just-in-time sync: If user doesn't exist, fetch from Privy and create - // This handles race conditions where webhooks haven't fired yet - if (!user) { - logger.info("[AUTH] User not in DB, starting JIT sync for:", verifiedClaims.userId); - - try { - let privyUser = null; - - // Try efficient method first: use privy-id-token to avoid rate limits - const idToken = cookieStore.get("privy-id-token"); - if (idToken?.value) { - logger.debug("[AUTH] Using privy-id-token for user lookup"); - try { - privyUser = await getUserFromIdToken(idToken.value); - } catch (_idTokenError) { - logger.warn("[AUTH] privy-id-token method failed, will fallback to userId"); + // Get user from database by Privy ID + let user = await usersService.getByPrivyId(verifiedClaims.userId); + + // Just-in-time sync: If user doesn't exist, fetch from Privy and create + // This handles race conditions where webhooks haven't fired yet + if (!user) { + logger.info( + "[AUTH] User not in DB, starting JIT sync for:", + verifiedClaims.userId, + ); + + try { + let privyUser = null; + + // Try efficient method first: use privy-id-token to avoid rate limits + const idToken = cookieStore.get("privy-id-token"); + if (idToken?.value) { + logger.debug("[AUTH] Using privy-id-token for user lookup"); + try { + privyUser = await getUserFromIdToken(idToken.value); + } catch (_idTokenError) { + logger.warn( + "[AUTH] privy-id-token method failed, will fallback to userId", + ); + } } - } - // Fallback: use userId directly (counts against rate limits) - if (!privyUser) { - logger.debug("[AUTH] Using userId for user lookup (fallback)"); - privyUser = await getUserById(verifiedClaims.userId); - } + // Fallback: use userId directly (counts against rate limits) + if (!privyUser) { + logger.debug("[AUTH] Using userId for user lookup (fallback)"); + privyUser = await getUserById(verifiedClaims.userId); + } - if (privyUser) { - const { syncUserFromPrivy } = await loadPrivySyncModule(); - user = await syncUserFromPrivy(privyUser); - logger.info("[AUTH] ✓ JIT sync complete:", { - userId: user.id, - orgId: user.organization_id, - }); - } else { - logger.error("[AUTH] ✗ Privy returned null for user"); + if (privyUser) { + const { syncUserFromPrivy } = await loadPrivySyncModule(); + user = await syncUserFromPrivy(privyUser); + logger.info("[AUTH] ✓ JIT sync complete:", { + userId: user.id, + orgId: user.organization_id, + }); + } else { + logger.error("[AUTH] ✗ Privy returned null for user"); + } + } catch (privyError) { + logger.error( + "[AUTH] ✗ Failed to fetch user from Privy:", + privyError instanceof Error ? privyError.message : privyError, + ); } - } catch (privyError) { - logger.error( - "[AUTH] ✗ Failed to fetch user from Privy:", - privyError instanceof Error ? privyError.message : privyError, - ); } - } - if (!user) { - return null; - } + if (!user) { + return null; + } - // Cache the user data in Redis (5 min TTL) - await redisCache.set(cacheKey, user, CacheTTL.session.user); - logger.debug("[AUTH] Cached user session data"); + // Cache the user data in Redis (5 min TTL) + await redisCache.set(cacheKey, user, CacheTTL.session.user); + logger.debug("[AUTH] Cached user session data"); - // Handle session tracking and API key in background (non-blocking) - if (user.organization_id) { - void trackSessionActivity(user.id, user.organization_id, authToken.value); - void ensureUserHasApiKey(user.id, user.organization_id); - } else { - logger.error("[AUTH] ✗ User missing organization_id:", user.id); - } + // Handle session tracking and API key in background (non-blocking) + if (user.organization_id) { + void trackSessionActivity( + user.id, + user.organization_id, + authToken.value, + ); + void ensureUserHasApiKey(user.id, user.organization_id); + } else { + logger.error("[AUTH] ✗ User missing organization_id:", user.id); + } - return user; - } catch (error) { - logger.error("[AUTH] ✗ Error:", error instanceof Error ? error.message : error); - if (error instanceof Error && error.cause) { + return user; + } catch (error) { logger.error( - "[AUTH] ✗ Root cause:", - error.cause instanceof Error ? error.cause.message : error.cause, + "[AUTH] ✗ Error:", + error instanceof Error ? error.message : error, ); + if (error instanceof Error && error.cause) { + logger.error( + "[AUTH] ✗ Root cause:", + error.cause instanceof Error ? error.cause.message : error.cause, + ); + } + return null; } - return null; - } -}); + }, +); /** * Track session activity in background (non-blocking, debounced) @@ -413,7 +460,9 @@ export async function requireAuthWithOrg(): Promise< } if (!user.organization_id) { - throw new ForbiddenError("This feature requires a full account. Please sign up to continue."); + throw new ForbiddenError( + "This feature requires a full account. Please sign up to continue.", + ); } if (!user.organization || !user.organization?.is_active) { @@ -432,11 +481,15 @@ export const requireSessionAuthWithOrg = requireAuthWithOrg; /** * Require user to belong to a specific organization */ -export async function requireOrganization(organizationId: string): Promise { +export async function requireOrganization( + organizationId: string, +): Promise { const user = await requireAuth(); if (user.organization_id !== organizationId) { - throw new ForbiddenError(`User does not have access to organization ${organizationId}`); + throw new ForbiddenError( + `User does not have access to organization ${organizationId}`, + ); } if (!user.organization?.is_active) { @@ -449,7 +502,9 @@ export async function requireOrganization(organizationId: string): Promise { +export async function requireRole( + allowedRoles: string[], +): Promise { const user = await requireAuth(); if (!allowedRoles.includes(user.role)) { @@ -465,7 +520,9 @@ export async function requireRole(allowedRoles: string[]): Promise { +async function validateAndGetApiKeyUser( + apiKey: ApiKey, +): Promise<{ user: UserWithOrganization }> { if (!apiKey.is_active) { throw new ForbiddenError("API key is inactive"); } @@ -507,7 +564,9 @@ function looksLikeJwt(token: string): boolean { * Allows anonymous users when the resolved user has no org requirement. For org-scoped billing or * resources, use `requireAuthOrApiKeyWithOrg`. */ -export async function requireAuthOrApiKey(request: NextRequest): Promise { +export async function requireAuthOrApiKey( + request: NextRequest, +): Promise { // Try wallet signature authentication first (if headers present) const hasWalletHeaders = request.headers.get("X-Wallet-Address") && @@ -529,7 +588,10 @@ export async function requireAuthOrApiKey(request: NextRequest): Promise { +export async function requireAdmin( + request: NextRequest, +): Promise { const { user } = await requireAuthOrApiKeyWithOrg(request); if (!user.wallet_address) { - throw new AuthenticationError("Wallet connection required for admin access"); + throw new AuthenticationError( + "Wallet connection required for admin access", + ); } const isAdmin = await adminService.isAdmin(user.wallet_address); diff --git a/packages/lib/auth/internal-api.ts b/packages/lib/auth/internal-api.ts index e8b9299de..bdaaac0cc 100644 --- a/packages/lib/auth/internal-api.ts +++ b/packages/lib/auth/internal-api.ts @@ -7,7 +7,11 @@ import { NextRequest, NextResponse } from "next/server"; import { isJWKSConfigured } from "./jwks"; -import { extractBearerToken, type InternalJWTPayload, verifyInternalToken } from "./jwt-internal"; +import { + extractBearerToken, + type InternalJWTPayload, + verifyInternalToken, +} from "./jwt-internal"; // Log config issues once at startup, not on every request if (!isJWKSConfigured() && process.env.NODE_ENV !== "test") { @@ -67,9 +71,16 @@ export async function validateInternalJWTAsync( * The auth result is passed to the handler for access to pod identity. */ export function withInternalAuth( - handler: (request: NextRequest, auth: InternalAuthResult, ...args: unknown[]) => Promise, + handler: ( + request: NextRequest, + auth: InternalAuthResult, + ...args: unknown[] + ) => Promise, ) { - return async (request: NextRequest, ...args: unknown[]): Promise => { + return async ( + request: NextRequest, + ...args: unknown[] + ): Promise => { const authResult = await validateInternalJWTAsync(request); if (authResult instanceof NextResponse) { return authResult; diff --git a/packages/lib/auth/jwks.ts b/packages/lib/auth/jwks.ts index 18085c45e..ec3e4caff 100644 --- a/packages/lib/auth/jwks.ts +++ b/packages/lib/auth/jwks.ts @@ -5,7 +5,13 @@ * Supports key rotation by allowing multiple active keys identified by "kid". */ -import { exportJWK, importPKCS8, importSPKI, type JWK, type KeyLike } from "jose"; +import { + exportJWK, + importPKCS8, + importSPKI, + type JWK, + type KeyLike, +} from "jose"; /** * Environment variables for JWT signing keys. @@ -26,10 +32,14 @@ let cachedPublicKey: KeyLike | null = null; // Log configuration issues once at startup if (!JWT_SIGNING_PRIVATE_KEY && process.env.NODE_ENV !== "test") { - console.error("[CRITICAL] JWT_SIGNING_PRIVATE_KEY not configured - JWT signing will fail"); + console.error( + "[CRITICAL] JWT_SIGNING_PRIVATE_KEY not configured - JWT signing will fail", + ); } if (!JWT_SIGNING_PUBLIC_KEY && process.env.NODE_ENV !== "test") { - console.error("[CRITICAL] JWT_SIGNING_PUBLIC_KEY not configured - JWT verification will fail"); + console.error( + "[CRITICAL] JWT_SIGNING_PUBLIC_KEY not configured - JWT verification will fail", + ); } /** diff --git a/packages/lib/auth/jwt-internal.ts b/packages/lib/auth/jwt-internal.ts index 43270ecab..e90d34d45 100644 --- a/packages/lib/auth/jwt-internal.ts +++ b/packages/lib/auth/jwt-internal.ts @@ -109,7 +109,9 @@ export async function signInternalToken(options: SignTokenOptions): Promise<{ * @returns Verification result with payload if valid * @throws Error if token is invalid, expired, or has wrong issuer/audience */ -export async function verifyInternalToken(token: string): Promise { +export async function verifyInternalToken( + token: string, +): Promise { const publicKey = await getPublicKey(); const { payload } = await jwtVerify(token, publicKey, { diff --git a/packages/lib/auth/playwright-test-session.ts b/packages/lib/auth/playwright-test-session.ts index 441a49743..54f2d37d5 100644 --- a/packages/lib/auth/playwright-test-session.ts +++ b/packages/lib/auth/playwright-test-session.ts @@ -9,11 +9,15 @@ export type PlaywrightTestSessionClaims = { exp: number; }; -export function isPlaywrightTestAuthEnabled(env: NodeJS.ProcessEnv = process.env): boolean { +export function isPlaywrightTestAuthEnabled( + env: NodeJS.ProcessEnv = process.env, +): boolean { return env.PLAYWRIGHT_TEST_AUTH === "true"; } -function getPlaywrightTestAuthSecret(env: NodeJS.ProcessEnv = process.env): string | null { +function getPlaywrightTestAuthSecret( + env: NodeJS.ProcessEnv = process.env, +): string | null { const secret = env.PLAYWRIGHT_TEST_AUTH_SECRET?.trim(); return secret && secret.length >= 16 ? secret : null; } @@ -34,7 +38,10 @@ export function createPlaywrightTestSessionToken( exp: Math.floor(Date.now() / 1000) + PLAYWRIGHT_TEST_SESSION_TTL_SECONDS, }; const payload = Buffer.from(JSON.stringify(claims)).toString("base64url"); - const signature = crypto.createHmac("sha256", secret).update(payload).digest("base64url"); + const signature = crypto + .createHmac("sha256", secret) + .update(payload) + .digest("base64url"); return `${payload}.${signature}`; } @@ -52,7 +59,10 @@ export function verifyPlaywrightTestSessionToken( return null; } - const expectedSignature = crypto.createHmac("sha256", secret).update(payload).digest(); + const expectedSignature = crypto + .createHmac("sha256", secret) + .update(payload) + .digest(); const receivedSignature = Buffer.from(signature, "base64url"); if ( @@ -63,9 +73,9 @@ export function verifyPlaywrightTestSessionToken( } try { - const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as - | PlaywrightTestSessionClaims - | undefined; + const claims = JSON.parse( + Buffer.from(payload, "base64url").toString("utf8"), + ) as PlaywrightTestSessionClaims | undefined; if (!claims?.userId || !claims.organizationId || !claims.exp) { return null; diff --git a/packages/lib/auth/privy-client.ts b/packages/lib/auth/privy-client.ts index a66e9f893..da8f6ecdb 100644 --- a/packages/lib/auth/privy-client.ts +++ b/packages/lib/auth/privy-client.ts @@ -79,7 +79,10 @@ function hashToken(token: string): string { * Eliminates Redis round-trip for repeated requests from the same user * within the same serverless function instance. This saves ~5-30ms per request. */ -const IN_MEMORY_PRIVY_CACHE = new InMemoryLRUCache(200, 30_000); +const IN_MEMORY_PRIVY_CACHE = new InMemoryLRUCache( + 200, + 30_000, +); /** * Verify a Privy auth token with caching @@ -92,7 +95,9 @@ const IN_MEMORY_PRIVY_CACHE = new InMemoryLRUCache(200, 30_000) * @param token - The Privy auth token from cookies or Authorization header * @returns Verified claims or null if invalid/expired */ -export async function verifyAuthTokenCached(token: string): Promise { +export async function verifyAuthTokenCached( + token: string, +): Promise { const tokenHash = hashToken(token); const cacheKey = CacheKeys.session.privy(tokenHash); @@ -104,10 +109,13 @@ export async function verifyAuthTokenCached(token: string): Promise now) { - logger.debug("[PrivyClient] ✓ In-memory cache hit for token verification", { - tokenHash: tokenHash.substring(0, 8), - durationMs: Date.now() - startTime, - }); + logger.debug( + "[PrivyClient] ✓ In-memory cache hit for token verification", + { + tokenHash: tokenHash.substring(0, 8), + durationMs: Date.now() - startTime, + }, + ); return inMemoryCached; } } @@ -164,7 +172,10 @@ export async function verifyAuthTokenCached(token: string): Promise 0) { const cachedClaims: CachedPrivyClaims = { diff --git a/packages/lib/auth/service-jwt.ts b/packages/lib/auth/service-jwt.ts index 2cab8126c..e7023a1c2 100644 --- a/packages/lib/auth/service-jwt.ts +++ b/packages/lib/auth/service-jwt.ts @@ -39,7 +39,9 @@ export async function verifyServiceJwt( const secret = getSecret(); if (!secret) return null; - const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader; + const token = authHeader.startsWith("Bearer ") + ? authHeader.slice(7) + : authHeader; if (!token) return null; try { diff --git a/packages/lib/auth/service-key.ts b/packages/lib/auth/service-key.ts index a1a9c8d04..5278262d7 100644 --- a/packages/lib/auth/service-key.ts +++ b/packages/lib/auth/service-key.ts @@ -26,7 +26,9 @@ export interface ServiceKeyIdentity { * Returns null when the header is missing or invalid. * Throws when env vars are misconfigured. */ -export function validateServiceKey(request: NextRequest): ServiceKeyIdentity | null { +export function validateServiceKey( + request: NextRequest, +): ServiceKeyIdentity | null { const header = request.headers.get("X-Service-Key"); if (!header || header.trim().length === 0) { return null; @@ -34,7 +36,9 @@ export function validateServiceKey(request: NextRequest): ServiceKeyIdentity | n const expectedKey = process.env.WAIFU_SERVICE_KEY; if (!expectedKey || expectedKey.trim().length === 0) { - logger.warn("[service-key] WAIFU_SERVICE_KEY is not configured — rejecting service key auth"); + logger.warn( + "[service-key] WAIFU_SERVICE_KEY is not configured — rejecting service key auth", + ); return null; } @@ -42,8 +46,14 @@ export function validateServiceKey(request: NextRequest): ServiceKeyIdentity | n // Comparing digests instead of raw values avoids leaking the // expected key's length through early-exit on size mismatch. const hmacKey = crypto.randomBytes(32); - const headerDigest = crypto.createHmac("sha256", hmacKey).update(header).digest(); - const expectedDigest = crypto.createHmac("sha256", hmacKey).update(expectedKey).digest(); + const headerDigest = crypto + .createHmac("sha256", hmacKey) + .update(header) + .digest(); + const expectedDigest = crypto + .createHmac("sha256", hmacKey) + .update(expectedKey) + .digest(); if (!crypto.timingSafeEqual(headerDigest, expectedDigest)) { logger.warn("[service-key] Invalid service key presented"); diff --git a/packages/lib/auth/steward-client.ts b/packages/lib/auth/steward-client.ts index 6f331745f..8680130df 100644 --- a/packages/lib/auth/steward-client.ts +++ b/packages/lib/auth/steward-client.ts @@ -63,10 +63,13 @@ let _jwtSecret: Uint8Array | null = null; function getJwtSecret(): Uint8Array | null { if (_jwtSecret) return _jwtSecret; - const raw = process.env.STEWARD_SESSION_SECRET || process.env.STEWARD_JWT_SECRET || ""; + const raw = + process.env.STEWARD_SESSION_SECRET || process.env.STEWARD_JWT_SECRET || ""; if (!raw) { - logger.warn("[StewardClient] No STEWARD_SESSION_SECRET or STEWARD_JWT_SECRET configured"); + logger.warn( + "[StewardClient] No STEWARD_SESSION_SECRET or STEWARD_JWT_SECRET configured", + ); return null; } @@ -87,7 +90,10 @@ function hashToken(token: string): string { * Eliminates Redis round-trip for repeated requests within the same * serverless function instance. */ -const IN_MEMORY_STEWARD_CACHE = new InMemoryLRUCache(200, 30_000); +const IN_MEMORY_STEWARD_CACHE = new InMemoryLRUCache( + 200, + 30_000, +); /** * Extract StewardTokenClaims from a raw jose JWTPayload. @@ -114,7 +120,9 @@ function extractClaims(payload: JWTPayload): StewardTokenClaims { * @param token - The Steward JWT from Authorization header * @returns Verified claims or null if invalid/expired/missing secret */ -export async function verifyStewardTokenCached(token: string): Promise { +export async function verifyStewardTokenCached( + token: string, +): Promise { const secret = getJwtSecret(); if (!secret) return null; @@ -181,7 +189,10 @@ export async function verifyStewardTokenCached(token: string): Promise 0) { const cachedClaims: CachedStewardClaims = { @@ -237,7 +248,9 @@ export async function verifyStewardTokenCached(token: string): Promise { +export async function invalidateStewardTokenCache( + token: string, +): Promise { const tokenHash = hashToken(token); IN_MEMORY_STEWARD_CACHE.delete(tokenHash); @@ -247,7 +260,10 @@ export async function invalidateStewardTokenCache(token: string): Promise cache.del(CacheKeys.session.user(tokenHash)), ]); - logger.debug("[StewardClient] ✓ Invalidated token cache (in-memory + Redis)", { - tokenHash: tokenHash.substring(0, 8), - }); + logger.debug( + "[StewardClient] ✓ Invalidated token cache (in-memory + Redis)", + { + tokenHash: tokenHash.substring(0, 8), + }, + ); } diff --git a/packages/lib/auth/waifu-bridge.ts b/packages/lib/auth/waifu-bridge.ts index b15efc8d7..bbf64b30a 100644 --- a/packages/lib/auth/waifu-bridge.ts +++ b/packages/lib/auth/waifu-bridge.ts @@ -12,7 +12,11 @@ import { organizationsService } from "@/lib/services/organizations"; import { usersService } from "@/lib/services/users"; import type { UserWithOrganization } from "@/lib/types"; import { logger } from "@/lib/utils/logger"; -import { isServiceJwtEnabled, type ServiceJwtPayload, verifyServiceJwt } from "./service-jwt"; +import { + isServiceJwtEnabled, + type ServiceJwtPayload, + verifyServiceJwt, +} from "./service-jwt"; export interface WaifuBridgeAuthResult { user: UserWithOrganization & { @@ -81,7 +85,11 @@ function slugFromUserId(userId: string): string { .replace(/[^a-zA-Z0-9-]/g, "-") .toLowerCase() .slice(0, 40); - const hash = crypto.createHash("sha256").update(userId).digest("hex").slice(0, 16); + const hash = crypto + .createHash("sha256") + .update(userId) + .digest("hex") + .slice(0, 16); return `${base}-${hash}`; } @@ -166,10 +174,13 @@ async function resolveServiceUser( orgErr.message.includes("23505")); if (!isConflict) throw orgErr; - logger.info("[waifu-bridge] Concurrent org creation detected, resolving existing org", { - serviceId, - slug, - }); + logger.info( + "[waifu-bridge] Concurrent org creation detected, resolving existing org", + { + serviceId, + slug, + }, + ); // Another request won the race and may have created the user too — // re-check before falling through to user creation. @@ -215,9 +226,12 @@ async function resolveServiceUser( err.message.includes("23505")); if (!isConflict) throw err; - logger.info("[waifu-bridge] Concurrent user creation detected, re-fetching", { - serviceId, - }); + logger.info( + "[waifu-bridge] Concurrent user creation detected, re-fetching", + { + serviceId, + }, + ); const existing = await usersService.getByPrivyId(serviceId); if (existing?.organization_id && existing?.organization) { @@ -225,7 +239,8 @@ async function resolveServiceUser( } // If wallet-based, try that path too if (walletAddr) { - const walletUser = await usersService.getByWalletAddressWithOrganization(walletAddr); + const walletUser = + await usersService.getByWalletAddressWithOrganization(walletAddr); if (walletUser?.organization_id && walletUser?.organization) { return walletUser as WaifuBridgeAuthResult["user"]; } @@ -243,7 +258,9 @@ async function resolveServiceUser( const fullUser = await usersService.getWithOrganization(newUser.id); if (!fullUser?.organization_id || !fullUser?.organization) { - throw new ForbiddenError("Failed to provision service account for waifu-core bridge"); + throw new ForbiddenError( + "Failed to provision service account for waifu-core bridge", + ); } return fullUser as WaifuBridgeAuthResult["user"]; diff --git a/packages/lib/auth/wallet-auth.ts b/packages/lib/auth/wallet-auth.ts index 567cf0a1c..0353a3132 100644 --- a/packages/lib/auth/wallet-auth.ts +++ b/packages/lib/auth/wallet-auth.ts @@ -66,7 +66,11 @@ export async function verifyWalletSignature( } // Atomic SET NX PX: only one concurrent request can claim this nonce; prevents TOCTOU race - const claimed = await cache.setIfNotExists(nonceKey, "used", MAX_TIMESTAMP_AGE_MS); + const claimed = await cache.setIfNotExists( + nonceKey, + "used", + MAX_TIMESTAMP_AGE_MS, + ); if (!claimed) { throw new Error("Signature has already been used"); } diff --git a/packages/lib/blob.ts b/packages/lib/blob.ts index bd09dec0b..f7f1255ff 100644 --- a/packages/lib/blob.ts +++ b/packages/lib/blob.ts @@ -4,7 +4,10 @@ import { del, list, put } from "@vercel/blob"; * Trusted blob storage hosts for URL validation. * Used to prevent SSRF attacks by ensuring URLs point to our storage. */ -export const TRUSTED_BLOB_HOSTS = ["blob.vercel-storage.com", "public.blob.vercel-storage.com"]; +export const TRUSTED_BLOB_HOSTS = [ + "blob.vercel-storage.com", + "public.blob.vercel-storage.com", +]; /** * Validates that a URL points to a trusted blob storage host. @@ -23,7 +26,8 @@ export function isValidBlobUrl(url: string): boolean { // Vercel Blob URLs have random subdomain prefixes (e.g., l5fpqchmvmrcwa0k.public.blob.vercel-storage.com) // Using endsWith is safe because Vercel controls all subdomains of blob.vercel-storage.com return TRUSTED_BLOB_HOSTS.some( - (host) => parsedUrl.hostname === host || parsedUrl.hostname.endsWith(`.${host}`), + (host) => + parsedUrl.hostname === host || parsedUrl.hostname.endsWith(`.${host}`), ); } catch { return false; @@ -89,7 +93,9 @@ export async function uploadToBlob( }); // Calculate size from the content - const size = Buffer.isBuffer(content) ? content.length : Buffer.byteLength(content); + const size = Buffer.isBuffer(content) + ? content.length + : Buffer.byteLength(content); return { url: blob.url, @@ -126,7 +132,8 @@ export async function uploadBase64Image( const MAX_IMAGE_SIZE = maxSizeMB * 1024 * 1024; // Account for base64 padding characters when calculating size const paddingCount = (base64Content.match(/=/g) || []).length; - const estimatedSize = Math.ceil((base64Content.length * 3) / 4) - paddingCount; + const estimatedSize = + Math.ceil((base64Content.length * 3) / 4) - paddingCount; if (estimatedSize > MAX_IMAGE_SIZE) { throw new Error( @@ -135,9 +142,17 @@ export async function uploadBase64Image( } // Validate MIME type - only allow images - const validImageTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"]; + const validImageTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", + ]; if (!validImageTypes.includes(mimeType.toLowerCase())) { - throw new Error(`Invalid image type: ${mimeType}. Allowed: ${validImageTypes.join(", ")}`); + throw new Error( + `Invalid image type: ${mimeType}. Allowed: ${validImageTypes.join(", ")}`, + ); } const buffer = Buffer.from(base64Content, "base64"); @@ -177,7 +192,10 @@ export async function uploadFromBuffer( export function isFalAiUrl(url: string): boolean { try { const urlObj = new URL(url); - return urlObj.hostname.includes("fal.media") || urlObj.hostname.includes("fal.ai"); + return ( + urlObj.hostname.includes("fal.media") || + urlObj.hostname.includes("fal.ai") + ); } catch { return false; } @@ -201,7 +219,8 @@ export async function uploadFromUrl( } const buffer = Buffer.from(await response.arrayBuffer()); - const contentType = options.contentType || response.headers.get("content-type") || undefined; + const contentType = + options.contentType || response.headers.get("content-type") || undefined; return uploadToBlob(buffer, { ...options, @@ -234,7 +253,10 @@ export async function ensureElizaCloudUrl( const result = await uploadFromUrl(sourceUrl, options); return result.url; } catch (error) { - console.error("[ensureElizaCloudUrl] Failed to upload Fal.ai URL to our storage:", error); + console.error( + "[ensureElizaCloudUrl] Failed to upload Fal.ai URL to our storage:", + error, + ); throw error; } } diff --git a/packages/lib/config/affiliate-themes.ts b/packages/lib/config/affiliate-themes.ts index 94ed89cc7..c638c4447 100644 --- a/packages/lib/config/affiliate-themes.ts +++ b/packages/lib/config/affiliate-themes.ts @@ -211,7 +211,9 @@ export const AFFILIATE_THEMES: Record = { * @param affiliateId - The affiliate identifier * @returns The theme configuration */ -export function getAffiliateTheme(affiliateId: string | undefined | null): AffiliateTheme { +export function getAffiliateTheme( + affiliateId: string | undefined | null, +): AffiliateTheme { if (!affiliateId) { return AFFILIATE_THEMES["default"]; } @@ -240,7 +242,9 @@ export function hasAffiliateTheme(affiliateId: string): boolean { * @param theme - The affiliate theme * @returns CSS properties object */ -export function getThemeCSSVariables(theme: AffiliateTheme): React.CSSProperties { +export function getThemeCSSVariables( + theme: AffiliateTheme, +): React.CSSProperties { return { "--theme-primary": theme.colors.primary, "--theme-primary-light": theme.colors.primaryLight, diff --git a/packages/lib/config/crypto.ts b/packages/lib/config/crypto.ts index c12c31500..aa5987d27 100644 --- a/packages/lib/config/crypto.ts +++ b/packages/lib/config/crypto.ts @@ -15,7 +15,9 @@ export const OXAPAY_FEE_PERCENT = new Decimal("1.5"); * Multiplier to convert received amount to what user actually paid. * userPaidAmount = receivedAmount / OXAPAY_FEE_MULTIPLIER */ -export const OXAPAY_FEE_MULTIPLIER = new Decimal(1).minus(OXAPAY_FEE_PERCENT.dividedBy(100)); +export const OXAPAY_FEE_MULTIPLIER = new Decimal(1).minus( + OXAPAY_FEE_PERCENT.dividedBy(100), +); /** * Supported payment currencies for OxaPay. @@ -218,7 +220,10 @@ export const NETWORK_CONFIGS: Record = { * Calculate tolerance threshold for a payment amount. * Uses percentage-based tolerance for consistency across all payment sizes. */ -export function calculateTolerance(amount: Decimal, network: OxaPayNetwork): Decimal { +export function calculateTolerance( + amount: Decimal, + network: OxaPayNetwork, +): Decimal { const config = NETWORK_CONFIGS[network]; const toleranceMultiplier = new Decimal(1).minus( new Decimal(config.tolerancePercent).dividedBy(100), diff --git a/packages/lib/config/deployment-environment.ts b/packages/lib/config/deployment-environment.ts index 8f52bd45c..782497fc5 100644 --- a/packages/lib/config/deployment-environment.ts +++ b/packages/lib/config/deployment-environment.ts @@ -12,8 +12,12 @@ export function isProductionDeployment(env: EnvLike = process.env): boolean { return env.NODE_ENV === "production"; } -export function shouldBlockUnsafeWebhookSkip(env: EnvLike = process.env): boolean { - return env.SKIP_WEBHOOK_VERIFICATION === "true" && isProductionDeployment(env); +export function shouldBlockUnsafeWebhookSkip( + env: EnvLike = process.env, +): boolean { + return ( + env.SKIP_WEBHOOK_VERIFICATION === "true" && isProductionDeployment(env) + ); } export function shouldBlockDevnetBypass(env: EnvLike = process.env): boolean { diff --git a/packages/lib/config/env-validator.ts b/packages/lib/config/env-validator.ts index 8e21f9844..6f09ced31 100644 --- a/packages/lib/config/env-validator.ts +++ b/packages/lib/config/env-validator.ts @@ -152,7 +152,8 @@ const ENV_VARS = { STRIPE_SECRET_KEY: { required: false, description: "Stripe secret key for payments", - validate: (value: string) => value.startsWith("sk_test_") || value.startsWith("sk_live_"), + validate: (value: string) => + value.startsWith("sk_test_") || value.startsWith("sk_live_"), errorMessage: "Must start with 'sk_test_' or 'sk_live_'", }, STRIPE_WEBHOOK_SECRET: { @@ -188,7 +189,8 @@ const ENV_VARS = { // Cron Jobs CRON_SECRET: { required: true, - description: "Secret for authenticating cron job requests (required for production security)", + description: + "Secret for authenticating cron job requests (required for production security)", validate: (value: string) => value.length >= 32, errorMessage: "Must be at least 32 characters for security", }, @@ -196,7 +198,8 @@ const ENV_VARS = { // Solana RPC SOLANA_RPC_PROVIDER_API_KEY: { required: false, - description: "Solana RPC provider API key (enables Solana blockchain access)", + description: + "Solana RPC provider API key (enables Solana blockchain access)", validate: (value: string) => value.trim().length > 0, errorMessage: "Must not be empty", }, @@ -204,7 +207,8 @@ const ENV_VARS = { // Market Data API MARKET_DATA_PROVIDER_API_KEY: { required: false, - description: "Market data API key (enables multi-chain token price and market data)", + description: + "Market data API key (enables multi-chain token price and market data)", validate: (value: string) => value.trim().length > 0, errorMessage: "Must not be empty", }, @@ -212,7 +216,8 @@ const ENV_VARS = { // Alchemy EVM RPC ALCHEMY_API_KEY: { required: false, - description: "Alchemy API key (enables EVM blockchain access via /api/v1/rpc/*)", + description: + "Alchemy API key (enables EVM blockchain access via /api/v1/rpc/*)", validate: (value: string) => value.trim().length > 0, errorMessage: "Must not be empty", }, @@ -255,9 +260,12 @@ export function validateEnvironment(): EnvValidationResult { // Validate format if validator is provided if ("validate" in config && config.validate && !config.validate(value)) { const errorMsg = - "errorMessage" in config && config.errorMessage ? config.errorMessage : "Invalid format"; + "errorMessage" in config && config.errorMessage + ? config.errorMessage + : "Invalid format"; // Treat as error if required OR if failOnInvalid is set (for optional vars that throw at runtime) - const treatAsError = config.required || ("failOnInvalid" in config && config.failOnInvalid); + const treatAsError = + config.required || ("failOnInvalid" in config && config.failOnInvalid); if (treatAsError) { errors.push({ variable, @@ -307,7 +315,9 @@ export function requireValidEnvironment(): void { console.error(` - ${error.message}`); } console.error(""); - console.error("Please check your .env.local file and set the required variables."); + console.error( + "Please check your .env.local file and set the required variables.", + ); console.error("See .env.example for reference."); throw new Error("Invalid environment configuration"); } @@ -349,7 +359,9 @@ export function isFeatureConfigured(feature: string): boolean { process.env.AWS_VPC_ID ); case "stripe": - return !!(process.env.STRIPE_SECRET_KEY && process.env.STRIPE_WEBHOOK_SECRET); + return !!( + process.env.STRIPE_SECRET_KEY && process.env.STRIPE_WEBHOOK_SECRET + ); case "crypto": return !!process.env.OXAPAY_MERCHANT_API_KEY; case "cron": diff --git a/packages/lib/config/feature-flags.ts b/packages/lib/config/feature-flags.ts index 0bf3df158..1f7366921 100644 --- a/packages/lib/config/feature-flags.ts +++ b/packages/lib/config/feature-flags.ts @@ -15,7 +15,10 @@ export interface FeatureFlagConfig { type FeatureFlagsMap = Record; -const parseEnvFlag = (envVar: string | undefined, defaultValue: boolean): boolean => { +const parseEnvFlag = ( + envVar: string | undefined, + defaultValue: boolean, +): boolean => { if (envVar === undefined) return defaultValue; return envVar === "true" || envVar === "1"; }; @@ -27,7 +30,10 @@ export const FEATURE_FLAGS: FeatureFlagsMap = { description: "Model Context Protocol integration and management", }, characterBuilder: { - enabled: parseEnvFlag(process.env.NEXT_PUBLIC_FEATURE_CHARACTER_BUILDER, true), + enabled: parseEnvFlag( + process.env.NEXT_PUBLIC_FEATURE_CHARACTER_BUILDER, + true, + ), name: "Character Builder", description: "Visual character creation and editing tool", }, @@ -63,14 +69,21 @@ export function isFeatureEnabled(flag: FeatureFlag): boolean { } export function getEnabledFeatures(): FeatureFlag[] { - return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter((key) => FEATURE_FLAGS[key].enabled); + return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter( + (key) => FEATURE_FLAGS[key].enabled, + ); } export function getDisabledFeatures(): FeatureFlag[] { - return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter((key) => !FEATURE_FLAGS[key].enabled); + return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter( + (key) => !FEATURE_FLAGS[key].enabled, + ); } -export const FEATURE_ROUTE_MAP: Record = { +export const FEATURE_ROUTE_MAP: Record< + FeatureFlag, + { frontend: string[]; api: string[] } +> = { mcp: { frontend: ["/dashboard/mcps"], api: ["/api/mcp", "/api/v1/mcp"], diff --git a/packages/lib/config/mcp.ts b/packages/lib/config/mcp.ts index 8a36f345f..b11685088 100644 --- a/packages/lib/config/mcp.ts +++ b/packages/lib/config/mcp.ts @@ -6,13 +6,22 @@ /** * Request Timeout Configuration */ -export const MCP_REQUEST_TIMEOUT = Number.parseInt(process.env.MCP_TIMEOUT || "60", 10); -export const SSE_MAX_DURATION = Number.parseInt(process.env.SSE_MAX_DURATION || "300", 10); +export const MCP_REQUEST_TIMEOUT = Number.parseInt( + process.env.MCP_TIMEOUT || "60", + 10, +); +export const SSE_MAX_DURATION = Number.parseInt( + process.env.SSE_MAX_DURATION || "300", + 10, +); /** * SSE (Server-Sent Events) Configuration */ -export const SSE_POLL_INTERVAL_MS = Number.parseInt(process.env.SSE_POLL_INTERVAL_MS || "500", 10); +export const SSE_POLL_INTERVAL_MS = Number.parseInt( + process.env.SSE_POLL_INTERVAL_MS || "500", + 10, +); export const SSE_HEARTBEAT_INTERVAL = Number.parseInt( process.env.SSE_HEARTBEAT_INTERVAL || "30", 10, @@ -31,7 +40,10 @@ export const SSE_BACKOFF_INITIAL_MS = Number.parseInt( process.env.SSE_BACKOFF_INITIAL_MS || "500", 10, ); -export const SSE_BACKOFF_MAX_MS = Number.parseInt(process.env.SSE_BACKOFF_MAX_MS || "5000", 10); +export const SSE_BACKOFF_MAX_MS = Number.parseInt( + process.env.SSE_BACKOFF_MAX_MS || "5000", + 10, +); export const SSE_BACKOFF_MULTIPLIER = Number.parseFloat( process.env.SSE_BACKOFF_MULTIPLIER || "1.5", ); @@ -70,4 +82,5 @@ export const MCP_EVENT_TYPES = { CONTAINER: "container", } as const; -export type MCPEventType = (typeof MCP_EVENT_TYPES)[keyof typeof MCP_EVENT_TYPES]; +export type MCPEventType = + (typeof MCP_EVENT_TYPES)[keyof typeof MCP_EVENT_TYPES]; diff --git a/packages/lib/config/payout-networks.ts b/packages/lib/config/payout-networks.ts index 1b8cce2d8..b439abeb7 100644 --- a/packages/lib/config/payout-networks.ts +++ b/packages/lib/config/payout-networks.ts @@ -10,7 +10,14 @@ */ import type { Chain } from "viem"; -import { base, baseSepolia, bsc, bscTestnet, mainnet, sepolia } from "viem/chains"; +import { + base, + baseSepolia, + bsc, + bscTestnet, + mainnet, + sepolia, +} from "viem/chains"; import { logger } from "@/lib/utils/logger"; // ============================================================================ @@ -18,7 +25,11 @@ import { logger } from "@/lib/utils/logger"; // ============================================================================ export type MainnetNetwork = "ethereum" | "base" | "bnb" | "solana"; -export type TestnetNetwork = "ethereum-sepolia" | "base-sepolia" | "bnb-testnet" | "solana-devnet"; +export type TestnetNetwork = + | "ethereum-sepolia" + | "base-sepolia" + | "bnb-testnet" + | "solana-devnet"; export type PayoutNetwork = MainnetNetwork | TestnetNetwork; // ============================================================================ @@ -62,12 +73,16 @@ export const ELIZA_TOKEN_ADDRESSES: Record = { // Testnet - Use test tokens or deploy your own for testing // These are placeholder addresses - deploy test ERC20 for actual testing "ethereum-sepolia": - process.env.ELIZA_TOKEN_SEPOLIA || "0x0000000000000000000000000000000000000000", + process.env.ELIZA_TOKEN_SEPOLIA || + "0x0000000000000000000000000000000000000000", "base-sepolia": - process.env.ELIZA_TOKEN_BASE_SEPOLIA || "0x0000000000000000000000000000000000000000", + process.env.ELIZA_TOKEN_BASE_SEPOLIA || + "0x0000000000000000000000000000000000000000", "bnb-testnet": - process.env.ELIZA_TOKEN_BNB_TESTNET || "0x0000000000000000000000000000000000000000", - "solana-devnet": process.env.ELIZA_TOKEN_SOLANA_DEVNET || "11111111111111111111111111111111", // Placeholder + process.env.ELIZA_TOKEN_BNB_TESTNET || + "0x0000000000000000000000000000000000000000", + "solana-devnet": + process.env.ELIZA_TOKEN_SOLANA_DEVNET || "11111111111111111111111111111111", // Placeholder }; // ============================================================================ @@ -176,7 +191,9 @@ export const NETWORK_CONFIGS: Record = { chainId: 97, chain: bscTestnet, isTestnet: true, - rpcUrl: process.env.BNB_TESTNET_RPC_URL || "https://data-seed-prebsc-1-s1.binance.org:8545", + rpcUrl: + process.env.BNB_TESTNET_RPC_URL || + "https://data-seed-prebsc-1-s1.binance.org:8545", blockExplorer: "https://testnet.bscscan.com", tokenAddress: ELIZA_TOKEN_ADDRESSES["bnb-testnet"], tokenDecimals: 9, // Match mainnet decimals @@ -191,7 +208,8 @@ export const NETWORK_CONFIGS: Record = { chainId: 0, chain: null, isTestnet: true, - rpcUrl: process.env.SOLANA_DEVNET_RPC_URL || "https://api.devnet.solana.com", + rpcUrl: + process.env.SOLANA_DEVNET_RPC_URL || "https://api.devnet.solana.com", blockExplorer: "https://solscan.io?cluster=devnet", tokenAddress: ELIZA_TOKEN_ADDRESSES["solana-devnet"], tokenDecimals: 9, @@ -236,7 +254,9 @@ export function getAvailableNetworks(): PayoutNetwork[] { /** * Map a mainnet network to its testnet equivalent */ -export function getTestnetEquivalent(mainnetNetwork: MainnetNetwork): TestnetNetwork { +export function getTestnetEquivalent( + mainnetNetwork: MainnetNetwork, +): TestnetNetwork { const mapping: Record = { ethereum: "ethereum-sepolia", base: "base-sepolia", @@ -249,7 +269,9 @@ export function getTestnetEquivalent(mainnetNetwork: MainnetNetwork): TestnetNet /** * Map a testnet network to its mainnet equivalent */ -export function getMainnetEquivalent(testnetNetwork: TestnetNetwork): MainnetNetwork { +export function getMainnetEquivalent( + testnetNetwork: TestnetNetwork, +): MainnetNetwork { const mapping: Record = { "ethereum-sepolia": "ethereum", "base-sepolia": "base", @@ -273,7 +295,9 @@ export function resolveNetwork(network: PayoutNetwork): PayoutNetwork { // If we're in mainnet mode and given a testnet network, warn but allow if (!isTestnetMode() && config.isTestnet) { - logger.warn(`[Payout] Warning: Using testnet network ${network} in mainnet mode`); + logger.warn( + `[Payout] Warning: Using testnet network ${network} in mainnet mode`, + ); } return network; @@ -301,12 +325,18 @@ export function isNetworkConfigured(network: PayoutNetwork): boolean { // Check wallet is configured if (config.chain) { // EVM network - if (!process.env.EVM_PAYOUT_PRIVATE_KEY && !process.env.EVM_PAYOUT_WALLET_ADDRESS) { + if ( + !process.env.EVM_PAYOUT_PRIVATE_KEY && + !process.env.EVM_PAYOUT_WALLET_ADDRESS + ) { return false; } } else { // Solana - if (!process.env.SOLANA_PAYOUT_PRIVATE_KEY && !process.env.SOLANA_PAYOUT_WALLET_ADDRESS) { + if ( + !process.env.SOLANA_PAYOUT_PRIVATE_KEY && + !process.env.SOLANA_PAYOUT_WALLET_ADDRESS + ) { return false; } } @@ -335,7 +365,9 @@ export function isValidNetwork(network: string): network is PayoutNetwork { /** * Assert a network is valid or throw */ -export function assertValidNetwork(network: string): asserts network is PayoutNetwork { +export function assertValidNetwork( + network: string, +): asserts network is PayoutNetwork { if (!isValidNetwork(network)) { throw new Error( `Invalid network: ${network}. Valid networks: ${Object.keys(NETWORK_CONFIGS).join(", ")}`, @@ -357,7 +389,10 @@ export function getNetworkDisplayName(network: PayoutNetwork): string { /** * Get block explorer URL for a transaction */ -export function getExplorerTxUrl(network: PayoutNetwork, txHash: string): string { +export function getExplorerTxUrl( + network: PayoutNetwork, + txHash: string, +): string { const config = NETWORK_CONFIGS[network]; if (config.chain) { return `${config.blockExplorer}/tx/${txHash}`; @@ -370,7 +405,10 @@ export function getExplorerTxUrl(network: PayoutNetwork, txHash: string): string /** * Get block explorer URL for an address */ -export function getExplorerAddressUrl(network: PayoutNetwork, address: string): string { +export function getExplorerAddressUrl( + network: PayoutNetwork, + address: string, +): string { const config = NETWORK_CONFIGS[network]; if (config.chain) { return `${config.blockExplorer}/address/${address}`; diff --git a/packages/lib/config/redemption-addresses.ts b/packages/lib/config/redemption-addresses.ts index 8829af2e4..44b8e85f0 100644 --- a/packages/lib/config/redemption-addresses.ts +++ b/packages/lib/config/redemption-addresses.ts @@ -24,7 +24,10 @@ import type { SupportedNetwork } from "@/lib/services/eliza-token-price"; * Known exchange hot wallet addresses by network. * These are frequently-used deposit addresses that may reject unexpected tokens. */ -export const KNOWN_EXCHANGE_ADDRESSES: Record> = { +export const KNOWN_EXCHANGE_ADDRESSES: Record< + SupportedNetwork, + Record +> = { ethereum: { // Coinbase "0x71660c4005ba85c37ccec55d0c4493e66fe775d3": "Coinbase", @@ -121,7 +124,10 @@ export interface AddressCheckResult { /** * Check if an address is a known exchange or custodial wallet. */ -export function checkKnownAddress(address: string, network: SupportedNetwork): AddressCheckResult { +export function checkKnownAddress( + address: string, + network: SupportedNetwork, +): AddressCheckResult { const normalizedAddress = address.toLowerCase(); const networkAddresses = KNOWN_EXCHANGE_ADDRESSES[network]; diff --git a/packages/lib/config/redemption-security.ts b/packages/lib/config/redemption-security.ts index 351896f4e..cc4eedec7 100644 --- a/packages/lib/config/redemption-security.ts +++ b/packages/lib/config/redemption-security.ts @@ -180,7 +180,10 @@ export const MONITORING = { * @param twapPrice - The TWAP price of elizaOS in USD * @returns The elizaOS tokens the user will receive */ -export function calculateEffectiveTokens(usdValue: number, twapPrice: number): number { +export function calculateEffectiveTokens( + usdValue: number, + twapPrice: number, +): number { // Apply safety spread (user gets slightly less than theoretical) const effectiveUsd = usdValue * (1 - ARBITRAGE_PROTECTION.SAFETY_SPREAD); return effectiveUsd / twapPrice; diff --git a/packages/lib/config/wallet-provider-flags.ts b/packages/lib/config/wallet-provider-flags.ts index 25cf0dd88..d018fe009 100644 --- a/packages/lib/config/wallet-provider-flags.ts +++ b/packages/lib/config/wallet-provider-flags.ts @@ -9,7 +9,8 @@ */ export const WALLET_PROVIDER_FLAGS = { /** When true, new agent wallets are created via Steward instead of Privy. */ - USE_STEWARD_FOR_NEW_WALLETS: process.env.USE_STEWARD_FOR_NEW_WALLETS === "true", + USE_STEWARD_FOR_NEW_WALLETS: + process.env.USE_STEWARD_FOR_NEW_WALLETS === "true", /** When true, the migration script is allowed to convert Privy wallets to Steward. */ ALLOW_PRIVY_MIGRATION: process.env.ALLOW_PRIVY_MIGRATION === "true", diff --git a/packages/lib/constants/agent-flavors.ts b/packages/lib/constants/agent-flavors.ts index 35e723276..4e65c0a1b 100644 --- a/packages/lib/constants/agent-flavors.ts +++ b/packages/lib/constants/agent-flavors.ts @@ -20,7 +20,8 @@ export const AGENT_FLAVORS: AgentFlavor[] = [ { id: "milady", name: "Milady", - description: "Full milady agent with Steward wallet vault integration and VRM companion UI", + description: + "Full milady agent with Steward wallet vault integration and VRM companion UI", dockerImage: DEFAULT_MILADY_IMAGE, }, { diff --git a/packages/lib/constants/character-categories.ts b/packages/lib/constants/character-categories.ts index c313f4a27..1c45b6b5f 100644 --- a/packages/lib/constants/character-categories.ts +++ b/packages/lib/constants/character-categories.ts @@ -26,7 +26,10 @@ export interface CategoryDefinition { color: string; } -export const CHARACTER_CATEGORIES: Record, CategoryDefinition> = { +export const CHARACTER_CATEGORIES: Record< + Uppercase, + CategoryDefinition +> = { ASSISTANT: { id: "assistant", name: "Assistants", @@ -102,7 +105,9 @@ export const CATEGORY_ORDER: Array = [ * @param id - Category ID. * @returns Category definition or undefined if not found. */ -export function getCategoryById(id: CategoryId): CategoryDefinition | undefined { +export function getCategoryById( + id: CategoryId, +): CategoryDefinition | undefined { const key = id.toUpperCase() as keyof typeof CHARACTER_CATEGORIES; return CHARACTER_CATEGORIES[key]; } diff --git a/packages/lib/constants/milady-pricing-display.ts b/packages/lib/constants/milady-pricing-display.ts index 6b340321d..546a96c3f 100644 --- a/packages/lib/constants/milady-pricing-display.ts +++ b/packages/lib/constants/milady-pricing-display.ts @@ -12,7 +12,8 @@ export const MONTHLY_RUNNING_COST = Math.round(MILADY_PRICING.RUNNING_HOURLY_RATE * 24 * 30 * 100) / 100; // ~$7.20 /** Monthly cost for an idle agent. */ -export const MONTHLY_IDLE_COST = Math.round(MILADY_PRICING.IDLE_HOURLY_RATE * 24 * 30 * 100) / 100; // ~$1.80 +export const MONTHLY_IDLE_COST = + Math.round(MILADY_PRICING.IDLE_HOURLY_RATE * 24 * 30 * 100) / 100; // ~$1.80 // ── Formatting helpers ────────────────────────────────────────── @@ -66,7 +67,10 @@ export function formatDuration(hours: number): string { /** * Calculate credit pack savings percentage. */ -export function packSavingsPercent(priceCents: number, credits: number): number { +export function packSavingsPercent( + priceCents: number, + credits: number, +): number { const price = priceCents / 100; if (credits <= 0 || price >= credits) return 0; return Math.round(((credits - price) / credits) * 100); diff --git a/packages/lib/constants/pricing.ts b/packages/lib/constants/pricing.ts index 22dcdf307..b72a71d43 100644 --- a/packages/lib/constants/pricing.ts +++ b/packages/lib/constants/pricing.ts @@ -30,27 +30,48 @@ const BASE_CONTAINER_PRICING = { export const CONTAINER_PRICING = { // One-time costs (with 20% markup) DEPLOYMENT: - Math.round(BASE_CONTAINER_PRICING.DEPLOYMENT * PLATFORM_MARKUP_MULTIPLIER * 100) / 100, // $0.50 per deployment + Math.round( + BASE_CONTAINER_PRICING.DEPLOYMENT * PLATFORM_MARKUP_MULTIPLIER * 100, + ) / 100, // $0.50 per deployment IMAGE_UPLOAD: - Math.round(BASE_CONTAINER_PRICING.IMAGE_UPLOAD * PLATFORM_MARKUP_MULTIPLIER * 100) / 100, // $0.25 per image upload + Math.round( + BASE_CONTAINER_PRICING.IMAGE_UPLOAD * PLATFORM_MARKUP_MULTIPLIER * 100, + ) / 100, // $0.25 per image upload // Recurring costs - DAILY BILLING (with 20% markup) MONTHLY_BASE_COST: - Math.round(BASE_CONTAINER_PRICING.MONTHLY_BASE_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100, // $20/month + Math.round( + BASE_CONTAINER_PRICING.MONTHLY_BASE_COST * + PLATFORM_MARKUP_MULTIPLIER * + 100, + ) / 100, // $20/month DAILY_RUNNING_COST: - Math.round(BASE_CONTAINER_PRICING.DAILY_RUNNING_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100, // $0.67/day per container + Math.round( + BASE_CONTAINER_PRICING.DAILY_RUNNING_COST * + PLATFORM_MARKUP_MULTIPLIER * + 100, + ) / 100, // $0.67/day per container // Resource-based costs (with 20% markup) COST_PER_GB_STORAGE: - Math.round(BASE_CONTAINER_PRICING.COST_PER_GB_STORAGE * PLATFORM_MARKUP_MULTIPLIER * 100) / 100, // $0.10/GB/month + Math.round( + BASE_CONTAINER_PRICING.COST_PER_GB_STORAGE * + PLATFORM_MARKUP_MULTIPLIER * + 100, + ) / 100, // $0.10/GB/month COST_PER_GB_BANDWIDTH: - Math.round(BASE_CONTAINER_PRICING.COST_PER_GB_BANDWIDTH * PLATFORM_MARKUP_MULTIPLIER * 100) / - 100, // $0.05/GB outbound + Math.round( + BASE_CONTAINER_PRICING.COST_PER_GB_BANDWIDTH * + PLATFORM_MARKUP_MULTIPLIER * + 100, + ) / 100, // $0.05/GB outbound // Scaling costs (with 20% markup) COST_PER_ADDITIONAL_INSTANCE: Math.round( - BASE_CONTAINER_PRICING.COST_PER_ADDITIONAL_INSTANCE * PLATFORM_MARKUP_MULTIPLIER * 100, + BASE_CONTAINER_PRICING.COST_PER_ADDITIONAL_INSTANCE * + PLATFORM_MARKUP_MULTIPLIER * + 100, ) / 100, // $0.05 per instance per hour // Warning thresholds (not pricing, keep as-is) @@ -158,7 +179,8 @@ export function calculateDeploymentCost(config: { // Additional cost for scaling beyond single instance if (instanceCount > 1) { - totalCost += (instanceCount - 1) * CONTAINER_PRICING.COST_PER_ADDITIONAL_INSTANCE; + totalCost += + (instanceCount - 1) * CONTAINER_PRICING.COST_PER_ADDITIONAL_INSTANCE; } // Additional cost for higher CPU/memory allocations diff --git a/packages/lib/constants/sandbox-status.ts b/packages/lib/constants/sandbox-status.ts index b319a06d2..aba5739f2 100644 --- a/packages/lib/constants/sandbox-status.ts +++ b/packages/lib/constants/sandbox-status.ts @@ -23,7 +23,9 @@ export function statusDotColor(status: string): string { } export function statusBadgeColor(status: string): string { - return STATUS_BADGE_COLORS[status] ?? "bg-white/5 text-white/40 border-white/10"; + return ( + STATUS_BADGE_COLORS[status] ?? "bg-white/5 text-white/40 border-white/10" + ); } /** Format a date into a human-readable relative time string. */ diff --git a/packages/lib/debug/collector.ts b/packages/lib/debug/collector.ts index d714501bf..71e1d54d1 100644 --- a/packages/lib/debug/collector.ts +++ b/packages/lib/debug/collector.ts @@ -120,7 +120,10 @@ export class DebugTraceCollector { recordIterationStart(iteration: number): void { this.currentIteration = iteration; - this.trace.summary.iterationCount = Math.max(this.trace.summary.iterationCount, iteration); + this.trace.summary.iterationCount = Math.max( + this.trace.summary.iterationCount, + iteration, + ); const stepData: IterationBoundaryStepData = { type: "iteration_boundary", @@ -235,7 +238,9 @@ export class DebugTraceCollector { }, ): void { if (!this.pendingModelCall) { - console.warn("[DebugTraceCollector] recordModelCallEnd called without matching start"); + console.warn( + "[DebugTraceCollector] recordModelCallEnd called without matching start", + ); return; } @@ -307,7 +312,10 @@ export class DebugTraceCollector { } else if (!rawInput.trim().startsWith("<")) { suggestedFix = "Model is adding preamble text before XML. Add instruction: 'Start your response immediately with '"; - } else if (!rawInput.includes("") && rawInput.includes("")) { + } else if ( + !rawInput.includes("") && + rawInput.includes("") + ) { suggestedFix = "Response appears truncated. Check maxTokens setting or simplify expected output structure."; } @@ -356,7 +364,9 @@ export class DebugTraceCollector { error?: string; }): void { if (!this.pendingAction) { - console.warn("[DebugTraceCollector] recordActionEnd called without matching start"); + console.warn( + "[DebugTraceCollector] recordActionEnd called without matching start", + ); return; } @@ -382,11 +392,15 @@ export class DebugTraceCollector { } else { this.trace.summary.failedActions++; - this.addFailure("action_failure", `Action ${this.pendingAction.actionName} failed`, { - actionName: this.pendingAction.actionName, - parameters: this.pendingAction.parameters, - error: result.error, - }); + this.addFailure( + "action_failure", + `Action ${this.pendingAction.actionName} failed`, + { + actionName: this.pendingAction.actionName, + parameters: this.pendingAction.parameters, + error: result.error, + }, + ); } this.pendingAction = undefined; @@ -416,9 +430,13 @@ export class DebugTraceCollector { } recordServiceUnavailable(serviceName: string): void { - this.addFailure("service_unavailable", `Service '${serviceName}' is not available`, { - serviceName, - }); + this.addFailure( + "service_unavailable", + `Service '${serviceName}' is not available`, + { + serviceName, + }, + ); } // ============================================================================ @@ -473,7 +491,11 @@ export class DebugTraceCollector { // Private Helpers // ============================================================================ - private addStep(type: DebugStepData["type"], data: DebugStepData, durationMs?: number): void { + private addStep( + type: DebugStepData["type"], + data: DebugStepData, + durationMs?: number, + ): void { const step: DebugStep = { stepIndex: this.stepIndex++, type, diff --git a/packages/lib/debug/plugin.ts b/packages/lib/debug/plugin.ts index 75eb2c05d..a88ac3f06 100644 --- a/packages/lib/debug/plugin.ts +++ b/packages/lib/debug/plugin.ts @@ -7,7 +7,12 @@ import type { Plugin, UUID } from "@elizaos/core"; import { EventType } from "@elizaos/core"; -import { DebugTraceCollector, getCollector, registerCollector, removeCollector } from "./collector"; +import { + DebugTraceCollector, + getCollector, + registerCollector, + removeCollector, +} from "./collector"; import { storeDebugTrace } from "./store"; import type { DebugIterationPayload, @@ -109,7 +114,10 @@ async function handleRunStarted(payload: RunStartedPayload): Promise { let agentMode: "chat" | "assistant" | "build" | "unknown" = "unknown"; if (source?.includes("chatPlayground")) { agentMode = "chat"; - } else if (source?.includes("chatAssistant") || source?.includes("assistant")) { + } else if ( + source?.includes("chatAssistant") || + source?.includes("assistant") + ) { agentMode = "assistant"; } else if (source?.includes("build") || source?.includes("character")) { agentMode = "build"; @@ -129,7 +137,9 @@ async function handleRunStarted(payload: RunStartedPayload): Promise { registerCollector(collector); - console.log(`[Debug] Trace started: ${runId.substring(0, 8)} (mode: ${agentMode})`); + console.log( + `[Debug] Trace started: ${runId.substring(0, 8)} (mode: ${agentMode})`, + ); } async function handleRunEnded(payload: RunEndedPayload): Promise { @@ -139,7 +149,9 @@ async function handleRunEnded(payload: RunEndedPayload): Promise { const collector = getCollector(runId); if (!collector) { - console.warn(`[Debug] No collector found for runId: ${runId.substring(0, 8)}`); + console.warn( + `[Debug] No collector found for runId: ${runId.substring(0, 8)}`, + ); return; } @@ -170,7 +182,9 @@ async function handleRunEnded(payload: RunEndedPayload): Promise { ); } -async function handleActionStarted(payload: ActionStartedPayload): Promise { +async function handleActionStarted( + payload: ActionStartedPayload, +): Promise { if (!isDebugTracingEnabled()) return; const { runId, actionName, actionId, parameters, thought } = payload; @@ -182,7 +196,9 @@ async function handleActionStarted(payload: ActionStartedPayload): Promise collector.recordActionStart(actionName, parameters ?? {}, thought, actionId); } -async function handleActionCompleted(payload: ActionCompletedPayload): Promise { +async function handleActionCompleted( + payload: ActionCompletedPayload, +): Promise { if (!isDebugTracingEnabled()) return; const { runId, result } = payload; @@ -217,11 +233,22 @@ async function handleStateComposed( ): Promise { if (!isDebugTracingEnabled()) return; - const { runId, requestedProviders, providerOutputs, composedValues, durationMs } = payload; + const { + runId, + requestedProviders, + providerOutputs, + composedValues, + durationMs, + } = payload; const collector = getCollector(runId); if (!collector) return; - collector.recordStateComposition(requestedProviders, providerOutputs, composedValues, durationMs); + collector.recordStateComposition( + requestedProviders, + providerOutputs, + composedValues, + durationMs, + ); } async function handlePromptComposed( @@ -241,8 +268,15 @@ async function handleParseResult( ): Promise { if (!isDebugTracingEnabled()) return; - const { runId, rawInput, success, parsedOutput, parseError, attemptNumber, maxAttempts } = - payload; + const { + runId, + rawInput, + success, + parsedOutput, + parseError, + attemptNumber, + maxAttempts, + } = payload; const collector = getCollector(runId); if (!collector) return; @@ -322,20 +356,44 @@ function formatDuration(ms: number): string { // elizaOS runtime.emitEvent accepts any string event type, so custom events work at runtime const debugPluginEvents = { // Core elizaOS events - [EventType.RUN_STARTED]: [handleRunStarted as (payload: unknown) => Promise], - [EventType.RUN_ENDED]: [handleRunEnded as (payload: unknown) => Promise], - [EventType.ACTION_STARTED]: [handleActionStarted as (payload: unknown) => Promise], - [EventType.ACTION_COMPLETED]: [handleActionCompleted as (payload: unknown) => Promise], - [EventType.MODEL_USED]: [handleModelUsed as (payload: unknown) => Promise], + [EventType.RUN_STARTED]: [ + handleRunStarted as (payload: unknown) => Promise, + ], + [EventType.RUN_ENDED]: [ + handleRunEnded as (payload: unknown) => Promise, + ], + [EventType.ACTION_STARTED]: [ + handleActionStarted as (payload: unknown) => Promise, + ], + [EventType.ACTION_COMPLETED]: [ + handleActionCompleted as (payload: unknown) => Promise, + ], + [EventType.MODEL_USED]: [ + handleModelUsed as (payload: unknown) => Promise, + ], // Custom debug events with 'debug:' prefix - [DebugEventType.STATE_COMPOSED]: [handleStateComposed as (payload: unknown) => Promise], - [DebugEventType.PROMPT_COMPOSED]: [handlePromptComposed as (payload: unknown) => Promise], - [DebugEventType.PARSE_RESULT]: [handleParseResult as (payload: unknown) => Promise], - [DebugEventType.ITERATION_START]: [handleIterationStart as (payload: unknown) => Promise], - [DebugEventType.ITERATION_END]: [handleIterationEnd as (payload: unknown) => Promise], - [DebugEventType.MODEL_CALL_START]: [handleModelCallStart as (payload: unknown) => Promise], - [DebugEventType.MODEL_CALL_END]: [handleModelCallEnd as (payload: unknown) => Promise], + [DebugEventType.STATE_COMPOSED]: [ + handleStateComposed as (payload: unknown) => Promise, + ], + [DebugEventType.PROMPT_COMPOSED]: [ + handlePromptComposed as (payload: unknown) => Promise, + ], + [DebugEventType.PARSE_RESULT]: [ + handleParseResult as (payload: unknown) => Promise, + ], + [DebugEventType.ITERATION_START]: [ + handleIterationStart as (payload: unknown) => Promise, + ], + [DebugEventType.ITERATION_END]: [ + handleIterationEnd as (payload: unknown) => Promise, + ], + [DebugEventType.MODEL_CALL_START]: [ + handleModelCallStart as (payload: unknown) => Promise, + ], + [DebugEventType.MODEL_CALL_END]: [ + handleModelCallEnd as (payload: unknown) => Promise, + ], }; export const debugPlugin: Plugin = { diff --git a/packages/lib/debug/renderer.ts b/packages/lib/debug/renderer.ts index 759527b47..a0d369780 100644 --- a/packages/lib/debug/renderer.ts +++ b/packages/lib/debug/renderer.ts @@ -117,7 +117,9 @@ export class DebugTraceRenderer { lines.push(`- **Status**: ${statusEmoji(trace.status)} ${trace.status}`); lines.push(`- **Mode**: ${trace.agentMode.toUpperCase()}`); lines.push(`- **Duration**: ${formatDuration(trace.durationMs ?? 0)}`); - lines.push(`- **Iterations**: ${trace.summary.iterationCount}/${trace.summary.maxIterations}`); + lines.push( + `- **Iterations**: ${trace.summary.iterationCount}/${trace.summary.maxIterations}`, + ); lines.push( `- **Actions**: ${trace.summary.totalActions} executed, ${trace.summary.failedActions} failed`, ); @@ -224,16 +226,22 @@ export class DebugTraceRenderer { lines.push(`# Debug Trace: ${trace.runId.substring(0, 8)} - Prompts`); lines.push(""); - const promptSteps = trace.steps.filter((s) => s.data.type === "prompt_composition"); + const promptSteps = trace.steps.filter( + (s) => s.data.type === "prompt_composition", + ); const modelSteps = trace.steps.filter((s) => s.data.type === "model_call"); - const parseSteps = trace.steps.filter((s) => s.data.type === "parse_result"); + const parseSteps = trace.steps.filter( + (s) => s.data.type === "parse_result", + ); let promptIdx = 0; for (const step of promptSteps) { const data = step.data as PromptCompositionStepData; promptIdx++; - lines.push(`## Prompt ${promptIdx}: ${data.purpose} (Iteration ${data.iteration})`); + lines.push( + `## Prompt ${promptIdx}: ${data.purpose} (Iteration ${data.iteration})`, + ); lines.push(""); lines.push(`**Template**: \`${data.templateName}\``); lines.push(`**Estimated Tokens**: ~${data.estimatedTokens}`); @@ -302,7 +310,9 @@ export class DebugTraceRenderer { lines.push(`# Debug Trace: ${trace.runId.substring(0, 8)} - Actions`); lines.push(""); - const actionSteps = trace.steps.filter((s) => s.data.type === "action_execution"); + const actionSteps = trace.steps.filter( + (s) => s.data.type === "action_execution", + ); if (actionSteps.length === 0) { lines.push("*No actions were executed in this trace.*"); @@ -385,7 +395,9 @@ export class DebugTraceRenderer { lines.push(`## Failure ${failureIdx}: ${failure.type}`); lines.push(""); lines.push(`**Step**: ${failure.stepIndex}`); - lines.push(`**Time**: ${formatRelativeTime(failure.timestamp, trace.startedAt)}`); + lines.push( + `**Time**: ${formatRelativeTime(failure.timestamp, trace.startedAt)}`, + ); lines.push(""); lines.push("### What Happened"); diff --git a/packages/lib/debug/store.ts b/packages/lib/debug/store.ts index 3ec3cb79b..44b869b12 100644 --- a/packages/lib/debug/store.ts +++ b/packages/lib/debug/store.ts @@ -184,6 +184,8 @@ export function clearDebugTraces(): void { debugTraceStore.clear(); } -export function getDebugTraceStoreStats(): ReturnType { +export function getDebugTraceStoreStats(): ReturnType< + DebugTraceStore["getStats"] +> { return debugTraceStore.getStats(); } diff --git a/packages/lib/debug/types.ts b/packages/lib/debug/types.ts index 2ae1cadcb..f88706ba6 100644 --- a/packages/lib/debug/types.ts +++ b/packages/lib/debug/types.ts @@ -21,7 +21,8 @@ export const DebugEventType = { MODEL_CALL_END: "debug:model_call_end", } as const; -export type DebugEventTypeValue = (typeof DebugEventType)[keyof typeof DebugEventType]; +export type DebugEventTypeValue = + (typeof DebugEventType)[keyof typeof DebugEventType]; // ============================================================================ // Failure Types @@ -311,7 +312,12 @@ export interface DebugModelCallEndPayload { // Render Options // ============================================================================ -export type DebugRenderView = "summary" | "prompts" | "actions" | "failures" | "full"; +export type DebugRenderView = + | "summary" + | "prompts" + | "actions" + | "failures" + | "full"; export interface DebugTraceRenderOptions { view: DebugRenderView; diff --git a/packages/lib/docs/api-route-discovery.ts b/packages/lib/docs/api-route-discovery.ts index e54ef3223..67611a036 100644 --- a/packages/lib/docs/api-route-discovery.ts +++ b/packages/lib/docs/api-route-discovery.ts @@ -76,7 +76,10 @@ async function walkRoutes( } // Next route handler file - if (ent.isFile() && (ent.name === "route.ts" || ent.name === "route.js")) { + if ( + ent.isFile() && + (ent.name === "route.ts" || ent.name === "route.js") + ) { out.push({ filePath: full, segments: relativeSegments }); } }), @@ -90,7 +93,10 @@ function extractMethods(source: string): HttpMethod[] { } for (const match of source.matchAll(METHOD_REEXPORT_RE)) { for (const exported of match[1].split(",")) { - const method = exported.trim().split(/\s+as\s+/i)[0]?.trim(); + const method = exported + .trim() + .split(/\s+as\s+/i)[0] + ?.trim(); if ( method === "GET" || method === "POST" || diff --git a/packages/lib/eliza/advanced-memory-storage-service.ts b/packages/lib/eliza/advanced-memory-storage-service.ts index 52963f971..4e689bda1 100644 --- a/packages/lib/eliza/advanced-memory-storage-service.ts +++ b/packages/lib/eliza/advanced-memory-storage-service.ts @@ -64,7 +64,11 @@ function toJsonValue(value: unknown): JsonValue | undefined { if (value === null) { return null; } - if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { return value; } if (Array.isArray(value)) { @@ -112,18 +116,24 @@ function buildCustomMemoryMetadata(params: { } function asString(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : undefined; } function asNumber(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) ? value : undefined; + return typeof value === "number" && Number.isFinite(value) + ? value + : undefined; } function asStringArray(value: unknown): string[] | undefined { if (!Array.isArray(value)) { return undefined; } - const values = value.filter((entry): entry is string => typeof entry === "string"); + const values = value.filter( + (entry): entry is string => typeof entry === "string", + ); return values.length > 0 ? values : undefined; } @@ -150,7 +160,9 @@ function getMemoryText(memory: Memory): string { return typeof memory.content?.text === "string" ? memory.content.text : ""; } -function getAdvancedMemoryEnvelope(memory: Memory): AdvancedMemoryEnvelope | null { +function getAdvancedMemoryEnvelope( + memory: Memory, +): AdvancedMemoryEnvelope | null { const metadata = asRecord(memory.metadata); const advancedMemory = asRecord(metadata?.advancedMemory); if (!advancedMemory) { @@ -164,9 +176,13 @@ function getAdvancedMemoryEnvelope(memory: Memory): AdvancedMemoryEnvelope | nul return { kind, - originalEntityId: asString(advancedMemory.originalEntityId) as UUID | undefined, + originalEntityId: asString(advancedMemory.originalEntityId) as + | UUID + | undefined, anchorEntityId: asString(advancedMemory.anchorEntityId) as UUID | undefined, - category: asString(advancedMemory.category) as LongTermMemoryCategory | undefined, + category: asString(advancedMemory.category) as + | LongTermMemoryCategory + | undefined, confidence: asNumber(advancedMemory.confidence), source: asString(advancedMemory.source), semanticMetadata: toJsonRecord(advancedMemory.semanticMetadata), @@ -182,10 +198,14 @@ function getAdvancedMemoryEnvelope(memory: Memory): AdvancedMemoryEnvelope | nul }; } -export class AdvancedMemoryStorageService extends Service implements MemoryStorageProvider { +export class AdvancedMemoryStorageService + extends Service + implements MemoryStorageProvider +{ static serviceType = "memoryStorage" as ServiceTypeName; - capabilityDescription = "Persistent advanced-memory storage backed by SQL memory tables"; + capabilityDescription = + "Persistent advanced-memory storage backed by SQL memory tables"; static async start(runtime: IAgentRuntime): Promise { const service = new AdvancedMemoryStorageService(); @@ -200,7 +220,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora } private getLongTermRoomId(entityId: UUID): UUID { - return stringToUuid(`advanced-memory:long-term:${this.runtime.agentId}:${entityId}`); + return stringToUuid( + `advanced-memory:long-term:${this.runtime.agentId}:${entityId}`, + ); } private async ensureMemoryWorld(): Promise { @@ -223,7 +245,10 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora return worldId; } - private async ensureLongTermRoom(entityId: UUID, worldId: UUID): Promise { + private async ensureLongTermRoom( + entityId: UUID, + worldId: UUID, + ): Promise { const roomId = this.getLongTermRoomId(entityId); const existing = await this.runtime.getRoom(roomId); if (existing) { @@ -295,14 +320,22 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora private parseLongTermMemory(memory: Memory): LongTermMemory | null { const envelope = getAdvancedMemoryEnvelope(memory); - if (!envelope || envelope.kind !== "long_term_memory" || !memory.id || !memory.agentId) { + if ( + !envelope || + envelope.kind !== "long_term_memory" || + !memory.id || + !memory.agentId + ) { return null; } return { id: memory.id, agentId: memory.agentId, - entityId: envelope.originalEntityId ?? envelope.anchorEntityId ?? (memory.entityId as UUID), + entityId: + envelope.originalEntityId ?? + envelope.anchorEntityId ?? + (memory.entityId as UUID), category: (envelope.category ?? "semantic") as LongTermMemoryCategory, content: getMemoryText(memory), metadata: envelope.semanticMetadata, @@ -311,7 +344,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora source: envelope.source, createdAt: toDate(memory.createdAt), updatedAt: toDate(envelope.updatedAt, toDate(memory.createdAt)), - lastAccessedAt: envelope.lastAccessedAt ? toDate(envelope.lastAccessedAt) : undefined, + lastAccessedAt: envelope.lastAccessedAt + ? toDate(envelope.lastAccessedAt) + : undefined, accessCount: envelope.accessCount ?? 0, }; } @@ -332,7 +367,8 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora id: memory.id, agentId: memory.agentId, roomId: memory.roomId, - entityId: envelope.originalEntityId ?? (memory.entityId as UUID | undefined), + entityId: + envelope.originalEntityId ?? (memory.entityId as UUID | undefined), summary: getMemoryText(memory), messageCount: envelope.messageCount ?? 0, lastMessageOffset: envelope.lastMessageOffset ?? 0, @@ -374,7 +410,10 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora } async storeLongTermMemory( - memory: Omit, + memory: Omit< + LongTermMemory, + "id" | "createdAt" | "updatedAt" | "accessCount" + >, ): Promise { const now = new Date(); const anchorEntityId = await this.getAnchorEntityId(memory.entityId); @@ -447,7 +486,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora .filter((memory) => memory.agentId === agentId) .map((memory) => this.parseLongTermMemory(memory)) .filter((memory): memory is LongTermMemory => memory !== null) - .filter((memory) => (opts?.category ? memory.category === opts.category : true)); + .filter((memory) => + opts?.category ? memory.category === opts.category : true, + ); return this.sortLongTermMemories(filtered).slice(0, opts?.limit ?? 20); } @@ -456,7 +497,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora id: UUID, agentId: UUID, entityId: UUID, - updates: Partial>, + updates: Partial< + Omit + >, ): Promise { const existing = await this.runtime.getMemoryById(id); const parsed = existing ? this.parseLongTermMemory(existing) : null; @@ -466,7 +509,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora const allowedGroup = await this.getIdentityGroup(entityId); if (!allowedGroup.has(parsed.entityId)) { - throw new Error(`Long-term memory ${id} does not belong to entity ${entityId}`); + throw new Error( + `Long-term memory ${id} does not belong to entity ${entityId}`, + ); } const currentEnvelope = getAdvancedMemoryEnvelope(existing); @@ -485,7 +530,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora accessCount: updates.accessCount ?? parsed.accessCount ?? 0, }); if (!advancedMemory) { - throw new Error("Updated long-term memory metadata is not JSON-serializable"); + throw new Error( + "Updated long-term memory metadata is not JSON-serializable", + ); } await this.runtime.updateMemory({ @@ -502,7 +549,11 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora }); } - async deleteLongTermMemory(id: UUID, agentId: UUID, entityId: UUID): Promise { + async deleteLongTermMemory( + id: UUID, + agentId: UUID, + entityId: UUID, + ): Promise { const existing = await this.runtime.getMemoryById(id); const parsed = existing ? this.parseLongTermMemory(existing) : null; if (!existing || !parsed || existing.agentId !== agentId) { @@ -510,7 +561,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora } const allowedGroup = await this.getIdentityGroup(entityId); if (!allowedGroup.has(parsed.entityId)) { - throw new Error(`Long-term memory ${id} does not belong to entity ${entityId}`); + throw new Error( + `Long-term memory ${id} does not belong to entity ${entityId}`, + ); } await this.runtime.deleteMemory(id); } @@ -562,7 +615,10 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora return parsed; } - async getCurrentSessionSummary(agentId: UUID, roomId: UUID): Promise { + async getCurrentSessionSummary( + agentId: UUID, + roomId: UUID, + ): Promise { const summaries = await this.getSessionSummaries(agentId, roomId, 1); return summaries[0] ?? null; } @@ -571,11 +627,21 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora id: UUID, agentId: UUID, roomId: UUID, - updates: Partial>, + updates: Partial< + Omit< + SessionSummary, + "id" | "agentId" | "roomId" | "createdAt" | "updatedAt" + > + >, ): Promise { const existing = await this.runtime.getMemoryById(id); const parsed = existing ? this.parseSessionSummary(existing) : null; - if (!existing || !parsed || existing.agentId !== agentId || parsed.roomId !== roomId) { + if ( + !existing || + !parsed || + existing.agentId !== agentId || + parsed.roomId !== roomId + ) { throw new Error(`Session summary ${id} not found`); } @@ -594,7 +660,9 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora updatedAt: updatedAt.toISOString(), }); if (!advancedMemory) { - throw new Error("Updated session summary metadata is not JSON-serializable"); + throw new Error( + "Updated session summary metadata is not JSON-serializable", + ); } await this.runtime.updateMemory({ @@ -610,7 +678,11 @@ export class AdvancedMemoryStorageService extends Service implements MemoryStora }); } - async getSessionSummaries(agentId: UUID, roomId: UUID, limit = 5): Promise { + async getSessionSummaries( + agentId: UUID, + roomId: UUID, + limit = 5, + ): Promise { const memories = await this.runtime.getMemories({ tableName: SESSION_SUMMARY_TABLE, roomId, diff --git a/packages/lib/eliza/advanced-memory/evaluators.ts b/packages/lib/eliza/advanced-memory/evaluators.ts index a30fcb597..4f465ad1d 100644 --- a/packages/lib/eliza/advanced-memory/evaluators.ts +++ b/packages/lib/eliza/advanced-memory/evaluators.ts @@ -15,7 +15,11 @@ import { longTermExtractionTemplate, updateSummarizationTemplate, } from "./prompts"; -import { LongTermMemoryCategory, type MemoryExtraction, type SummaryResult } from "./types"; +import { + LongTermMemoryCategory, + type MemoryExtraction, + type SummaryResult, +} from "./types"; function isDialogueMessage(message: Memory): boolean { return ( @@ -28,7 +32,10 @@ function isDialogueMessage(message: Memory): boolean { ); } -async function getDialogueMessageCount(runtime: IAgentRuntime, roomId: UUID): Promise { +async function getDialogueMessageCount( + runtime: IAgentRuntime, + roomId: UUID, +): Promise { const messages = await runtime.getMemories({ tableName: "messages", roomId, @@ -38,15 +45,24 @@ async function getDialogueMessageCount(runtime: IAgentRuntime, roomId: UUID): Pr return messages.filter(isDialogueMessage).length; } -async function countRoomMemories(runtime: IAgentRuntime, roomId: UUID): Promise { +async function countRoomMemories( + runtime: IAgentRuntime, + roomId: UUID, +): Promise { type ModernCounter = (params: { roomIds: UUID[]; unique: boolean; tableName: string; }) => Promise; - type LegacyCounter = (roomId: UUID, unique?: boolean, tableName?: string) => Promise; - - const counter = runtime.countMemories as unknown as ModernCounter | LegacyCounter; + type LegacyCounter = ( + roomId: UUID, + unique?: boolean, + tableName?: string, + ) => Promise; + + const counter = runtime.countMemories as unknown as + | ModernCounter + | LegacyCounter; if (counter.length >= 2) { return (counter as LegacyCounter)(roomId, false, "messages"); } @@ -63,7 +79,9 @@ function isRecord(value: unknown): value is Record { function toStringArray(value: unknown): string[] { if (Array.isArray(value)) { - return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); + return value + .map((entry) => (typeof entry === "string" ? entry.trim() : "")) + .filter(Boolean); } if (typeof value === "string") { @@ -90,7 +108,11 @@ function parseSummaryResponse(text: string): SummaryResult { const topics = toStringArray(parsed.topics); const keyPoints = toStringArray(parsed.keyPoints); - if (summary !== "Summary not available" || topics.length > 0 || keyPoints.length > 0) { + if ( + summary !== "Summary not available" || + topics.length > 0 || + keyPoints.length > 0 + ) { return { summary, topics, keyPoints }; } } @@ -132,7 +154,8 @@ function parseMemoryExtractionResponse(text: string): MemoryExtraction[] { typeof entry.category === "string" ? (entry.category.trim() as LongTermMemoryCategory) : null; - const content = typeof entry.content === "string" ? entry.content.trim() : ""; + const content = + typeof entry.content === "string" ? entry.content.trim() : ""; const confidenceRaw = entry.confidence; const confidence = typeof confidenceRaw === "number" @@ -184,7 +207,10 @@ export const summarizationEvaluator: Evaluator = { similes: [], alwaysRun: true, examples: [], - validate: async (runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + runtime: IAgentRuntime, + message: Memory, + ): Promise => { if (!message.content?.text) { return false; } @@ -195,14 +221,20 @@ export const summarizationEvaluator: Evaluator = { } const config = memoryService.getConfig(); - const currentDialogueCount = await getDialogueMessageCount(runtime, message.roomId); - const existingSummary = await memoryService.getCurrentSessionSummary(message.roomId); + const currentDialogueCount = await getDialogueMessageCount( + runtime, + message.roomId, + ); + const existingSummary = await memoryService.getCurrentSessionSummary( + message.roomId, + ); if (!existingSummary) { return currentDialogueCount >= config.shortTermSummarizationThreshold; } - const newDialogueCount = currentDialogueCount - existingSummary.lastMessageOffset; + const newDialogueCount = + currentDialogueCount - existingSummary.lastMessageOffset; return newDialogueCount >= config.shortTermSummarizationInterval; }, handler: async (runtime: IAgentRuntime, message: Memory) => { @@ -216,7 +248,8 @@ export const summarizationEvaluator: Evaluator = { const roomId = message.roomId; try { - const existingSummary = await memoryService.getCurrentSessionSummary(roomId); + const existingSummary = + await memoryService.getCurrentSessionSummary(roomId); const lastOffset = existingSummary?.lastMessageOffset || 0; const allMessages = await runtime.getMemories({ @@ -250,7 +283,9 @@ export const summarizationEvaluator: Evaluator = { const formattedMessages = newDialogueMessages .map((currentMessage) => { const sender = - currentMessage.entityId === runtime.agentId ? runtime.character.name : "User"; + currentMessage.entityId === runtime.agentId + ? runtime.character.name + : "User"; return `${sender}: ${currentMessage.content.text || "[non-text message]"}`; }) .join("\n"); @@ -272,7 +307,9 @@ export const summarizationEvaluator: Evaluator = { const initialMessages = sortedDialogueMessages .map((currentMessage) => { const sender = - currentMessage.entityId === runtime.agentId ? runtime.character.name : "User"; + currentMessage.entityId === runtime.agentId + ? runtime.character.name + : "User"; return `${sender}: ${currentMessage.content.text || "[non-text message]"}`; }) .join("\n"); @@ -307,7 +344,8 @@ export const summarizationEvaluator: Evaluator = { if (existingSummary) { await memoryService.updateSessionSummary(existingSummary.id, roomId, { summary: summaryResult.summary, - messageCount: existingSummary.messageCount + newDialogueMessages.length, + messageCount: + existingSummary.messageCount + newDialogueMessages.length, lastMessageOffset: newOffset, endTime, topics: summaryResult.topics, @@ -317,7 +355,8 @@ export const summarizationEvaluator: Evaluator = { await memoryService.storeSessionSummary({ agentId: runtime.agentId, roomId, - entityId: message.entityId !== runtime.agentId ? message.entityId : undefined, + entityId: + message.entityId !== runtime.agentId ? message.entityId : undefined, summary: summaryResult.summary, messageCount: totalDialogueCount, lastMessageOffset: totalDialogueCount, @@ -329,7 +368,10 @@ export const summarizationEvaluator: Evaluator = { } } catch (error) { const err = error instanceof Error ? error.message : String(error); - logger.error({ src: "evaluator:memory", err }, "Error during summarization"); + logger.error( + { src: "evaluator:memory", err }, + "Error during summarization", + ); } return undefined; @@ -342,7 +384,10 @@ export const longTermExtractionEvaluator: Evaluator = { similes: [], alwaysRun: true, examples: [], - validate: async (runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + runtime: IAgentRuntime, + message: Memory, + ): Promise => { if (message.entityId === runtime.agentId || !message.content?.text) { return false; } @@ -357,8 +402,15 @@ export const longTermExtractionEvaluator: Evaluator = { return false; } - const currentMessageCount = await countRoomMemories(runtime, message.roomId); - return memoryService.shouldRunExtraction(message.entityId, message.roomId, currentMessageCount); + const currentMessageCount = await countRoomMemories( + runtime, + message.roomId, + ); + return memoryService.shouldRunExtraction( + message.entityId, + message.roomId, + currentMessageCount, + ); }, handler: async (runtime: IAgentRuntime, message: Memory) => { const memoryService = runtime.getService("memory") as MemoryService | null; @@ -381,12 +433,17 @@ export const longTermExtractionEvaluator: Evaluator = { const formattedMessages = recentMessages .sort((left, right) => (left.createdAt || 0) - (right.createdAt || 0)) .map((currentMessage) => { - const sender = currentMessage.entityId === runtime.agentId ? agentName : "User"; + const sender = + currentMessage.entityId === runtime.agentId ? agentName : "User"; return `${sender}: ${currentMessage.content.text || "[non-text message]"}`; }) .join("\n"); - const existingMemories = await memoryService.getLongTermMemories(entityId, undefined, 30); + const existingMemories = await memoryService.getLongTermMemories( + entityId, + undefined, + 30, + ); const formattedExisting = existingMemories.length > 0 ? existingMemories @@ -436,10 +493,17 @@ export const longTermExtractionEvaluator: Evaluator = { ); const currentMessageCount = await countRoomMemories(runtime, roomId); - await memoryService.setLastExtractionCheckpoint(entityId, roomId, currentMessageCount); + await memoryService.setLastExtractionCheckpoint( + entityId, + roomId, + currentMessageCount, + ); } catch (error) { const err = error instanceof Error ? error.message : String(error); - logger.error({ src: "evaluator:memory", err }, "Error during long-term memory extraction"); + logger.error( + { src: "evaluator:memory", err }, + "Error during long-term memory extraction", + ); } return undefined; diff --git a/packages/lib/eliza/advanced-memory/index.ts b/packages/lib/eliza/advanced-memory/index.ts index db1675b4c..e0dac8905 100644 --- a/packages/lib/eliza/advanced-memory/index.ts +++ b/packages/lib/eliza/advanced-memory/index.ts @@ -1,5 +1,8 @@ import type { Plugin } from "@elizaos/core"; -import { longTermExtractionEvaluator, summarizationEvaluator } from "./evaluators"; +import { + longTermExtractionEvaluator, + summarizationEvaluator, +} from "./evaluators"; import { MemoryService } from "./memory-service"; import { contextSummaryProvider, longTermMemoryProvider } from "./providers"; diff --git a/packages/lib/eliza/advanced-memory/memory-service.ts b/packages/lib/eliza/advanced-memory/memory-service.ts index 10ca67ded..cb0b03f78 100644 --- a/packages/lib/eliza/advanced-memory/memory-service.ts +++ b/packages/lib/eliza/advanced-memory/memory-service.ts @@ -82,15 +82,23 @@ export class MemoryService extends Service { const threshold = runtime.getSetting("MEMORY_SUMMARIZATION_THRESHOLD"); if (threshold) { - this.memoryConfig.shortTermSummarizationThreshold = Number.parseInt(String(threshold), 10); + this.memoryConfig.shortTermSummarizationThreshold = Number.parseInt( + String(threshold), + 10, + ); } const retainRecent = runtime.getSetting("MEMORY_RETAIN_RECENT"); if (retainRecent) { - this.memoryConfig.shortTermRetainRecent = Number.parseInt(String(retainRecent), 10); + this.memoryConfig.shortTermRetainRecent = Number.parseInt( + String(retainRecent), + 10, + ); } - const summarizationInterval = runtime.getSetting("MEMORY_SUMMARIZATION_INTERVAL"); + const summarizationInterval = runtime.getSetting( + "MEMORY_SUMMARIZATION_INTERVAL", + ); if (summarizationInterval) { this.memoryConfig.shortTermSummarizationInterval = Number.parseInt( String(summarizationInterval), @@ -100,7 +108,10 @@ export class MemoryService extends Service { const maxNewMessages = runtime.getSetting("MEMORY_MAX_NEW_MESSAGES"); if (maxNewMessages) { - this.memoryConfig.summaryMaxNewMessages = Number.parseInt(String(maxNewMessages), 10); + this.memoryConfig.summaryMaxNewMessages = Number.parseInt( + String(maxNewMessages), + 10, + ); } const longTermEnabled = runtime.getSetting("MEMORY_LONG_TERM_ENABLED"); @@ -110,14 +121,18 @@ export class MemoryService extends Service { this.memoryConfig.longTermExtractionEnabled = true; } - const confidenceThreshold = runtime.getSetting("MEMORY_CONFIDENCE_THRESHOLD"); + const confidenceThreshold = runtime.getSetting( + "MEMORY_CONFIDENCE_THRESHOLD", + ); if (confidenceThreshold) { this.memoryConfig.longTermConfidenceThreshold = Number.parseFloat( String(confidenceThreshold), ); } - const extractionThreshold = runtime.getSetting("MEMORY_EXTRACTION_THRESHOLD"); + const extractionThreshold = runtime.getSetting( + "MEMORY_EXTRACTION_THRESHOLD", + ); if (extractionThreshold) { this.memoryConfig.longTermExtractionThreshold = Number.parseInt( String(extractionThreshold), @@ -135,7 +150,8 @@ export class MemoryService extends Service { logger.debug( { - summarizationThreshold: this.memoryConfig.shortTermSummarizationThreshold, + summarizationThreshold: + this.memoryConfig.shortTermSummarizationThreshold, summarizationInterval: this.memoryConfig.shortTermSummarizationInterval, maxNewMessages: this.memoryConfig.summaryMaxNewMessages, retainRecent: this.memoryConfig.shortTermRetainRecent, @@ -180,9 +196,15 @@ export class MemoryService extends Service { unique: boolean; tableName: string; }) => Promise; - type LegacyCounter = (roomId: UUID, unique?: boolean, tableName?: string) => Promise; - - const counter = this.runtime.countMemories as unknown as ModernCounter | LegacyCounter; + type LegacyCounter = ( + roomId: UUID, + unique?: boolean, + tableName?: string, + ) => Promise; + + const counter = this.runtime.countMemories as unknown as + | ModernCounter + | LegacyCounter; if (counter.length >= 2) { return (counter as LegacyCounter)(roomId, false, "messages"); } @@ -225,7 +247,10 @@ export class MemoryService extends Service { return `memory:extraction:${entityId}:${roomId}`; } - async getLastExtractionCheckpoint(entityId: UUID, roomId: UUID): Promise { + async getLastExtractionCheckpoint( + entityId: UUID, + roomId: UUID, + ): Promise { const key = this.getExtractionKey(entityId, roomId); const cached = this.lastExtractionCheckpoints.get(key); if (cached !== undefined) { @@ -235,12 +260,17 @@ export class MemoryService extends Service { try { const checkpoint = await this.runtime.getCache(key); const messageCount = - typeof checkpoint === "number" && Number.isFinite(checkpoint) ? checkpoint : 0; + typeof checkpoint === "number" && Number.isFinite(checkpoint) + ? checkpoint + : 0; this.lastExtractionCheckpoints.set(key, messageCount); return messageCount; } catch (error) { const err = error instanceof Error ? error.message : String(error); - logger.warn({ src: "service:memory", err }, "Failed to get extraction checkpoint from cache"); + logger.warn( + { src: "service:memory", err }, + "Failed to get extraction checkpoint from cache", + ); return 0; } } @@ -280,13 +310,22 @@ export class MemoryService extends Service { return false; } - const lastCheckpoint = await this.getLastExtractionCheckpoint(entityId, roomId); - const currentCheckpoint = Math.floor(currentMessageCount / interval) * interval; - return currentMessageCount >= threshold && currentCheckpoint > lastCheckpoint; + const lastCheckpoint = await this.getLastExtractionCheckpoint( + entityId, + roomId, + ); + const currentCheckpoint = + Math.floor(currentMessageCount / interval) * interval; + return ( + currentMessageCount >= threshold && currentCheckpoint > lastCheckpoint + ); } async storeLongTermMemory( - memory: Omit, + memory: Omit< + LongTermMemory, + "id" | "createdAt" | "updatedAt" | "accessCount" + >, ): Promise { return (await this.getStorage()).storeLongTermMemory(memory); } @@ -299,16 +338,22 @@ export class MemoryService extends Service { if (limit <= 0) { return []; } - return (await this.getStorage()).getLongTermMemories(this.runtime.agentId, entityId, { - category, - limit, - }); + return (await this.getStorage()).getLongTermMemories( + this.runtime.agentId, + entityId, + { + category, + limit, + }, + ); } async updateLongTermMemory( id: UUID, entityId: UUID, - updates: Partial>, + updates: Partial< + Omit + >, ): Promise { await (await this.getStorage()).updateLongTermMemory( id, @@ -319,11 +364,18 @@ export class MemoryService extends Service { } async deleteLongTermMemory(id: UUID, entityId: UUID): Promise { - await (await this.getStorage()).deleteLongTermMemory(id, this.runtime.agentId, entityId); + await (await this.getStorage()).deleteLongTermMemory( + id, + this.runtime.agentId, + entityId, + ); } async getCurrentSessionSummary(roomId: UUID): Promise { - return (await this.getStorage()).getCurrentSessionSummary(this.runtime.agentId, roomId); + return (await this.getStorage()).getCurrentSessionSummary( + this.runtime.agentId, + roomId, + ); } async storeSessionSummary( @@ -335,13 +387,30 @@ export class MemoryService extends Service { async updateSessionSummary( id: UUID, roomId: UUID, - updates: Partial>, + updates: Partial< + Omit< + SessionSummary, + "id" | "agentId" | "roomId" | "createdAt" | "updatedAt" + > + >, ): Promise { - await (await this.getStorage()).updateSessionSummary(id, this.runtime.agentId, roomId, updates); + await (await this.getStorage()).updateSessionSummary( + id, + this.runtime.agentId, + roomId, + updates, + ); } - async getSessionSummaries(roomId: UUID, limit = 5): Promise { - return (await this.getStorage()).getSessionSummaries(this.runtime.agentId, roomId, limit); + async getSessionSummaries( + roomId: UUID, + limit = 5, + ): Promise { + return (await this.getStorage()).getSessionSummaries( + this.runtime.agentId, + roomId, + limit, + ); } async searchLongTermMemories( @@ -363,13 +432,20 @@ export class MemoryService extends Service { } try { - const candidates = await this.getLongTermMemories(entityId, undefined, 200); + const candidates = await this.getLongTermMemories( + entityId, + undefined, + 200, + ); const scored: Array<{ memory: LongTermMemory; similarity: number }> = []; for (const memory of candidates) { if ((memory.embedding?.length ?? 0) === 0) { continue; } - const similarity = cosineSimilarity(memory.embedding ?? [], queryEmbedding); + const similarity = cosineSimilarity( + memory.embedding ?? [], + queryEmbedding, + ); if (similarity < matchThreshold) { continue; } @@ -382,7 +458,10 @@ export class MemoryService extends Service { continue; } let index = 0; - while (index < scored.length && scored[index]!.similarity > similarity) { + while ( + index < scored.length && + scored[index]!.similarity > similarity + ) { index += 1; } scored.splice(index, 0, { memory, similarity }); @@ -425,7 +504,9 @@ export class MemoryService extends Service { .split("_") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); - const items = categoryMemories.map((memory) => `- ${memory.content}`).join("\n"); + const items = categoryMemories + .map((memory) => `- ${memory.content}`) + .join("\n"); sections.push(`**${categoryName}**:\n${items}`); } diff --git a/packages/lib/eliza/advanced-memory/providers.ts b/packages/lib/eliza/advanced-memory/providers.ts index 9bc2a1eab..654036fc1 100644 --- a/packages/lib/eliza/advanced-memory/providers.ts +++ b/packages/lib/eliza/advanced-memory/providers.ts @@ -13,9 +13,15 @@ export const contextSummaryProvider: Provider = { name: "SUMMARIZED_CONTEXT", description: "Provides summarized context from previous conversations", position: 96, - get: async (runtime: IAgentRuntime, message: Memory, _state: State): Promise => { + get: async ( + runtime: IAgentRuntime, + message: Memory, + _state: State, + ): Promise => { try { - const memoryService = runtime.getService("memory") as MemoryService | null; + const memoryService = runtime.getService( + "memory", + ) as MemoryService | null; if (!memoryService) { return { data: {}, @@ -24,7 +30,9 @@ export const contextSummaryProvider: Provider = { }; } - const currentSummary = await memoryService.getCurrentSessionSummary(message.roomId); + const currentSummary = await memoryService.getCurrentSessionSummary( + message.roomId, + ); if (!currentSummary) { return { data: {}, @@ -44,7 +52,10 @@ export const contextSummaryProvider: Provider = { } const sessionSummaries = addHeader("# Conversation Summary", summaryOnly); - const sessionSummariesWithTopics = addHeader("# Conversation Summary", summaryWithTopics); + const sessionSummariesWithTopics = addHeader( + "# Conversation Summary", + summaryWithTopics, + ); return { data: { @@ -57,7 +68,10 @@ export const contextSummaryProvider: Provider = { }; } catch (error) { const err = error instanceof Error ? error.message : String(error); - logger.error({ src: "provider:memory", err }, "Error in contextSummaryProvider"); + logger.error( + { src: "provider:memory", err }, + "Error in contextSummaryProvider", + ); return { data: {}, values: { sessionSummaries: "", sessionSummariesWithTopics: "" }, @@ -71,9 +85,15 @@ export const longTermMemoryProvider: Provider = { name: "LONG_TERM_MEMORY", description: "Persistent facts and preferences about the user", position: 50, - get: async (runtime: IAgentRuntime, message: Memory, _state: State): Promise => { + get: async ( + runtime: IAgentRuntime, + message: Memory, + _state: State, + ): Promise => { try { - const memoryService = runtime.getService("memory") as MemoryService | null; + const memoryService = runtime.getService( + "memory", + ) as MemoryService | null; if (!memoryService || message.entityId === runtime.agentId) { return { data: { memoryCount: 0 }, @@ -82,7 +102,11 @@ export const longTermMemoryProvider: Provider = { }; } - const memories = await memoryService.getLongTermMemories(message.entityId, undefined, 25); + const memories = await memoryService.getLongTermMemories( + message.entityId, + undefined, + 25, + ); if (memories.length === 0) { return { data: { memoryCount: 0 }, @@ -91,7 +115,8 @@ export const longTermMemoryProvider: Provider = { }; } - const formattedMemories = await memoryService.getFormattedLongTermMemories(message.entityId); + const formattedMemories = + await memoryService.getFormattedLongTermMemories(message.entityId); const text = addHeader("# What I Know About You", formattedMemories); const categoryCounts = new Map(); @@ -116,7 +141,10 @@ export const longTermMemoryProvider: Provider = { }; } catch (error) { const err = error instanceof Error ? error.message : String(error); - logger.error({ src: "provider:memory", err }, "Error in longTermMemoryProvider"); + logger.error( + { src: "provider:memory", err }, + "Error in longTermMemoryProvider", + ); return { data: { memoryCount: 0 }, values: { longTermMemories: "" }, diff --git a/packages/lib/eliza/advanced-memory/types.ts b/packages/lib/eliza/advanced-memory/types.ts index 93381e1dd..93aac57ba 100644 --- a/packages/lib/eliza/advanced-memory/types.ts +++ b/packages/lib/eliza/advanced-memory/types.ts @@ -1,7 +1,10 @@ import type { TextGenerationModelType, UUID } from "@elizaos/core"; export type JsonPrimitive = string | number | boolean | null; -export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; +export type JsonValue = + | JsonPrimitive + | JsonValue[] + | { [key: string]: JsonValue }; export enum LongTermMemoryCategory { EPISODIC = "episodic", @@ -72,7 +75,10 @@ export interface SummaryResult { export interface MemoryStorageProvider { storeLongTermMemory( - memory: Omit, + memory: Omit< + LongTermMemory, + "id" | "createdAt" | "updatedAt" | "accessCount" + >, ): Promise; getLongTermMemories( @@ -85,7 +91,9 @@ export interface MemoryStorageProvider { id: UUID, agentId: UUID, entityId: UUID, - updates: Partial>, + updates: Partial< + Omit + >, ): Promise; deleteLongTermMemory(id: UUID, agentId: UUID, entityId: UUID): Promise; @@ -94,14 +102,26 @@ export interface MemoryStorageProvider { summary: Omit, ): Promise; - getCurrentSessionSummary(agentId: UUID, roomId: UUID): Promise; + getCurrentSessionSummary( + agentId: UUID, + roomId: UUID, + ): Promise; updateSessionSummary( id: UUID, agentId: UUID, roomId: UUID, - updates: Partial>, + updates: Partial< + Omit< + SessionSummary, + "id" | "agentId" | "roomId" | "createdAt" | "updatedAt" + > + >, ): Promise; - getSessionSummaries(agentId: UUID, roomId: UUID, limit?: number): Promise; + getSessionSummaries( + agentId: UUID, + roomId: UUID, + limit?: number, + ): Promise; } diff --git a/packages/lib/eliza/agent-loader.ts b/packages/lib/eliza/agent-loader.ts index 217527440..8a557a021 100644 --- a/packages/lib/eliza/agent-loader.ts +++ b/packages/lib/eliza/agent-loader.ts @@ -41,10 +41,15 @@ async function preloadPlugins(): Promise { try { // Only preload web-search plugin (local version) // Knowledge plugin is loaded on-demand when documents exist - const webSearchModule = await import("./plugin-web-search/src").catch((e) => { - logger.warn("[AgentLoader] Failed to preload local web-search plugin:", e); - return null; - }); + const webSearchModule = await import("./plugin-web-search/src").catch( + (e) => { + logger.warn( + "[AgentLoader] Failed to preload local web-search plugin:", + e, + ); + return null; + }, + ); if (webSearchModule) { _webSearchPlugin = asPlugin(webSearchModule.webSearchPlugin); @@ -86,7 +91,10 @@ async function resolveEffectiveMode( // Query document count once - needed for multiple checks and plugin resolution // Note: no roomId filter — we want agent-level document count across all rooms - const documentCount = await memoriesRepository.countByType(characterId, "documents"); + const documentCount = await memoriesRepository.countByType( + characterId, + "documents", + ); // Already ASSISTANT mode - no upgrade needed if (requestedMode === AgentMode.ASSISTANT) { @@ -140,7 +148,9 @@ async function getWebSearchPlugin(): Promise { } /** Cast external plugin to local Plugin type for cross-version compatibility. */ -function asPlugin(plugin: T): Plugin { +function asPlugin( + plugin: T, +): Plugin { return plugin as Plugin; } @@ -173,7 +183,10 @@ export class AgentLoader { const elizaCharacter = charactersService.toElizaCharacter(dbCharacter); const character = this.buildCharacter(elizaCharacter); - const characterSettings = (elizaCharacter.settings ?? {}) as Record; + const characterSettings = (elizaCharacter.settings ?? {}) as Record< + string, + unknown + >; const characterPlugins = elizaCharacter.plugins || []; if (options?.webSearchEnabled) { @@ -219,7 +232,11 @@ export class AgentLoader { characterSettings, [], ); - const plugins = await this.resolvePlugins(modeResolution.mode, [], characterSettings); + const plugins = await this.resolvePlugins( + modeResolution.mode, + [], + characterSettings, + ); const character = this.buildCharacter({ ...(defaultAgent.character as unknown as ElizaCharacter), settings: characterSettings as Record< @@ -232,20 +249,26 @@ export class AgentLoader { } private buildCharacter(elizaCharacter: ElizaCharacter): Character { - const characterId = elizaCharacter.id || "b850bc30-45f8-0041-a00a-83df46d8555d"; + const characterId = + elizaCharacter.id || "b850bc30-45f8-0041-a00a-83df46d8555d"; const charSettings = (elizaCharacter.settings || {}) as Record< string, string | boolean | number | Record >; - const settings: Record> = { + const settings: Record< + string, + string | boolean | number | Record + > = { ...charSettings, POSTGRES_URL: process.env.DATABASE_URL!, DATABASE_URL: process.env.DATABASE_URL!, ELIZAOS_CLOUD_BASE_URL: getElizaCloudApiUrl(), // ElevenLabs settings (shared config) ...buildElevenLabsSettings(charSettings), - ...(elizaCharacter.avatarUrl ? { avatarUrl: elizaCharacter.avatarUrl } : {}), + ...(elizaCharacter.avatarUrl + ? { avatarUrl: elizaCharacter.avatarUrl } + : {}), }; // createCharacter() normalizes all fields internally. The ElizaCharacter DB type @@ -278,7 +301,9 @@ export class AgentLoader { const plugins: Plugin[] = []; const isAffiliate = hasAffiliateData(characterSettings); - const conditionalPlugins = isAffiliate ? [] : getConditionalPlugins(characterSettings); + const conditionalPlugins = isAffiliate + ? [] + : getConditionalPlugins(characterSettings); const modePlugins = AGENT_MODE_PLUGINS[agentMode].map((pluginName) => { if (isAffiliate && pluginName === "@eliza-cloud/plugin-assistant") { @@ -287,13 +312,19 @@ export class AgentLoader { return pluginName; }); - const allPluginNames = [...modePlugins, ...characterPlugins, ...conditionalPlugins]; + const allPluginNames = [ + ...modePlugins, + ...characterPlugins, + ...conditionalPlugins, + ]; // Only load knowledge plugin when documents actually exist // Upload capability is handled separately — no need to init the full plugin if (options?.hasKnowledge) { allPluginNames.push("knowledge"); - logger.info("[AgentLoader] Loading native knowledge plugin - documents found"); + logger.info( + "[AgentLoader] Loading native knowledge plugin - documents found", + ); } for (const pluginName of allPluginNames) { diff --git a/packages/lib/eliza/agent-mode-types.ts b/packages/lib/eliza/agent-mode-types.ts index fee2c9499..7f4ba546a 100644 --- a/packages/lib/eliza/agent-mode-types.ts +++ b/packages/lib/eliza/agent-mode-types.ts @@ -40,13 +40,18 @@ export const DEFAULT_AGENT_MODE: AgentModeConfig = { * Type guard to check if a value is a valid AgentMode */ export function isValidAgentMode(mode: unknown): mode is AgentMode { - return typeof mode === "string" && Object.values(AgentMode).includes(mode as AgentMode); + return ( + typeof mode === "string" && + Object.values(AgentMode).includes(mode as AgentMode) + ); } /** * Type guard to check if a value is a valid AgentModeConfig */ -export function isValidAgentModeConfig(config: unknown): config is AgentModeConfig { +export function isValidAgentModeConfig( + config: unknown, +): config is AgentModeConfig { if (!config || typeof config !== "object") { return false; } @@ -60,7 +65,11 @@ export function isValidAgentModeConfig(config: unknown): config is AgentModeConf // Check metadata if present if (cfg.metadata !== undefined) { - if (typeof cfg.metadata !== "object" || cfg.metadata === null || Array.isArray(cfg.metadata)) { + if ( + typeof cfg.metadata !== "object" || + cfg.metadata === null || + Array.isArray(cfg.metadata) + ) { return false; } } @@ -152,7 +161,10 @@ function hasValidConfiguration( const apiKey = settings.N8N_API_KEY ?? secrets.N8N_API_KEY; const host = settings.N8N_HOST ?? secrets.N8N_HOST; return ( - typeof apiKey === "string" && apiKey.length > 0 && typeof host === "string" && host.length > 0 + typeof apiKey === "string" && + apiKey.length > 0 && + typeof host === "string" && + host.length > 0 ); } @@ -162,7 +174,10 @@ function hasValidConfiguration( switch (key) { case "mcp": { const mcpSettings = value as ConditionalPluginSettings["mcp"]; - return mcpSettings?.servers != null && Object.keys(mcpSettings.servers).length > 0; + return ( + mcpSettings?.servers != null && + Object.keys(mcpSettings.servers).length > 0 + ); } case "webSearch": { const webSearchSettings = value as ConditionalPluginSettings["webSearch"]; @@ -177,9 +192,13 @@ function hasValidConfiguration( * Get plugins that should be injected based on character settings. * Only returns plugins when settings contain actual configuration. */ -export function getConditionalPlugins(settings: Record): string[] { +export function getConditionalPlugins( + settings: Record, +): string[] { return Object.entries(SETTINGS_PLUGIN_MAP) - .filter(([key]) => hasValidConfiguration(key as keyof typeof SETTINGS_PLUGIN_MAP, settings)) + .filter(([key]) => + hasValidConfiguration(key as keyof typeof SETTINGS_PLUGIN_MAP, settings), + ) .map(([, pluginName]) => pluginName); } @@ -191,7 +210,9 @@ export function requiresAssistantMode( settings: Record, ): keyof typeof SETTINGS_PLUGIN_MAP | null { for (const key of Object.keys(SETTINGS_PLUGIN_MAP)) { - if (hasValidConfiguration(key as keyof typeof SETTINGS_PLUGIN_MAP, settings)) { + if ( + hasValidConfiguration(key as keyof typeof SETTINGS_PLUGIN_MAP, settings) + ) { return key as keyof typeof SETTINGS_PLUGIN_MAP; } } diff --git a/packages/lib/eliza/agent.ts b/packages/lib/eliza/agent.ts index 87c88e60e..90a8e9f15 100644 --- a/packages/lib/eliza/agent.ts +++ b/packages/lib/eliza/agent.ts @@ -63,25 +63,33 @@ const character = { N8N_API_KEY: process.env.N8N_API_KEY!, N8N_HOST: process.env.N8N_HOST!, ELEVENLABS_API_KEY: process.env.ELEVENLABS_API_KEY!, - ELEVENLABS_VOICE_ID: process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL", // Rachel voice (default) - ELEVENLABS_MODEL_ID: process.env.ELEVENLABS_MODEL_ID || "eleven_multilingual_v2", + ELEVENLABS_VOICE_ID: + process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL", // Rachel voice (default) + ELEVENLABS_MODEL_ID: + process.env.ELEVENLABS_MODEL_ID || "eleven_multilingual_v2", ELEVENLABS_VOICE_STABILITY: process.env.ELEVENLABS_VOICE_STABILITY || "0.5", - ELEVENLABS_VOICE_SIMILARITY_BOOST: process.env.ELEVENLABS_VOICE_SIMILARITY_BOOST || "0.75", + ELEVENLABS_VOICE_SIMILARITY_BOOST: + process.env.ELEVENLABS_VOICE_SIMILARITY_BOOST || "0.75", ELEVENLABS_VOICE_STYLE: process.env.ELEVENLABS_VOICE_STYLE || "0", - ELEVENLABS_VOICE_USE_SPEAKER_BOOST: process.env.ELEVENLABS_VOICE_USE_SPEAKER_BOOST || "true", - ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: process.env.ELEVENLABS_OPTIMIZE_STREAMING_LATENCY || "0", - ELEVENLABS_OUTPUT_FORMAT: process.env.ELEVENLABS_OUTPUT_FORMAT || "mp3_44100_128", + ELEVENLABS_VOICE_USE_SPEAKER_BOOST: + process.env.ELEVENLABS_VOICE_USE_SPEAKER_BOOST || "true", + ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: + process.env.ELEVENLABS_OPTIMIZE_STREAMING_LATENCY || "0", + ELEVENLABS_OUTPUT_FORMAT: + process.env.ELEVENLABS_OUTPUT_FORMAT || "mp3_44100_128", ELEVENLABS_LANGUAGE_CODE: process.env.ELEVENLABS_LANGUAGE_CODE || "en", // ElevenLabs STT Configuration ELEVENLABS_STT_MODEL_ID: process.env.ELEVENLABS_STT_MODEL_ID || "scribe_v1", - ELEVENLABS_STT_LANGUAGE_CODE: process.env.ELEVENLABS_STT_LANGUAGE_CODE || "en", + ELEVENLABS_STT_LANGUAGE_CODE: + process.env.ELEVENLABS_STT_LANGUAGE_CODE || "en", ELEVENLABS_STT_TIMESTAMPS_GRANULARITY: process.env.ELEVENLABS_STT_TIMESTAMPS_GRANULARITY || "word", ELEVENLABS_STT_DIARIZE: process.env.ELEVENLABS_STT_DIARIZE || "false", ...(process.env.ELEVENLABS_STT_NUM_SPEAKERS && { ELEVENLABS_STT_NUM_SPEAKERS: process.env.ELEVENLABS_STT_NUM_SPEAKERS, }), - ELEVENLABS_STT_TAG_AUDIO_EVENTS: process.env.ELEVENLABS_STT_TAG_AUDIO_EVENTS || "false", + ELEVENLABS_STT_TAG_AUDIO_EVENTS: + process.env.ELEVENLABS_STT_TAG_AUDIO_EVENTS || "false", avatarUrl: "https://raw.githubusercontent.com/elizaOS/eliza-avatars/refs/heads/master/Eliza/portrait.png", // Note: MCP servers are injected dynamically at runtime based on user's OAuth connections @@ -1251,7 +1259,10 @@ const agent = { ...(cloudBootstrapPlugin.providers || []), ...(cloudBillingPlugin.providers || []), ].flat(), - actions: [...(elevenLabsPlugin.actions || []), ...(cloudBootstrapPlugin.actions || [])].flat(), + actions: [ + ...(elevenLabsPlugin.actions || []), + ...(cloudBootstrapPlugin.actions || []), + ].flat(), }; export default agent; diff --git a/packages/lib/eliza/config.ts b/packages/lib/eliza/config.ts index 11ce161c6..c8d0aadd2 100644 --- a/packages/lib/eliza/config.ts +++ b/packages/lib/eliza/config.ts @@ -46,8 +46,10 @@ export function getElizaCloudApiUrl(): string { export function getDefaultModels() { return { small: process.env.ELIZAOS_CLOUD_SMALL_MODEL || "minimax/minimax-m2.7", - large: process.env.ELIZAOS_CLOUD_LARGE_MODEL || "anthropic/claude-sonnet-4.6", - embedding: process.env.ELIZAOS_CLOUD_EMBEDDING_MODEL || "text-embedding-3-small", + large: + process.env.ELIZAOS_CLOUD_LARGE_MODEL || "anthropic/claude-sonnet-4.6", + embedding: + process.env.ELIZAOS_CLOUD_EMBEDDING_MODEL || "text-embedding-3-small", }; } @@ -102,29 +104,54 @@ export function buildElevenLabsSettings( const settings: ElevenLabsSettings = { // TTS ELEVENLABS_API_KEY: process.env.ELEVENLABS_API_KEY || "", - ELEVENLABS_VOICE_ID: getSetting("ELEVENLABS_VOICE_ID", "EXAVITQu4vr4xnSDxMaL"), - ELEVENLABS_MODEL_ID: getSetting("ELEVENLABS_MODEL_ID", "eleven_multilingual_v2"), + ELEVENLABS_VOICE_ID: getSetting( + "ELEVENLABS_VOICE_ID", + "EXAVITQu4vr4xnSDxMaL", + ), + ELEVENLABS_MODEL_ID: getSetting( + "ELEVENLABS_MODEL_ID", + "eleven_multilingual_v2", + ), ELEVENLABS_VOICE_STABILITY: getSetting("ELEVENLABS_VOICE_STABILITY", "0.5"), - ELEVENLABS_VOICE_SIMILARITY_BOOST: getSetting("ELEVENLABS_VOICE_SIMILARITY_BOOST", "0.75"), + ELEVENLABS_VOICE_SIMILARITY_BOOST: getSetting( + "ELEVENLABS_VOICE_SIMILARITY_BOOST", + "0.75", + ), ELEVENLABS_VOICE_STYLE: getSetting("ELEVENLABS_VOICE_STYLE", "0"), - ELEVENLABS_VOICE_USE_SPEAKER_BOOST: getSetting("ELEVENLABS_VOICE_USE_SPEAKER_BOOST", "true"), - ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: getSetting("ELEVENLABS_OPTIMIZE_STREAMING_LATENCY", "0"), - ELEVENLABS_OUTPUT_FORMAT: getSetting("ELEVENLABS_OUTPUT_FORMAT", "mp3_44100_128"), + ELEVENLABS_VOICE_USE_SPEAKER_BOOST: getSetting( + "ELEVENLABS_VOICE_USE_SPEAKER_BOOST", + "true", + ), + ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: getSetting( + "ELEVENLABS_OPTIMIZE_STREAMING_LATENCY", + "0", + ), + ELEVENLABS_OUTPUT_FORMAT: getSetting( + "ELEVENLABS_OUTPUT_FORMAT", + "mp3_44100_128", + ), ELEVENLABS_LANGUAGE_CODE: getSetting("ELEVENLABS_LANGUAGE_CODE", "en"), // STT ELEVENLABS_STT_MODEL_ID: getSetting("ELEVENLABS_STT_MODEL_ID", "scribe_v1"), - ELEVENLABS_STT_LANGUAGE_CODE: getSetting("ELEVENLABS_STT_LANGUAGE_CODE", "en"), + ELEVENLABS_STT_LANGUAGE_CODE: getSetting( + "ELEVENLABS_STT_LANGUAGE_CODE", + "en", + ), ELEVENLABS_STT_TIMESTAMPS_GRANULARITY: getSetting( "ELEVENLABS_STT_TIMESTAMPS_GRANULARITY", "word", ), ELEVENLABS_STT_DIARIZE: getSetting("ELEVENLABS_STT_DIARIZE", "false"), - ELEVENLABS_STT_TAG_AUDIO_EVENTS: getSetting("ELEVENLABS_STT_TAG_AUDIO_EVENTS", "false"), + ELEVENLABS_STT_TAG_AUDIO_EVENTS: getSetting( + "ELEVENLABS_STT_TAG_AUDIO_EVENTS", + "false", + ), }; // Optional: ELEVENLABS_STT_NUM_SPEAKERS const numSpeakers = - charSettings.ELEVENLABS_STT_NUM_SPEAKERS || process.env.ELEVENLABS_STT_NUM_SPEAKERS; + charSettings.ELEVENLABS_STT_NUM_SPEAKERS || + process.env.ELEVENLABS_STT_NUM_SPEAKERS; if (numSpeakers) { settings.ELEVENLABS_STT_NUM_SPEAKERS = String(numSpeakers); } diff --git a/packages/lib/eliza/knowledge-service.ts b/packages/lib/eliza/knowledge-service.ts index ad06a1a97..54d3d13ba 100644 --- a/packages/lib/eliza/knowledge-service.ts +++ b/packages/lib/eliza/knowledge-service.ts @@ -31,6 +31,8 @@ export async function getKnowledgeService( return null; } -export async function hasKnowledgeService(runtime: AgentRuntime): Promise { +export async function hasKnowledgeService( + runtime: AgentRuntime, +): Promise { return (await getKnowledgeService(runtime)) !== null; } diff --git a/packages/lib/eliza/message-handler.ts b/packages/lib/eliza/message-handler.ts index 02ff0fa6f..4e3f01eb7 100644 --- a/packages/lib/eliza/message-handler.ts +++ b/packages/lib/eliza/message-handler.ts @@ -53,7 +53,10 @@ export interface MessageResult { usage?: UsageInfo; } -export type StreamChunkCallback = (chunk: string, messageId?: UUID) => Promise; +export type StreamChunkCallback = ( + chunk: string, + messageId?: UUID, +) => Promise; export type ReasoningChunkCallback = ( chunk: string, @@ -79,7 +82,14 @@ export class MessageHandler { ) {} async process(options: MessageOptions): Promise { - const { roomId, text, attachments, agentModeConfig, onStreamChunk, onReasoningChunk } = options; + const { + roomId, + text, + attachments, + agentModeConfig, + onStreamChunk, + onReasoningChunk, + } = options; const entityId = this.userContext.userId; const modeConfig = agentModeConfig || DEFAULT_AGENT_MODE; @@ -99,7 +109,10 @@ export class MessageHandler { const callback = async (content: Content) => { if (content.text) { responseMemory = { - id: createUniqueUuid(this.runtime, (userMessage.id ?? uuidv4()) as UUID), + id: createUniqueUuid( + this.runtime, + (userMessage.id ?? uuidv4()) as UUID, + ), entityId: this.runtime.agentId, agentId: this.runtime.agentId, roomId: roomId as UUID, @@ -173,7 +186,10 @@ export class MessageHandler { if (!responseMemory && result && result.responseContent) { responseMemory = { - id: createUniqueUuid(this.runtime, (userMessage.id ?? uuidv4()) as UUID), + id: createUniqueUuid( + this.runtime, + (userMessage.id ?? uuidv4()) as UUID, + ), entityId: this.runtime.agentId, agentId: this.runtime.agentId, roomId: roomId as UUID, @@ -227,20 +243,32 @@ export class MessageHandler { typeof responseMemory.content === "string" ? responseMemory.content : responseMemory.content?.text || ""; - this.sendToDiscordThread(roomId, text, responseText, options.characterId).catch((e) => { - elizaLogger.warn(`[MessageHandler] Discord thread sync failed for room ${roomId}: ${e}`); + this.sendToDiscordThread( + roomId, + text, + responseText, + options.characterId, + ).catch((e) => { + elizaLogger.warn( + `[MessageHandler] Discord thread sync failed for room ${roomId}: ${e}`, + ); }); // PERF: Fire-and-forget room title generation -- don't block the response. // Title generation makes a separate LLM call (1-3s) that the user doesn't need to wait for. generateRoomTitle(roomId).catch((e) => { - elizaLogger.warn(`[MessageHandler] Room title generation failed for room ${roomId}: ${e}`); + elizaLogger.warn( + `[MessageHandler] Room title generation failed for room ${roomId}: ${e}`, + ); }); return { message: responseMemory, usage }; } - private async ensureConnectionForCloud(roomId: string, entityId: string): Promise { + private async ensureConnectionForCloud( + roomId: string, + entityId: string, + ): Promise { if (await connectionCache.isEstablished(roomId, entityId)) return; const entityUuid = stringToUuid(entityId) as UUID; @@ -249,10 +277,15 @@ export class MessageHandler { const serverId = stringToUuid("eliza-server") as UUID; const displayName = - this.userContext.name || this.userContext.email || this.userContext.userId || "User"; - const names = [this.userContext.name, this.userContext.email, displayName].filter( - Boolean, - ) as string[]; + this.userContext.name || + this.userContext.email || + this.userContext.userId || + "User"; + const names = [ + this.userContext.name, + this.userContext.email, + displayName, + ].filter(Boolean) as string[]; await Promise.all([ this.ensureWorldExists(worldId, serverId), @@ -270,7 +303,10 @@ export class MessageHandler { }); } - private async ensureWorldExists(worldId: UUID, serverId: UUID): Promise { + private async ensureWorldExists( + worldId: UUID, + serverId: UUID, + ): Promise { try { await this.runtime.ensureWorldExists({ id: worldId, @@ -370,9 +406,9 @@ export class MessageHandler { } } } else { - const mergedNames = [...new Set([...(existingEntity.names || []), ...names])].filter( - Boolean, - ) as string[]; + const mergedNames = [ + ...new Set([...(existingEntity.names || []), ...names]), + ].filter(Boolean) as string[]; const mergedMetadata = { ...existingEntity.metadata, web: { @@ -396,13 +432,18 @@ export class MessageHandler { } } - private async ensureParticipants(roomId: UUID, entityUuid: UUID): Promise { + private async ensureParticipants( + roomId: UUID, + entityUuid: UUID, + ): Promise { await Promise.all([ - this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId).catch((e) => { - elizaLogger.warn( - `[MessageHandler] Agent participant setup failed for room ${roomId} - messages may not be attributed correctly: ${e}`, - ); - }), + this.runtime + .ensureParticipantInRoom(this.runtime.agentId, roomId) + .catch((e) => { + elizaLogger.warn( + `[MessageHandler] Agent participant setup failed for room ${roomId} - messages may not be attributed correctly: ${e}`, + ); + }), this.runtime.ensureParticipantInRoom(entityUuid, roomId).catch((e) => { elizaLogger.warn( `[MessageHandler] User participant setup failed for entity ${entityUuid} in room ${roomId}: ${e}`, @@ -449,7 +490,9 @@ export class MessageHandler { private async incrementAnonymousMessageCount(): Promise { if (!this.userContext.sessionToken) return; - const session = await anonymousSessionsService.getByToken(this.userContext.sessionToken); + const session = await anonymousSessionsService.getByToken( + this.userContext.sessionToken, + ); if (session) { await anonymousSessionsService.incrementMessageCount(session.id); @@ -463,7 +506,9 @@ export class MessageHandler { characterId?: string, ): Promise { const room = await roomsRepository.findById(roomId); - const roomMetadata = room?.metadata as { discordThreadId?: string } | undefined; + const roomMetadata = room?.metadata as + | { discordThreadId?: string } + | undefined; const threadId = roomMetadata?.discordThreadId; if (!threadId) return; @@ -477,7 +522,10 @@ export class MessageHandler { threadId, `**${this.userContext.name || this.userContext.email || this.userContext.entityId}:** ${userText}`, ); - await discordService.sendToThread(threadId, `**🤖 ${characterName}:** ${agentResponse}`); + await discordService.sendToThread( + threadId, + `**🤖 ${characterName}:** ${agentResponse}`, + ); } } diff --git a/packages/lib/eliza/model-preferences.ts b/packages/lib/eliza/model-preferences.ts index 8ae51f210..95ce80bb2 100644 --- a/packages/lib/eliza/model-preferences.ts +++ b/packages/lib/eliza/model-preferences.ts @@ -28,7 +28,9 @@ export interface ModelPreferences { mediaDescriptionModel?: string; } -export function sanitizeModelPreferences(value: unknown): ModelPreferences | undefined { +export function sanitizeModelPreferences( + value: unknown, +): ModelPreferences | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) { return undefined; } diff --git a/packages/lib/eliza/plugin-advanced-memory-storage.ts b/packages/lib/eliza/plugin-advanced-memory-storage.ts index c6591208e..827a43fcb 100644 --- a/packages/lib/eliza/plugin-advanced-memory-storage.ts +++ b/packages/lib/eliza/plugin-advanced-memory-storage.ts @@ -3,7 +3,8 @@ import { AdvancedMemoryStorageService } from "./advanced-memory-storage-service" export const advancedMemoryStoragePlugin: Plugin = { name: "advanced-memory-storage", - description: "Registers plugin-sql advanced-memory storage for cloud runtimes", + description: + "Registers plugin-sql advanced-memory storage for cloud runtimes", services: [AdvancedMemoryStorageService], }; diff --git a/packages/lib/eliza/plugin-advanced-memory.ts b/packages/lib/eliza/plugin-advanced-memory.ts index 6338858b7..1d66b3aac 100644 --- a/packages/lib/eliza/plugin-advanced-memory.ts +++ b/packages/lib/eliza/plugin-advanced-memory.ts @@ -1,6 +1,7 @@ import type { Plugin } from "@elizaos/core"; import { createAdvancedMemoryPlugin } from "./advanced-memory"; -export const advancedMemoryPlugin = createAdvancedMemoryPlugin() as unknown as Plugin; +export const advancedMemoryPlugin = + createAdvancedMemoryPlugin() as unknown as Plugin; export default advancedMemoryPlugin; diff --git a/packages/lib/eliza/plugin-affiliate/actions/image-generation.ts b/packages/lib/eliza/plugin-affiliate/actions/image-generation.ts index 8cf4b4214..e8588402f 100644 --- a/packages/lib/eliza/plugin-affiliate/actions/image-generation.ts +++ b/packages/lib/eliza/plugin-affiliate/actions/image-generation.ts @@ -33,7 +33,9 @@ function parseXmlSafe(input: string): ParsedXml { if (!parsed || typeof parsed !== "object") return {}; const result: ParsedXml = {}; - for (const [key, value] of Object.entries(parsed as Record)) { + for (const [key, value] of Object.entries( + parsed as Record, + )) { if (typeof value === "string") { result[key] = value; } @@ -49,7 +51,9 @@ function extractAffiliateImageConfig( referenceImageUrls: [], }; - const affiliateData = settings?.affiliateData as Partial | undefined; + const affiliateData = settings?.affiliateData as + | Partial + | undefined; if (!affiliateData) return result; const vibe = affiliateData.vibe; @@ -67,7 +71,9 @@ function extractAffiliateImageConfig( if (Array.isArray(imageUrls)) { result.referenceImageUrls = imageUrls.filter( (url): url is string => - typeof url === "string" && url.startsWith("http") && !url.startsWith("data:"), + typeof url === "string" && + url.startsWith("http") && + !url.startsWith("data:"), ); result.primaryImageUrl = result.referenceImageUrls[0]; } @@ -172,7 +178,9 @@ async function getOrExtractAppearanceDescription( const cacheKey = characterId || config.referenceImageUrls.join(","); const cached = appearanceDescriptionCache.get(cacheKey); if (cached) { - logger.info("[GENERATE_IMAGE] 📋 Using in-memory cached appearance description"); + logger.info( + "[GENERATE_IMAGE] 📋 Using in-memory cached appearance description", + ); return cached; } @@ -209,7 +217,8 @@ async function getOrExtractAppearanceDescription( visionResult !== null && "description" in visionResult ) { - const description = (visionResult as { description: unknown }).description; + const description = (visionResult as { description: unknown }) + .description; if (typeof description === "string") { descriptionText = description; } @@ -222,10 +231,16 @@ async function getOrExtractAppearanceDescription( if (gender === "woman" || gender === "man") { detectedGenders.push(gender); - logger.info(`[GENERATE_IMAGE] 👤 Image ${i + 1} detected gender: ${gender}`); + logger.info( + `[GENERATE_IMAGE] 👤 Image ${i + 1} detected gender: ${gender}`, + ); } - if (appearance && typeof appearance === "string" && appearance.length > 20) { + if ( + appearance && + typeof appearance === "string" && + appearance.length > 20 + ) { if ( !appearance.toLowerCase().startsWith("woman") && !appearance.toLowerCase().startsWith("man") && @@ -266,7 +281,9 @@ async function getOrExtractAppearanceDescription( } if (appearanceDescriptions.length === 0) { - logger.warn("[GENERATE_IMAGE] ⚠️ Could not extract appearance from any reference images"); + logger.warn( + "[GENERATE_IMAGE] ⚠️ Could not extract appearance from any reference images", + ); return null; } @@ -274,7 +291,9 @@ async function getOrExtractAppearanceDescription( if (appearanceDescriptions.length === 1) { finalAppearance = appearanceDescriptions[0]; } else { - logger.info("[GENERATE_IMAGE] 🧩 Combining appearance descriptions from multiple images..."); + logger.info( + "[GENERATE_IMAGE] 🧩 Combining appearance descriptions from multiple images...", + ); const genderInstruction = dominantGender ? `CRITICAL: This is a ${dominantGender.toUpperCase()}. Your description MUST start with "${dominantGender}".` : ""; @@ -303,17 +322,30 @@ Your response MUST be in this XML format: }); const combineParsed = parseXmlSafe(combineResponse); - finalAppearance = combineParsed.appearance || appearanceDescriptions.join(", "); + finalAppearance = + combineParsed.appearance || appearanceDescriptions.join(", "); } - if (dominantGender && !finalAppearance.toLowerCase().startsWith(dominantGender)) { + if ( + dominantGender && + !finalAppearance.toLowerCase().startsWith(dominantGender) + ) { const startsWithOtherGender = - (dominantGender === "woman" && finalAppearance.toLowerCase().startsWith("man")) || - (dominantGender === "man" && finalAppearance.toLowerCase().startsWith("woman")); + (dominantGender === "woman" && + finalAppearance.toLowerCase().startsWith("man")) || + (dominantGender === "man" && + finalAppearance.toLowerCase().startsWith("woman")); if (startsWithOtherGender) { - finalAppearance = finalAppearance.replace(/^(wo)?man,?\s*/i, `${dominantGender}, `); - logger.info(`[GENERATE_IMAGE] 🔄 Corrected gender mismatch to: ${dominantGender}`); - } else if (!finalAppearance.toLowerCase().startsWith("young " + dominantGender)) { + finalAppearance = finalAppearance.replace( + /^(wo)?man,?\s*/i, + `${dominantGender}, `, + ); + logger.info( + `[GENERATE_IMAGE] 🔄 Corrected gender mismatch to: ${dominantGender}`, + ); + } else if ( + !finalAppearance.toLowerCase().startsWith("young " + dominantGender) + ) { finalAppearance = `${dominantGender}, ${finalAppearance}`; logger.info(`[GENERATE_IMAGE] ➕ Prepended gender: ${dominantGender}`); } @@ -326,7 +358,9 @@ Your response MUST be in this XML format: return finalAppearance; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`[GENERATE_IMAGE] ❌ Failed to extract appearance: ${errorMessage}`); + logger.error( + `[GENERATE_IMAGE] ❌ Failed to extract appearance: ${errorMessage}`, + ); return null; } } @@ -341,7 +375,9 @@ interface AppearanceGenerationFallback { fallbackReason: string; } -type AppearanceResult = AppearanceGenerationConfig | AppearanceGenerationFallback; +type AppearanceResult = + | AppearanceGenerationConfig + | AppearanceGenerationFallback; async function prepareAppearanceBasedGeneration( runtime: IAgentRuntime, @@ -377,7 +413,8 @@ async function prepareAppearanceBasedGeneration( return { hasValidAppearance: false, - fallbackReason: "Failed to extract appearance description from reference images", + fallbackReason: + "Failed to extract appearance description from reference images", }; } @@ -393,11 +430,16 @@ function isBase64DataUrl(url: string): boolean { * This is CRITICAL for preventing token limit exhaustion * Retries up to 3 times with exponential backoff to ensure images are persisted */ -async function ensureBlobUrl(imageUrl: string, userId?: string): Promise { +async function ensureBlobUrl( + imageUrl: string, + userId?: string, +): Promise { if (!isBase64DataUrl(imageUrl)) { // Check if it's a Fal.ai URL - if so, upload to our storage if (isFalAiUrl(imageUrl)) { - logger.info("[GENERATE_IMAGE] Fal.ai URL detected, uploading to our storage..."); + logger.info( + "[GENERATE_IMAGE] Fal.ai URL detected, uploading to our storage...", + ); try { const timestamp = Date.now(); const ourUrl = await ensureElizaCloudUrl(imageUrl, { @@ -407,7 +449,9 @@ async function ensureBlobUrl(imageUrl: string, userId?: string): Promise { // STRICT validation - only trigger for EXPLICIT image requests const text = message?.content?.text?.toLowerCase() || ""; @@ -759,7 +815,9 @@ export const generateImageAction = { ]; // Check if any keyword matches - const hasImageRequest = imageRequestKeywords.some((keyword) => text.includes(keyword)); + const hasImageRequest = imageRequestKeywords.some((keyword) => + text.includes(keyword), + ); if (!hasImageRequest) { logger.debug( @@ -779,7 +837,8 @@ export const generateImageAction = { callback: HandlerCallback, responses?: Memory[], ): Promise => { - const allProviders = responses?.flatMap((res) => res.content?.providers ?? []) ?? []; + const allProviders = + responses?.flatMap((res) => res.content?.providers ?? []) ?? []; if (allProviders.length > 0) { state.values = { ...state.values, additionalProviders: allProviders }; } @@ -791,7 +850,11 @@ export const generateImageAction = { const affiliateConfig = extractAffiliateImageConfig( (() => { const settings = runtime.character?.settings; - if (settings && typeof settings === "object" && !Array.isArray(settings)) { + if ( + settings && + typeof settings === "object" && + !Array.isArray(settings) + ) { return settings as Record; } return undefined; @@ -844,7 +907,9 @@ export const generateImageAction = { const detectedGender = isWoman ? "woman" : isMan ? "man" : null; if (detectedGender) { - logger.info(`[GENERATE_IMAGE] 👤 Gender from appearance: ${detectedGender}`); + logger.info( + `[GENERATE_IMAGE] 👤 Gender from appearance: ${detectedGender}`, + ); } if (!imagePrompt || imagePrompt.length < 20) { @@ -854,7 +919,9 @@ export const generateImageAction = { .substring(0, 40) .toLowerCase() .split(",") - .some((part) => imagePrompt.toLowerCase().includes(part.trim().toLowerCase())); + .some((part) => + imagePrompt.toLowerCase().includes(part.trim().toLowerCase()), + ); if (!hasAppearance) { imagePrompt = `photorealistic portrait photo of a ${appearance}, ${imagePrompt}`; } else { @@ -877,7 +944,9 @@ export const generateImageAction = { if (!imagePrompt.toLowerCase().includes(detectedGender)) { imagePrompt = `${detectedGender}, ${imagePrompt}`; - logger.info(`[GENERATE_IMAGE] ➕ Prepended gender to prompt: ${detectedGender}`); + logger.info( + `[GENERATE_IMAGE] ➕ Prepended gender to prompt: ${detectedGender}`, + ); } } @@ -893,8 +962,14 @@ export const generateImageAction = { prompt: imagePrompt, }); - if (!imageResponse || imageResponse.length === 0 || !imageResponse[0]?.url) { - logger.error("[GENERATE_IMAGE] ❌ Image generation failed - no response from model"); + if ( + !imageResponse || + imageResponse.length === 0 || + !imageResponse[0]?.url + ) { + logger.error( + "[GENERATE_IMAGE] ❌ Image generation failed - no response from model", + ); return { text: "I couldn't generate an image right now, let's chat instead! 💬", values: { @@ -911,12 +986,16 @@ export const generateImageAction = { } const rawImageUrl = imageResponse[0].url; - logger.info(`[GENERATE_IMAGE] ✅ Generated synthetic image successfully`); + logger.info( + `[GENERATE_IMAGE] ✅ Generated synthetic image successfully`, + ); let blobUrl: string | null = null; try { blobUrl = await ensureBlobUrl(rawImageUrl); - logger.info(`[GENERATE_IMAGE] 📦 Uploaded to blob: ${blobUrl?.substring(0, 60)}...`); + logger.info( + `[GENERATE_IMAGE] 📦 Uploaded to blob: ${blobUrl?.substring(0, 60)}...`, + ); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); logger.warn(`[GENERATE_IMAGE] ⚠️ Blob upload failed: ${errorMessage}`); @@ -942,18 +1021,21 @@ export const generateImageAction = { "Hey you 😘 I'd love to know more about you! What's something that makes you smile?", shy: "Oh hi! 😊 I'm a bit nervous but... I'd really like to get to know you better. What do you like to do? 🌸", bold: "I like your energy 🔥 So tell me - what's the most interesting thing about you?", - spicy: "Mmm I'm intrigued 😈 What gets you excited? I want to know everything about you", + spicy: + "Mmm I'm intrigued 😈 What gets you excited? I want to know everything about you", romantic: "Hey there 💕 I'd love to hear about your day. What's been on your mind lately? 💖", playful: "Heyyy! 🎉 What fun stuff are you up to? Tell me something random about yourself! ✨", - mysterious: "Hey... 🌙 I'm curious about you. What brought you here tonight?", + mysterious: + "Hey... 🌙 I'm curious about you. What brought you here tonight?", intellectual: "Hi there ✨ I'm curious - what's something you're really passionate about?", }; const defaultCaption = - affiliateConfig.vibe && defaultCaptions[affiliateConfig.vibe.toLowerCase()] + affiliateConfig.vibe && + defaultCaptions[affiliateConfig.vibe.toLowerCase()] ? defaultCaptions[affiliateConfig.vibe.toLowerCase()] : "Hey! 😊 I'd love to get to know you better. Tell me something about yourself!"; @@ -971,13 +1053,18 @@ export const generateImageAction = { } } } catch { - logger.warn("[GENERATE_IMAGE] Failed to generate caption, using vibe-specific default"); + logger.warn( + "[GENERATE_IMAGE] Failed to generate caption, using vibe-specific default", + ); } logger.info(`[GENERATE_IMAGE] 💬 Caption: "${caption}"`); const attachmentId = v4(); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); const displayAttachments = [ { id: attachmentId, @@ -995,7 +1082,9 @@ export const generateImageAction = { text: caption, }; - logger.info(`[GENERATE_IMAGE] 📤 Sending generated image to callback...`); + logger.info( + `[GENERATE_IMAGE] 📤 Sending generated image to callback...`, + ); await callback(responseContent); logger.info(`[GENERATE_IMAGE] ✅ Generated image sent successfully`); @@ -1025,7 +1114,8 @@ export const generateImageAction = { const selectedTemplate = affiliateConfig.isAffiliateCharacter ? affiliateImageGenerationTemplate - : runtime.character.templates?.imageGenerationTemplate || imageGenerationTemplate; + : runtime.character.templates?.imageGenerationTemplate || + imageGenerationTemplate; // Add character info to state for the template const characterBio = runtime.character?.bio; @@ -1052,16 +1142,23 @@ export const generateImageAction = { const parsedXml = parseXmlSafe(promptResponse); - let imagePrompt = parsedXml.prompt || "Unable to generate descriptive prompt for image"; + let imagePrompt = + parsedXml.prompt || "Unable to generate descriptive prompt for image"; // For affiliate characters, ensure the prompt generates human selfies if (affiliateConfig.isAffiliateCharacter) { const lowerPrompt = imagePrompt.toLowerCase(); // Add selfie/human keywords if not present - if (!lowerPrompt.includes("selfie") && !lowerPrompt.includes("portrait")) { + if ( + !lowerPrompt.includes("selfie") && + !lowerPrompt.includes("portrait") + ) { imagePrompt = `photorealistic selfie, ${imagePrompt}`; } - if (!lowerPrompt.includes("photorealistic") && !lowerPrompt.includes("photo")) { + if ( + !lowerPrompt.includes("photorealistic") && + !lowerPrompt.includes("photo") + ) { imagePrompt = `photorealistic ${imagePrompt}`; } // Ensure human-related keywords @@ -1074,10 +1171,15 @@ export const generateImageAction = { imagePrompt = `${imagePrompt}, human person, natural face`; } // Add quality keywords - if (!lowerPrompt.includes("8k") && !lowerPrompt.includes("high quality")) { + if ( + !lowerPrompt.includes("8k") && + !lowerPrompt.includes("high quality") + ) { imagePrompt = `${imagePrompt}, high quality, detailed face, 8k`; } - logger.info(`[GENERATE_IMAGE] 🤳 Enhanced selfie prompt for affiliate character`); + logger.info( + `[GENERATE_IMAGE] 🤳 Enhanced selfie prompt for affiliate character`, + ); } const imageModelOptions: { @@ -1090,9 +1192,16 @@ export const generateImageAction = { `[GENERATE_IMAGE] 🎨 Generating new image with prompt: "${imagePrompt.substring(0, 100)}..."`, ); - const imageResponse = await runtime.useModel(ModelType.IMAGE, imageModelOptions); + const imageResponse = await runtime.useModel( + ModelType.IMAGE, + imageModelOptions, + ); - if (!imageResponse || imageResponse.length === 0 || !imageResponse[0]?.url) { + if ( + !imageResponse || + imageResponse.length === 0 || + !imageResponse[0]?.url + ) { logger.error( { imageResponse, @@ -1153,7 +1262,10 @@ export const generateImageAction = { const urlPath = new URL(url).pathname; const extension = urlPath.split(".").pop()?.toLowerCase(); // Common image extensions - if (extension && ["png", "jpg", "jpeg", "gif", "webp", "bmp"].includes(extension)) { + if ( + extension && + ["png", "jpg", "jpeg", "gif", "webp", "bmp"].includes(extension) + ) { return extension; } // Extension not in allowed list, fall through to default @@ -1162,7 +1274,10 @@ export const generateImageAction = { // Create shared attachment data to avoid duplication const extension = getFileExtension(imageUrl); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); const fileName = `Generated_Image_${timestamp}.${extension}`; const attachmentId = v4(); @@ -1197,7 +1312,9 @@ export const generateImageAction = { text: "", // No text needed - the image speaks for itself }; - logger.info(`[GENERATE_IMAGE] 📤 Invoking callback with responseContent...`); + logger.info( + `[GENERATE_IMAGE] 📤 Invoking callback with responseContent...`, + ); await callback(responseContent); logger.info(`[GENERATE_IMAGE] ✅ Callback completed`); diff --git a/packages/lib/eliza/plugin-affiliate/handler.ts b/packages/lib/eliza/plugin-affiliate/handler.ts index c8c2467cc..a02d6c0a2 100644 --- a/packages/lib/eliza/plugin-affiliate/handler.ts +++ b/packages/lib/eliza/plugin-affiliate/handler.ts @@ -28,7 +28,11 @@ import { postProcessResponse, runEvaluatorsWithTimeout, } from "../shared/utils/helpers"; -import { canRespondImmediately, type ParsedPlan, parsePlannedItems } from "../shared/utils/parsers"; +import { + canRespondImmediately, + type ParsedPlan, + parsePlannedItems, +} from "../shared/utils/parsers"; import { clearLatestResponseId, isResponseStillValid, @@ -123,7 +127,9 @@ export async function handleMessage({ [key: string]: unknown; } | undefined; - const isAffiliateChat = !!(affiliateData && Object.keys(affiliateData).length > 0); + const isAffiliateChat = !!( + affiliateData && Object.keys(affiliateData).length > 0 + ); const shouldAutoGenerateImages = affiliateData?.autoImage === true; const providers = isAffiliateChat @@ -153,7 +159,8 @@ export async function handleMessage({ const planningPrompt = cleanPrompt( composePromptFromState({ state: initialState, - template: runtime.character.templates?.planningTemplate || planningTemplate, + template: + runtime.character.templates?.planningTemplate || planningTemplate, }), ); @@ -167,7 +174,9 @@ export async function handleMessage({ const planningResponse = await generatePlanningWithStreaming( runtime, planningPrompt, - onReasoningChunk ? { onReasoningChunk, messageId: responseId as UUID } : undefined, + onReasoningChunk + ? { onReasoningChunk, messageId: responseId as UUID } + : undefined, ); runtime.character.system = originalSystemPrompt; @@ -192,7 +201,9 @@ export async function handleMessage({ }; } else { if (!plan.actions?.includes("GENERATE_IMAGE")) { - plan.actions = plan.actions ? `${plan.actions}, GENERATE_IMAGE` : "GENERATE_IMAGE"; + plan.actions = plan.actions + ? `${plan.actions}, GENERATE_IMAGE` + : "GENERATE_IMAGE"; } plan.canRespondNow = "NO"; } @@ -209,7 +220,10 @@ export async function handleMessage({ // Stream in reasonable chunks - frontend typewriter will smooth it out const chunkSize = 20; for (let i = 0; i < responseContent.length; i += chunkSize) { - await onStreamChunk(responseContent.slice(i, i + chunkSize), responseId as UUID); + await onStreamChunk( + responseContent.slice(i, i + chunkSize), + responseId as UUID, + ); } } } else { @@ -219,7 +233,12 @@ export async function handleMessage({ const plannedProviders = parsePlannedItems(plan?.providers); const plannedActions = parsePlannedItems(plan?.actions); - updatedState = await executeProviders(runtime, message, plannedProviders, updatedState); + updatedState = await executeProviders( + runtime, + message, + plannedProviders, + updatedState, + ); updatedState = await executeActions( runtime, message, @@ -273,7 +292,9 @@ export async function handleMessage({ const responsePrompt = cleanPrompt( composePromptFromState({ state: updatedState, - template: runtime.character.templates?.messageHandlerTemplate || responseTemplate, + template: + runtime.character.templates?.messageHandlerTemplate || + responseTemplate, }), ); @@ -312,10 +333,15 @@ export async function handleMessage({ // Collect attachments from action results and cache const actionResults = await runtime.getActionResults(message.id as UUID); const actionResultAttachments = extractAttachments(actionResults); - const cachedAttachments = getAndClearCachedAttachments(message.roomId as string); + const cachedAttachments = getAndClearCachedAttachments( + message.roomId as string, + ); // Dedupe attachments by ID, preferring cached (validated HTTP URLs) - const attachmentMap = new Map(); + const attachmentMap = new Map< + string, + { id: string; url: string; contentType?: string } + >(); for (const att of [...actionResultAttachments, ...cachedAttachments]) { if (att && typeof att === "object" && "id" in att && "url" in att) { const { id, url, contentType } = att as { @@ -337,7 +363,9 @@ export async function handleMessage({ // Ensure we have a response - if generation failed, provide a fallback if (!responseContent || responseContent.trim() === "") { - logger.warn("[Assistant] Response generation failed - using fallback response"); + logger.warn( + "[Assistant] Response generation failed - using fallback response", + ); responseContent = mediaAttachments.length > 0 ? "Here you go! 😊" @@ -345,7 +373,10 @@ export async function handleMessage({ } // Post-process response to remove AI-speak and track openings - const processedResponse = postProcessResponse(responseContent, message.roomId as string); + const processedResponse = postProcessResponse( + responseContent, + message.roomId as string, + ); const finalText = processedResponse.text; const content: Content = { @@ -373,7 +404,13 @@ export async function handleMessage({ } as DialogueMetadata, }; - await runEvaluatorsWithTimeout(runtime, message, initialState, responseMemory, callback); + await runEvaluatorsWithTimeout( + runtime, + message, + initialState, + responseMemory, + callback, + ); const endTime = Date.now(); await runtime.emitEvent(EventType.RUN_ENDED, { diff --git a/packages/lib/eliza/plugin-affiliate/index.ts b/packages/lib/eliza/plugin-affiliate/index.ts index 70bf17f5a..0a6fe7265 100644 --- a/packages/lib/eliza/plugin-affiliate/index.ts +++ b/packages/lib/eliza/plugin-affiliate/index.ts @@ -1,9 +1,17 @@ -import { EventType, logger, type MessagePayload, type Plugin } from "@elizaos/core"; +import { + EventType, + logger, + type MessagePayload, + type Plugin, +} from "@elizaos/core"; import { roomTitleEvaluator } from "../shared/evaluators/room-title"; import { appConfigProvider } from "../shared/providers/app-config"; import { characterProvider } from "../shared/providers/character"; import { recentMessagesProvider } from "../shared/providers/recent-messages"; -import type { ReasoningChunkCallback, StreamChunkCallback } from "../shared/types"; +import type { + ReasoningChunkCallback, + StreamChunkCallback, +} from "../shared/types"; import { generateImageAction } from "./actions/image-generation"; import { handleMessage } from "./handler"; import { actionsProvider } from "./providers/actions"; @@ -20,7 +28,8 @@ import { providersProvider } from "./providers/providers"; */ export const affiliatePlugin: Plugin = { name: "eliza-affiliate", - description: "Affiliate character handler with auto-image generation for miniapps", + description: + "Affiliate character handler with auto-image generation for miniapps", events: { [EventType.MESSAGE_RECEIVED]: [ async (payload: MessagePayload) => { diff --git a/packages/lib/eliza/plugin-affiliate/providers/actions.ts b/packages/lib/eliza/plugin-affiliate/providers/actions.ts index 443b149ea..32146e3b1 100644 --- a/packages/lib/eliza/plugin-affiliate/providers/actions.ts +++ b/packages/lib/eliza/plugin-affiliate/providers/actions.ts @@ -1,4 +1,10 @@ -import type { Action, IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; +import type { + Action, + IAgentRuntime, + Memory, + Provider, + State, +} from "@elizaos/core"; import { addHeader, formatActions } from "@elizaos/core"; /** diff --git a/packages/lib/eliza/plugin-affiliate/providers/affiliate-context.ts b/packages/lib/eliza/plugin-affiliate/providers/affiliate-context.ts index 6b4e6ac9b..af2b2da58 100644 --- a/packages/lib/eliza/plugin-affiliate/providers/affiliate-context.ts +++ b/packages/lib/eliza/plugin-affiliate/providers/affiliate-context.ts @@ -162,15 +162,21 @@ export const affiliateContextProvider: Provider = { // Vibe personality if (vibe && VIBE_PERSONALITIES[vibe]) { const vibeConfig = VIBE_PERSONALITIES[vibe]; - contextLines.push(`[VIBE: ${vibe.toUpperCase()}] ${vibeConfig.description}`); - contextLines.push(`Style: ${vibeConfig.behaviors.slice(0, 3).join("; ")}`); + contextLines.push( + `[VIBE: ${vibe.toUpperCase()}] ${vibeConfig.description}`, + ); + contextLines.push( + `Style: ${vibeConfig.behaviors.slice(0, 3).join("; ")}`, + ); contextLines.push(""); } // Backstory (truncated) if (backstory?.trim()) { const short = backstory.trim().slice(0, 200); - contextLines.push(`[Backstory] ${short}${backstory.length > 200 ? "..." : ""}`); + contextLines.push( + `[Backstory] ${short}${backstory.length > 200 ? "..." : ""}`, + ); contextLines.push(""); } diff --git a/packages/lib/eliza/plugin-affiliate/providers/current-run-context.ts b/packages/lib/eliza/plugin-affiliate/providers/current-run-context.ts index aa7ee4aa2..bec20802c 100644 --- a/packages/lib/eliza/plugin-affiliate/providers/current-run-context.ts +++ b/packages/lib/eliza/plugin-affiliate/providers/current-run-context.ts @@ -66,7 +66,10 @@ export const currentRunContextProvider: Provider = { const runIdShort = String(runId).slice(0, 8); const headerText = `**Current Run** (ID: ${runIdShort})\n\n${formattedActions}`; - const currentRunActionResults = addHeader("# Current Run Action Results", headerText); + const currentRunActionResults = addHeader( + "# Current Run Action Results", + headerText, + ); return { values: { diff --git a/packages/lib/eliza/plugin-affiliate/providers/providers.ts b/packages/lib/eliza/plugin-affiliate/providers/providers.ts index af2e433c4..fdb61612d 100644 --- a/packages/lib/eliza/plugin-affiliate/providers/providers.ts +++ b/packages/lib/eliza/plugin-affiliate/providers/providers.ts @@ -18,12 +18,15 @@ import { addHeader } from "@elizaos/core"; */ export const providersProvider: Provider = { name: "PROVIDERS", - description: "List of all data providers the agent can use to get additional information", + description: + "List of all data providers the agent can use to get additional information", get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => { const allProviders = runtime.providers; // Filter providers with dynamic: true - const dynamicProviders = allProviders.filter((provider) => provider.dynamic === true); + const dynamicProviders = allProviders.filter( + (provider) => provider.dynamic === true, + ); // Create formatted text for each provider const dynamicDescriptions = dynamicProviders.map((provider) => { @@ -41,7 +44,10 @@ export const providersProvider: Provider = { const dynamicSection = dynamicDescriptions.length > 0 ? addHeader(headerText, dynamicDescriptions.join("\n")) - : addHeader(headerText, "No dynamic providers are currently available."); + : addHeader( + headerText, + "No dynamic providers are currently available.", + ); const providersWithDescriptions = addHeader( "# Available Providers", diff --git a/packages/lib/eliza/plugin-character-builder/actions/avatar-generation.ts b/packages/lib/eliza/plugin-character-builder/actions/avatar-generation.ts index b65de1d5b..af99e49db 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/avatar-generation.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/avatar-generation.ts @@ -82,7 +82,11 @@ export const generateAvatarAction = { - User says "give me an avatar", "create avatar", "generate profile pic" - User describes how their character should look Returns the avatar URL for immediate preview and update.`, - validate: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { return true; }, handler: async ( @@ -96,10 +100,13 @@ Returns the avatar URL for immediate preview and update.`, const creatorMode = isCreatorMode(runtime); const modeLabel = creatorMode ? "Creator" : "Build"; - logger.info(`[GENERATE_AVATAR] ${modeLabel} mode - Generating character avatar`); + logger.info( + `[GENERATE_AVATAR] ${modeLabel} mode - Generating character avatar`, + ); // Compose state with character context - const allProviders = responses?.flatMap((res) => res.content?.providers ?? []) ?? []; + const allProviders = + responses?.flatMap((res) => res.content?.providers ?? []) ?? []; state = await runtime.composeState(message, [ ...(allProviders ?? []), @@ -124,7 +131,8 @@ Returns the avatar URL for immediate preview and update.`, // Generate the avatar prompt const promptTemplate = - runtime.character.templates?.avatarGenerationTemplate || avatarPromptTemplate; + runtime.character.templates?.avatarGenerationTemplate || + avatarPromptTemplate; const prompt = composePromptFromState({ state, @@ -149,14 +157,20 @@ Returns the avatar URL for immediate preview and update.`, const thought = parsed?.thought?.trim() || "Generating character avatar"; const responseText = parsed?.text?.trim() || "Here's the avatar I created!"; - logger.info(`[GENERATE_AVATAR] Generated prompt: ${avatarPrompt.substring(0, 100)}...`); + logger.info( + `[GENERATE_AVATAR] Generated prompt: ${avatarPrompt.substring(0, 100)}...`, + ); // Generate the avatar image const imageResponse = await runtime.useModel(ModelType.IMAGE, { prompt: avatarPrompt, }); - if (!imageResponse || imageResponse.length === 0 || !imageResponse[0]?.url) { + if ( + !imageResponse || + imageResponse.length === 0 || + !imageResponse[0]?.url + ) { logger.error( "[GENERATE_AVATAR] Image generation failed - no valid response", JSON.stringify( @@ -196,7 +210,9 @@ Returns the avatar URL for immediate preview and update.`, } const avatarUrl = imageResponse[0].url; - logger.info(`[GENERATE_AVATAR] Avatar generated successfully: ${avatarUrl}`); + logger.info( + `[GENERATE_AVATAR] Avatar generated successfully: ${avatarUrl}`, + ); // Auto-save avatar in build mode (existing character) let avatarSaved = false; @@ -205,7 +221,9 @@ Returns the avatar URL for immediate preview and update.`, const userId = runtime.getSetting("USER_ID") as string; if (userId) { - logger.info(`[GENERATE_AVATAR] Auto-saving avatar for character ${runtime.character.id}`); + logger.info( + `[GENERATE_AVATAR] Auto-saving avatar for character ${runtime.character.id}`, + ); const savedCharacter = await charactersService.updateForUser( runtime.character.id as string, @@ -227,13 +245,18 @@ Returns the avatar URL for immediate preview and update.`, ); } } else { - logger.warn(`[GENERATE_AVATAR] Cannot auto-save - no USER_ID in runtime settings`); + logger.warn( + `[GENERATE_AVATAR] Cannot auto-save - no USER_ID in runtime settings`, + ); } } // Create attachment for display const attachmentId = v4(); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); const fileName = `Avatar_${timestamp}.png`; const attachments = [ diff --git a/packages/lib/eliza/plugin-character-builder/actions/builder-chat.ts b/packages/lib/eliza/plugin-character-builder/actions/builder-chat.ts index 78d482104..ead40d7c3 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/builder-chat.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/builder-chat.ts @@ -109,7 +109,11 @@ DO NOT USE when: - User confirms they want to save → use CREATE_CHARACTER or SAVE_CHANGES This is your main tool for understanding what the user wants before taking action.`, - validate: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { return true; }, handler: async ( @@ -121,9 +125,13 @@ This is your main tool for understanding what the user wants before taking actio ): Promise => { const creatorMode = isCreatorMode(runtime); const modeLabel = creatorMode ? "Creator" : "Build"; - const onStreamChunk = options?.onStreamChunk as StreamChunkCallback | undefined; + const onStreamChunk = options?.onStreamChunk as + | StreamChunkCallback + | undefined; - logger.info(`[BUILDER_CHAT] ${modeLabel} mode conversation, streaming=${!!onStreamChunk}`); + logger.info( + `[BUILDER_CHAT] ${modeLabel} mode conversation, streaming=${!!onStreamChunk}`, + ); state = await runtime.composeState(message, [ "SUMMARIZED_CONTEXT", @@ -141,12 +149,16 @@ This is your main tool for understanding what the user wants before taking actio const originalSystemPrompt = runtime.character.system; - const systemTemplate = creatorMode ? creatorModeSystemPrompt : buildModeSystemPrompt; + const systemTemplate = creatorMode + ? creatorModeSystemPrompt + : buildModeSystemPrompt; runtime.character.system = cleanPrompt( composePromptFromState({ state, template: systemTemplate }), ); - const prompt = cleanPrompt(composePromptFromState({ state, template: chatTemplate })); + const prompt = cleanPrompt( + composePromptFromState({ state, template: chatTemplate }), + ); const response = await runtime.useModel(ModelType.TEXT_LARGE, { prompt }); runtime.character.system = originalSystemPrompt; diff --git a/packages/lib/eliza/plugin-character-builder/actions/create-character.ts b/packages/lib/eliza/plugin-character-builder/actions/create-character.ts index 645257fa6..579b1c381 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/create-character.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/create-character.ts @@ -48,12 +48,20 @@ export const createCharacterAction = { name: "CREATE_CHARACTER", description: "User has confirmed they want to save the character. ONLY use when: (1) a character definition exists in the UI with at least a name populated from previous SUGGEST_CHANGES, AND (2) user explicitly confirms with phrases like 'create it', 'save this', 'looks good', 'let's go'. Do NOT use if character JSON is empty or user is still exploring ideas.", - validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { if (!isCreatorMode(runtime)) return false; // Check if we have client character state with at least a name - const settings = runtime.character.settings as Record | undefined; - const clientState = settings?.clientCharacterState as ClientCharacterState | undefined; + const settings = runtime.character.settings as + | Record + | undefined; + const clientState = settings?.clientCharacterState as + | ClientCharacterState + | undefined; return Boolean(clientState?.name); }, @@ -91,7 +99,9 @@ export const createCharacterAction = { } // Get the client character state - this is what the UI currently shows - const clientState = settings.clientCharacterState as ClientCharacterState | undefined; + const clientState = settings.clientCharacterState as + | ClientCharacterState + | undefined; if (!clientState?.name) { logger.error("[CREATE_CHARACTER] No character state or name in UI"); @@ -141,7 +151,9 @@ export const createCharacterAction = { return; } - logger.info(`[CREATE_CHARACTER] Character created with ID: ${savedCharacter.id}`); + logger.info( + `[CREATE_CHARACTER] Character created with ID: ${savedCharacter.id}`, + ); // Lock the room - this creator session is complete const roomId = message.roomId; diff --git a/packages/lib/eliza/plugin-character-builder/actions/guide-onboarding.ts b/packages/lib/eliza/plugin-character-builder/actions/guide-onboarding.ts index 61f53e775..fbb7c38d1 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/guide-onboarding.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/guide-onboarding.ts @@ -70,7 +70,9 @@ After running once, it's disabled - use BUILDER_CHAT for follow-up questions.`, callback: HandlerCallback, ): Promise => { const entityId = message.entityId as string; - const _onStreamChunk = options?.onStreamChunk as StreamChunkCallback | undefined; + const _onStreamChunk = options?.onStreamChunk as + | StreamChunkCallback + | undefined; state = await runtime.composeState(message, ["RECENT_MESSAGES"]); @@ -80,7 +82,9 @@ After running once, it's disabled - use BUILDER_CHAT for follow-up questions.`, composePromptFromState({ state, template: onboardingSystemPrompt }), ); - const prompt = cleanPrompt(composePromptFromState({ state, template: onboardingTemplate })); + const prompt = cleanPrompt( + composePromptFromState({ state, template: onboardingTemplate }), + ); const response = await runtime.useModel(ModelType.TEXT_LARGE, { prompt }); runtime.character.system = originalSystemPrompt; diff --git a/packages/lib/eliza/plugin-character-builder/actions/save-changes.ts b/packages/lib/eliza/plugin-character-builder/actions/save-changes.ts index 4df485a53..e0784c358 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/save-changes.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/save-changes.ts @@ -44,7 +44,8 @@ function normalizeMessageExamples(raw: unknown): MessageExamples | null { const normalizedConversation: MessageExampleConversation = []; for (const message of conversation) { - if (!message || typeof message !== "object" || Array.isArray(message)) continue; + if (!message || typeof message !== "object" || Array.isArray(message)) + continue; interface MessageShape { name?: unknown; @@ -54,13 +55,19 @@ function normalizeMessageExamples(raw: unknown): MessageExamples | null { } const msg = message as MessageShape; - if (!("name" in msg) || typeof msg.name !== "string" || !msg.name) continue; + if (!("name" in msg) || typeof msg.name !== "string" || !msg.name) + continue; let content: { text: string; [key: string]: unknown }; - if (msg.content && typeof msg.content === "object" && !Array.isArray(msg.content)) { + if ( + msg.content && + typeof msg.content === "object" && + !Array.isArray(msg.content) + ) { const contentObj = msg.content as Record; - if (!("text" in contentObj) || typeof contentObj.text !== "string") continue; + if (!("text" in contentObj) || typeof contentObj.text !== "string") + continue; content = { text: contentObj.text, ...contentObj }; } else if (typeof msg.text === "string") { content = { text: msg.text }; @@ -206,7 +213,9 @@ function mapChangesToDbFormat( // Handle style.* nested updates const hasStyleUpdate = - "style.all" in changes || "style.chat" in changes || "style.post" in changes; + "style.all" in changes || + "style.chat" in changes || + "style.post" in changes; if (hasStyleUpdate) { interface StyleShape { @@ -235,7 +244,11 @@ export const saveChangesAction = { name: "SAVE_CHANGES", description: "User has confirmed they want to save changes to their existing character. Use when user says: 'yes', 'save it', 'apply changes', 'looks good', 'do it', 'update it'. Only available in build mode when editing an EXISTING character.", - validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { return !isCreatorMode(runtime); }, handler: async ( @@ -292,7 +305,8 @@ export const saveChangesAction = { const composedSystemPrompt = cleanPrompt( composePromptFromState({ state, template: extractSystemPrompt }), ); - runtime.character.system = composedSystemPrompt + messageExamplesFormatInstructions; + runtime.character.system = + composedSystemPrompt + messageExamplesFormatInstructions; const extractPrompt = composePromptFromState({ state, @@ -312,7 +326,9 @@ export const saveChangesAction = { } | null; if (!extraction?.changes) { - logger.error("[SAVE_CHANGES] Failed to extract changes from conversation"); + logger.error( + "[SAVE_CHANGES] Failed to extract changes from conversation", + ); await callback({ text: "I couldn't determine what changes to save. Could you be more specific about what you'd like to update?", error: true, @@ -322,7 +338,9 @@ export const saveChangesAction = { const changesObj: Record = JSON.parse(extraction.changes); - logger.info(`[SAVE_CHANGES] Saving changes: ${Object.keys(changesObj).join(", ")}`); + logger.info( + `[SAVE_CHANGES] Saving changes: ${Object.keys(changesObj).join(", ")}`, + ); // Merge changes with current character const updatedCharacter = { ...runtime.character }; @@ -330,15 +348,22 @@ export const saveChangesAction = { for (const [key, value] of Object.entries(changesObj)) { if (key.startsWith("style.")) { if (!updatedCharacter.style) { - updatedCharacter.style = {} as unknown as typeof updatedCharacter.style; + updatedCharacter.style = + {} as unknown as typeof updatedCharacter.style; } const styleProp = key.split(".")[1]; - if (styleProp === "all" || styleProp === "chat" || styleProp === "post") { - (updatedCharacter.style as Record)[styleProp] = value; + if ( + styleProp === "all" || + styleProp === "chat" || + styleProp === "post" + ) { + (updatedCharacter.style as Record)[styleProp] = + value; } } else if (key === "messageExamples") { const normalized = normalizeMessageExamples(value); - (updatedCharacter as Record).messageExamples = normalized || value; + (updatedCharacter as Record).messageExamples = + normalized || value; } else { (updatedCharacter as Record)[key] = value; } @@ -354,7 +379,9 @@ export const saveChangesAction = { } const dbUpdates = mapChangesToDbFormat(changesObj, characterRecord); - logger.debug(`[SAVE_CHANGES] DB updates: ${JSON.stringify(dbUpdates, null, 2)}`); + logger.debug( + `[SAVE_CHANGES] DB updates: ${JSON.stringify(dbUpdates, null, 2)}`, + ); // Save to database const savedCharacter = await charactersService.updateForUser( @@ -364,7 +391,9 @@ export const saveChangesAction = { ); if (!savedCharacter) { - logger.error(`[SAVE_CHANGES] Failed to save: access denied for user ${userId}`); + logger.error( + `[SAVE_CHANGES] Failed to save: access denied for user ${userId}`, + ); await callback({ text: "Unable to save: You may not have permission to update this character.", error: true, @@ -377,12 +406,21 @@ export const saveChangesAction = { await runtime.updateAgent(runtime.agentId, updatedCharacter); const fieldsUpdated = Object.keys(changesObj); - logger.info(`[SAVE_CHANGES] Successfully updated: ${fieldsUpdated.join(", ")}`); + logger.info( + `[SAVE_CHANGES] Successfully updated: ${fieldsUpdated.join(", ")}`, + ); // Generate confirmation in character's updated voice const originalSystemForConfirm = runtime.character.system; - const relevantFields = ["system", "bio", "adjectives", "topics", "style", "messageExamples"]; + const relevantFields = [ + "system", + "bio", + "adjectives", + "topics", + "style", + "messageExamples", + ]; const updatedCharacterForConfirm: Record = {}; for (const field of relevantFields) { const value = (updatedCharacter as Record)[field]; @@ -397,7 +435,11 @@ export const saveChangesAction = { values: { ...state.values, agentName: runtime.character.name, - updatedCharacterJson: JSON.stringify(updatedCharacterForConfirm, null, 2), + updatedCharacterJson: JSON.stringify( + updatedCharacterForConfirm, + null, + 2, + ), }, }, template: confirmSystemPrompt, @@ -415,7 +457,8 @@ export const saveChangesAction = { thought?: string; text?: string; } | null; - const confirmText = parsed?.text || `Changes saved! Updated ${fieldsUpdated.join(", ")}.`; + const confirmText = + parsed?.text || `Changes saved! Updated ${fieldsUpdated.join(", ")}.`; await callback({ thought: parsed?.thought || "", diff --git a/packages/lib/eliza/plugin-character-builder/actions/suggest-changes.ts b/packages/lib/eliza/plugin-character-builder/actions/suggest-changes.ts index 32e1fc5d3..c7177ce47 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/suggest-changes.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/suggest-changes.ts @@ -178,7 +178,9 @@ Note: This is the LIVE state from the user's form. If marked "(UNSAVED)", change * Expands dot notation keys into nested objects. * e.g., { "style.all": [...] } becomes { style: { all: [...] } } */ -function expandDotNotation(obj: Record): Record { +function expandDotNotation( + obj: Record, +): Record { const result: Record = {}; for (const [key, value] of Object.entries(obj)) { @@ -207,7 +209,11 @@ export const suggestChangesAction = { name: "SUGGEST_CHANGES", description: "User is asking about character design, requesting modifications, or needs guidance on best practices. Use for: 'make it funnier', 'improve the bio', 'how should I structure the system prompt?', 'add personality traits', 'what makes a good character?'. Provides expert guidance with field-level changes for interactive preview. Does NOT save changes.", - validate: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { return true; }, handler: async ( @@ -217,8 +223,12 @@ export const suggestChangesAction = { options: Record, callback: HandlerCallback, ): Promise => { - const onStreamChunk = options?.onStreamChunk as StreamChunkCallback | undefined; - logger.info(`[SUGGEST_CHANGES] Generating expert guidance, streaming=${!!onStreamChunk}`); + const onStreamChunk = options?.onStreamChunk as + | StreamChunkCallback + | undefined; + logger.info( + `[SUGGEST_CHANGES] Generating expert guidance, streaming=${!!onStreamChunk}`, + ); // Include both guides - agent determines what's relevant from conversation context state = await runtime.composeState(message, [ @@ -293,7 +303,9 @@ export const suggestChangesAction = { `[SUGGEST_CHANGES] Parsed changes for fields: ${Object.keys(parsed).join(", ")}`, ); } catch (_parseError) { - logger.warn("[SUGGEST_CHANGES] Failed to parse changes JSON, sending guidance only"); + logger.warn( + "[SUGGEST_CHANGES] Failed to parse changes JSON, sending guidance only", + ); changes = null; } } diff --git a/packages/lib/eliza/plugin-character-builder/actions/test-response.ts b/packages/lib/eliza/plugin-character-builder/actions/test-response.ts index dc5b96771..49eccacea 100644 --- a/packages/lib/eliza/plugin-character-builder/actions/test-response.ts +++ b/packages/lib/eliza/plugin-character-builder/actions/test-response.ts @@ -67,7 +67,11 @@ export const testResponseAction = { name: "TEST_RESPONSE", description: "User wants to test how the character would respond. Use when: 'how would you respond to...', 'test the character', 'let me see how they talk', 'show me how you'd answer', 'roleplay as the character'. Only available in build mode for existing characters. Simulates authentic character response.", - validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { return !isCreatorMode(runtime); }, handler: async ( @@ -77,8 +81,12 @@ export const testResponseAction = { options: Record, callback: HandlerCallback, ): Promise => { - const onStreamChunk = options?.onStreamChunk as StreamChunkCallback | undefined; - logger.info(`[TEST_RESPONSE] Generating character test response, streaming=${!!onStreamChunk}`); + const onStreamChunk = options?.onStreamChunk as + | StreamChunkCallback + | undefined; + logger.info( + `[TEST_RESPONSE] Generating character test response, streaming=${!!onStreamChunk}`, + ); // Verify we're in build mode if (isCreatorMode(runtime)) { diff --git a/packages/lib/eliza/plugin-character-builder/handler.ts b/packages/lib/eliza/plugin-character-builder/handler.ts index ecc5663bb..398825a81 100644 --- a/packages/lib/eliza/plugin-character-builder/handler.ts +++ b/packages/lib/eliza/plugin-character-builder/handler.ts @@ -23,9 +23,14 @@ import { isResponseStillValid, setLatestResponseId, } from "../shared/utils/response-tracking"; -import { buildModePlanningTemplate, buildModeSystemPrompt } from "./prompts/build-mode-prompts"; +import { + buildModePlanningTemplate, + buildModeSystemPrompt, +} from "./prompts/build-mode-prompts"; -function parsePlanningResponse(response: string): { thought: string; actions: string } | null { +function parsePlanningResponse( + response: string, +): { thought: string; actions: string } | null { const parsed = parseKeyValueXml(response) as { thought?: string; actions?: string; @@ -116,7 +121,8 @@ export async function handleMessage({ runtime.character.system = originalSystemPrompt; const plan = parsePlanningResponse(planningResponse); - const selectedAction = parsePlannedItems(plan?.actions)[0] || "BUILDER_CHAT"; + const selectedAction = + parsePlannedItems(plan?.actions)[0] || "BUILDER_CHAT"; // Create action response with thought and mode context const actionResponse: Memory = { @@ -150,11 +156,18 @@ export async function handleMessage({ onStreamChunk ? { onStreamChunk } : undefined, ); - if (!(await isResponseStillValid(runtime, message.roomId, responseId))) return; + if (!(await isResponseStillValid(runtime, message.roomId, responseId))) + return; await clearLatestResponseId(runtime, message.roomId); // Run evaluators asynchronously in background - await runEvaluatorsWithTimeout(runtime, message, state, actionResponse, callback); + await runEvaluatorsWithTimeout( + runtime, + message, + state, + actionResponse, + callback, + ); const endTime = Date.now(); await runtime.emitEvent(EventType.RUN_ENDED, { diff --git a/packages/lib/eliza/plugin-character-builder/index.ts b/packages/lib/eliza/plugin-character-builder/index.ts index 6d44d39d5..ab8740046 100644 --- a/packages/lib/eliza/plugin-character-builder/index.ts +++ b/packages/lib/eliza/plugin-character-builder/index.ts @@ -1,4 +1,9 @@ -import { EventType, logger, type MessagePayload, type Plugin } from "@elizaos/core"; +import { + EventType, + logger, + type MessagePayload, + type Plugin, +} from "@elizaos/core"; import { roomTitleEvaluator } from "../shared/evaluators/room-title"; import { characterProvider } from "../shared/providers/character"; import { recentMessagesProvider } from "../shared/providers/recent-messages"; @@ -42,8 +47,9 @@ export const characterBuilderPlugin: Plugin = { [EventType.MESSAGE_RECEIVED]: [ async (payload: MessagePayload) => { if (!payload.callback) return; - const onStreamChunk = (payload as MessagePayload & { onStreamChunk?: StreamChunkCallback }) - .onStreamChunk; + const onStreamChunk = ( + payload as MessagePayload & { onStreamChunk?: StreamChunkCallback } + ).onStreamChunk; logger.info( `[Builder] Message received in room ${payload.message.roomId}, streaming=${!!onStreamChunk}`, ); diff --git a/packages/lib/eliza/plugin-character-builder/providers/actions.ts b/packages/lib/eliza/plugin-character-builder/providers/actions.ts index 1b98e1220..f6187f58a 100644 --- a/packages/lib/eliza/plugin-character-builder/providers/actions.ts +++ b/packages/lib/eliza/plugin-character-builder/providers/actions.ts @@ -1,4 +1,10 @@ -import type { Action, IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; +import type { + Action, + IAgentRuntime, + Memory, + Provider, + State, +} from "@elizaos/core"; import { addHeader, formatActions } from "@elizaos/core"; /** diff --git a/packages/lib/eliza/plugin-character-builder/providers/character-guide.ts b/packages/lib/eliza/plugin-character-builder/providers/character-guide.ts index c1016e64d..bb4f074bc 100644 --- a/packages/lib/eliza/plugin-character-builder/providers/character-guide.ts +++ b/packages/lib/eliza/plugin-character-builder/providers/character-guide.ts @@ -142,7 +142,8 @@ Alex: Analysis paralysis. Pick reversible and iterate. export const characterGuideProvider: Provider = { name: "CHARACTER_GUIDE", - description: "Lightweight character field reference with JSON to prompt mapping", + description: + "Lightweight character field reference with JSON to prompt mapping", get: async (_runtime: IAgentRuntime, _message: Memory, _state: State) => { return { values: { diff --git a/packages/lib/eliza/plugin-character-builder/providers/current-character.ts b/packages/lib/eliza/plugin-character-builder/providers/current-character.ts index 2656a4056..49205d26a 100644 --- a/packages/lib/eliza/plugin-character-builder/providers/current-character.ts +++ b/packages/lib/eliza/plugin-character-builder/providers/current-character.ts @@ -34,10 +34,15 @@ interface ClientCharacterState { export const currentCharacterProvider: Provider = { name: "CURRENT_CHARACTER", - description: "Current character JSON for build mode (uses client-side state when available)", + description: + "Current character JSON for build mode (uses client-side state when available)", get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => { - const settings = runtime.character.settings as Record | undefined; - const clientState = settings?.clientCharacterState as ClientCharacterState | undefined; + const settings = runtime.character.settings as + | Record + | undefined; + const clientState = settings?.clientCharacterState as + | ClientCharacterState + | undefined; const isUnsaved = settings?.isClientStateUnsaved as boolean | undefined; // Check if we have client-side state from the frontend @@ -54,7 +59,9 @@ export const currentCharacterProvider: Provider = { }; const characterJSON = JSON.stringify(characterFields, null, 2); - const stateLabel = isUnsaved ? "(UNSAVED - client preview)" : "(from client)"; + const stateLabel = isUnsaved + ? "(UNSAVED - client preview)" + : "(from client)"; return { text: `# Current Character State ${stateLabel}\n${characterJSON}`, diff --git a/packages/lib/eliza/plugin-character-builder/providers/mode-context.ts b/packages/lib/eliza/plugin-character-builder/providers/mode-context.ts index 891d1814c..514e1ea19 100644 --- a/packages/lib/eliza/plugin-character-builder/providers/mode-context.ts +++ b/packages/lib/eliza/plugin-character-builder/providers/mode-context.ts @@ -52,7 +52,8 @@ const EDIT_MODE_CONTEXT = `## Mode: EDIT MODE export const modeContextProvider: Provider = { name: "MODE_CONTEXT", - description: "Provides mode-aware context (Creator vs Edit mode) for build prompts", + description: + "Provides mode-aware context (Creator vs Edit mode) for build prompts", get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => { const creatorMode = isCreatorMode(runtime); const modeLabel = creatorMode ? "Creator" : "Edit"; diff --git a/packages/lib/eliza/plugin-character-builder/utils/onboarding-state.ts b/packages/lib/eliza/plugin-character-builder/utils/onboarding-state.ts index 461e5897a..21c8fad40 100644 --- a/packages/lib/eliza/plugin-character-builder/utils/onboarding-state.ts +++ b/packages/lib/eliza/plugin-character-builder/utils/onboarding-state.ts @@ -20,7 +20,10 @@ function onboardingKey(entityId: string): string { /** * Check if user has been onboarded */ -export async function isOnboarded(runtime: IAgentRuntime, entityId: string): Promise { +export async function isOnboarded( + runtime: IAgentRuntime, + entityId: string, +): Promise { const key = onboardingKey(entityId); const state = await runtime.getCache(key); return state?.completed === true; @@ -29,7 +32,10 @@ export async function isOnboarded(runtime: IAgentRuntime, entityId: string): Pro /** * Mark user as onboarded (disables GUIDE_ONBOARDING for this user) */ -export async function markOnboarded(runtime: IAgentRuntime, entityId: string): Promise { +export async function markOnboarded( + runtime: IAgentRuntime, + entityId: string, +): Promise { const key = onboardingKey(entityId); const state: OnboardingState = { completed: true, diff --git a/packages/lib/eliza/plugin-chat-playground/handler.ts b/packages/lib/eliza/plugin-chat-playground/handler.ts index a91aa020b..06652b65f 100644 --- a/packages/lib/eliza/plugin-chat-playground/handler.ts +++ b/packages/lib/eliza/plugin-chat-playground/handler.ts @@ -16,7 +16,10 @@ import { } from "@elizaos/core"; import { v4 } from "uuid"; import type { DialogueMetadata } from "@/lib/types/message-content"; -import type { MessageReceivedHandlerParams, RunEndedEventPayload } from "../shared/types"; +import type { + MessageReceivedHandlerParams, + RunEndedEventPayload, +} from "../shared/types"; import { cleanPrompt, postProcessResponse, @@ -103,7 +106,9 @@ export async function handleMessage({ const prompt = cleanPrompt( composePromptFromState({ state, - template: runtime.character.templates?.chatPlaygroundTemplate || chatPlaygroundTemplate, + template: + runtime.character.templates?.chatPlaygroundTemplate || + chatPlaygroundTemplate, }), ); @@ -122,10 +127,14 @@ export async function handleMessage({ throw new Error("Failed to generate valid response"); } - if (!(await isResponseStillValid(runtime, message.roomId, responseId))) return; + if (!(await isResponseStillValid(runtime, message.roomId, responseId))) + return; await clearLatestResponseId(runtime, message.roomId); - const processedResponse = postProcessResponse(parsedResponse.text, message.roomId as string); + const processedResponse = postProcessResponse( + parsedResponse.text, + message.roomId as string, + ); const finalText = processedResponse.text; if (callback) { @@ -157,7 +166,13 @@ export async function handleMessage({ } as DialogueMetadata, }; - runEvaluatorsWithTimeout(runtime, message, state, responseMemory, callback).catch((e) => { + runEvaluatorsWithTimeout( + runtime, + message, + state, + responseMemory, + callback, + ).catch((e) => { logger.warn(`[ChatPlayground] Evaluators failed: ${e}`); }); @@ -174,7 +189,9 @@ export async function handleMessage({ endTime: Date.now(), duration: Date.now() - startTime, }) - .catch((e) => logger.debug(`[ChatPlayground] RUN_ENDED emit failed: ${e}`)); + .catch((e) => + logger.debug(`[ChatPlayground] RUN_ENDED emit failed: ${e}`), + ); } catch (error) { const errorPayload: RunEndedEventPayload = { runtime, @@ -215,7 +232,9 @@ async function checkAndRunMcpAction( try { // Debug: Log registered actions for MCP troubleshooting const registeredActions = runtime.actions?.map((a: Action) => a.name) || []; - logger.debug(`[ChatPlayground/MCP] Registered actions: ${registeredActions.join(", ")}`); + logger.debug( + `[ChatPlayground/MCP] Registered actions: ${registeredActions.join(", ")}`, + ); // Check if MCP data is available in state (from MCP provider) const stateData = state.data as Record | undefined; @@ -244,7 +263,9 @@ async function checkAndRunMcpAction( } // Log connected servers and their tools - logger.info(`[ChatPlayground/MCP] MCP servers connected: ${serverNames.join(", ")}`); + logger.info( + `[ChatPlayground/MCP] MCP servers connected: ${serverNames.join(", ")}`, + ); for (const serverName of serverNames) { const server = mcpServers?.[serverName] as @@ -259,7 +280,8 @@ async function checkAndRunMcpAction( // Find the CALL_MCP_TOOL action from registered actions const mcpAction = runtime.actions?.find( (action: Action) => - action.name === "CALL_MCP_TOOL" || action.similes?.includes("CALL_MCP_TOOL"), + action.name === "CALL_MCP_TOOL" || + action.similes?.includes("CALL_MCP_TOOL"), ); if (!mcpAction) { @@ -282,10 +304,18 @@ async function checkAndRunMcpAction( return false; } - logger.info("[ChatPlayground/MCP] CALL_MCP_TOOL action is valid, executing..."); + logger.info( + "[ChatPlayground/MCP] CALL_MCP_TOOL action is valid, executing...", + ); // Execute the MCP action - const result = await mcpAction.handler(runtime, message, state, {}, callback); + const result = await mcpAction.handler( + runtime, + message, + state, + {}, + callback, + ); // Check result for success const actionResult = result as diff --git a/packages/lib/eliza/plugin-chat-playground/index.ts b/packages/lib/eliza/plugin-chat-playground/index.ts index 512c19cb1..dc30f9997 100644 --- a/packages/lib/eliza/plugin-chat-playground/index.ts +++ b/packages/lib/eliza/plugin-chat-playground/index.ts @@ -1,4 +1,9 @@ -import { EventType, logger, type MessagePayload, type Plugin } from "@elizaos/core"; +import { + EventType, + logger, + type MessagePayload, + type Plugin, +} from "@elizaos/core"; import { roomTitleEvaluator } from "../shared/evaluators/room-title"; import { appConfigProvider } from "../shared/providers/app-config"; import { characterProvider } from "../shared/providers/character"; @@ -14,8 +19,9 @@ export const chatPlaygroundPlugin: Plugin = { async (payload: MessagePayload) => { if (!payload.callback) return; // Extract onStreamChunk if present (added by eliza-cloud message handler) - const onStreamChunk = (payload as MessagePayload & { onStreamChunk?: StreamChunkCallback }) - .onStreamChunk; + const onStreamChunk = ( + payload as MessagePayload & { onStreamChunk?: StreamChunkCallback } + ).onStreamChunk; logger.info( `[Playground] Message received in room ${payload.message.roomId}, streaming=${!!onStreamChunk}`, ); diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/actions/finish.ts b/packages/lib/eliza/plugin-cloud-bootstrap/actions/finish.ts index e604105d4..d76570c2e 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/actions/finish.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/actions/finish.ts @@ -1,4 +1,10 @@ -import type { ActionResult, HandlerCallback, IAgentRuntime, Memory, State } from "@elizaos/core"; +import type { + ActionResult, + HandlerCallback, + IAgentRuntime, + Memory, + State, +} from "@elizaos/core"; import { type ActionWithParams, defineActionParameters } from "../types"; /** diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/actions/image-generation.ts b/packages/lib/eliza/plugin-cloud-bootstrap/actions/image-generation.ts index f640ed162..d2b1dde91 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/actions/image-generation.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/actions/image-generation.ts @@ -38,7 +38,14 @@ Your response should be formatted in XML like this: Your response should include the valid XML block and nothing else.`; -const VALID_IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp"]); +const VALID_IMAGE_EXTENSIONS = new Set([ + "png", + "jpg", + "jpeg", + "gif", + "webp", + "bmp", +]); function getFileExtension(url: string): string { try { @@ -49,7 +56,10 @@ function getFileExtension(url: string): string { } } -function extractParams(message: Memory, state?: State): Record { +function extractParams( + message: Memory, + state?: State, +): Record { const content = message.content as Record; return (content.actionParams || content.actionInput || @@ -73,7 +83,10 @@ export const generateImageAction: ActionWithParams = { }, }), - validate: async (runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + runtime: IAgentRuntime, + message: Memory, + ): Promise => { // Check runtime has required model capability if (!runtime.useModel) { logger.warn("[GENERATE_IMAGE] Runtime missing useModel capability"); @@ -88,14 +101,21 @@ export const generateImageAction: ActionWithParams = { // Image generation requires either text content or explicit prompt parameter const content = message.content as Record; - const actionParams = content.actionParams as Record | undefined; - const actionInput = content.actionInput as Record | undefined; + const actionParams = content.actionParams as + | Record + | undefined; + const actionInput = content.actionInput as + | Record + | undefined; const hasText = - typeof message.content.text === "string" && message.content.text.trim().length > 0; + typeof message.content.text === "string" && + message.content.text.trim().length > 0; const hasPromptParam = actionParams?.prompt || actionInput?.prompt; if (!hasText && !hasPromptParam) { - logger.debug("[GENERATE_IMAGE] No text content or prompt parameter available"); + logger.debug( + "[GENERATE_IMAGE] No text content or prompt parameter available", + ); return false; } @@ -117,10 +137,14 @@ export const generateImageAction: ActionWithParams = { `[GENERATE_IMAGE] Starting image generation${providedPrompt ? ` with prompt: "${providedPrompt.substring(0, 50)}..."` : " from conversation"}`, ); - const allProviders = responses?.flatMap((res) => res.content?.providers ?? []) ?? []; + const allProviders = + responses?.flatMap((res) => res.content?.providers ?? []) ?? []; if (!state) { - state = await runtime.composeState(message, [...allProviders, "RECENT_MESSAGES"]); + state = await runtime.composeState(message, [ + ...allProviders, + "RECENT_MESSAGES", + ]); } else if (allProviders.length > 0) { state.values = { ...state.values, additionalProviders: allProviders }; } @@ -131,10 +155,14 @@ export const generateImageAction: ActionWithParams = { const prompt = composePromptFromState({ state, - template: runtime.character.templates?.imageGenerationTemplate || IMAGE_GENERATION_TEMPLATE, + template: + runtime.character.templates?.imageGenerationTemplate || + IMAGE_GENERATION_TEMPLATE, }); - const promptResponse = await runtime.useModel(ModelType.TEXT_LARGE, { prompt }); + const promptResponse = await runtime.useModel(ModelType.TEXT_LARGE, { + prompt, + }); const parsedXml = parseKeyValueXml(promptResponse); const imagePrompt: string = typeof parsedXml?.prompt === "string" @@ -143,16 +171,25 @@ export const generateImageAction: ActionWithParams = { logger.info(`[GENERATE_IMAGE] Using prompt: "${imagePrompt}"`); - const imageResponse = await runtime.useModel(ModelType.IMAGE, { prompt: imagePrompt }); + const imageResponse = await runtime.useModel(ModelType.IMAGE, { + prompt: imagePrompt, + }); if (!imageResponse?.length || !imageResponse[0]?.url) { - logger.error(`[GENERATE_IMAGE] Image generation failed - no valid response received`); + logger.error( + `[GENERATE_IMAGE] Image generation failed - no valid response received`, + ); return { text: `Image generation failed for prompt: "${imagePrompt}"`, - values: { success: false, error: "IMAGE_GENERATION_FAILED", prompt: imagePrompt }, + values: { + success: false, + error: "IMAGE_GENERATION_FAILED", + prompt: imagePrompt, + }, data: { actionName: "GENERATE_IMAGE", - error: "Image model returned no results. Try a different image generation model.", + error: + "Image model returned no results. Try a different image generation model.", prompt: imagePrompt, }, success: false, @@ -161,7 +198,10 @@ export const generateImageAction: ActionWithParams = { const imageUrl = imageResponse[0].url; const extension = getFileExtension(imageUrl); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); const fileName = `Generated_Image_${timestamp}.${extension}`; const attachmentId = v4(); @@ -185,7 +225,12 @@ export const generateImageAction: ActionWithParams = { return { text: `Generated image: "${imagePrompt}"`, - values: { success: true, imageGenerated: true, imageUrl, prompt: imagePrompt }, + values: { + success: true, + imageGenerated: true, + imageUrl, + prompt: imagePrompt, + }, data: { actionName: "GENERATE_IMAGE", imageUrl, diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/index.ts b/packages/lib/eliza/plugin-cloud-bootstrap/index.ts index 3e541d19c..aa29850bf 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/index.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/index.ts @@ -36,13 +36,16 @@ export * from "./utils"; */ class MessageServiceInstaller extends Service { static serviceType = "cloud-bootstrap-message-installer"; - capabilityDescription = "Installs CloudBootstrapMessageService after runtime initialization"; + capabilityDescription = + "Installs CloudBootstrapMessageService after runtime initialization"; static async start(runtime: IAgentRuntime): Promise { const service = new MessageServiceInstaller(runtime); // Replace DefaultMessageService with our custom implementation - logger.info("[CloudBootstrap] Installing CloudBootstrapMessageService (post-initialization)"); + logger.info( + "[CloudBootstrap] Installing CloudBootstrapMessageService (post-initialization)", + ); runtime.messageService = new CloudBootstrapMessageService(); logger.info("[CloudBootstrap] CloudBootstrapMessageService installed"); @@ -86,7 +89,9 @@ async function logRunEvent(payload: RunEventPayload): Promise { const loggedStatuses = loggedRunIds.get(runId); if (!loggedStatuses) return; // Shouldn't happen, but guard against it if (loggedStatuses.has(status)) { - logger.debug(`[CloudBootstrap] Skipping duplicate log: runId=${runId} status=${status}`); + logger.debug( + `[CloudBootstrap] Skipping duplicate log: runId=${runId} status=${status}`, + ); return; } loggedStatuses.add(status); @@ -107,7 +112,10 @@ async function logRunEvent(payload: RunEventPayload): Promise { if (payload.error !== undefined) { // Cap error message size const errorStr = String(payload.error); - body.error = errorStr.length > 1000 ? errorStr.substring(0, 1000) + "...(truncated)" : errorStr; + body.error = + errorStr.length > 1000 + ? errorStr.substring(0, 1000) + "...(truncated)" + : errorStr; } // PERF: Cap total payload size to prevent oversized writes from blocking. @@ -121,7 +129,10 @@ async function logRunEvent(payload: RunEventPayload): Promise { // Aggressively truncate error to 200 chars if (body.error) { const errStr = String(body.error); - body.error = errStr.length > 200 ? errStr.substring(0, 200) + "...(truncated)" : errStr; + body.error = + errStr.length > 200 + ? errStr.substring(0, 200) + "...(truncated)" + : errStr; } body._truncated = true; body._originalSize = originalSize; diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/providers/action-state.ts b/packages/lib/eliza/plugin-cloud-bootstrap/providers/action-state.ts index 592ea49cf..d5eed4c0f 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/providers/action-state.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/providers/action-state.ts @@ -9,14 +9,20 @@ import { } from "@elizaos/core"; import type { MultiStepActionResult } from "../types"; -function formatActionResult(result: MultiStepActionResult, index: number): string { +function formatActionResult( + result: MultiStepActionResult, + index: number, +): string { const actionName = result.data?.actionName || "Unknown Action"; const status = result.success ? "Success" : "Failed"; const lines = [`**${index + 1}. ${actionName}** - ${status}`]; if (result.text) lines.push(` Output: ${result.text}`); if (result.error) { - const errorStr = result.error instanceof Error ? result.error.message : String(result.error); + const errorStr = + result.error instanceof Error + ? result.error.message + : String(result.error); lines.push(` Error: ${errorStr}`); } if (result.values && Object.keys(result.values).length > 0) { @@ -37,7 +43,11 @@ function formatWorkingMemory(workingMemory: Record): string { }; return Object.entries(workingMemory) - .sort((a, b) => ((b[1] as MemEntry)?.timestamp || 0) - ((a[1] as MemEntry)?.timestamp || 0)) + .sort( + (a, b) => + ((b[1] as MemEntry)?.timestamp || 0) - + ((a[1] as MemEntry)?.timestamp || 0), + ) .slice(0, 10) .map(([key, value]) => { const v = value as MemEntry; @@ -60,7 +70,9 @@ function formatActionMemories(memories: Memory[]): string { return Array.from(groupedByRun.entries()) .map(([runId, mems]) => { - const sorted = mems.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0)); + const sorted = mems.sort( + (a, b) => (a.createdAt || 0) - (b.createdAt || 0), + ); const runText = sorted .map((mem) => { const actionName = mem.content?.actionName || "Unknown"; @@ -69,7 +81,8 @@ function formatActionMemories(memories: Memory[]): string { const text = mem.content?.text || ""; let line = ` - ${actionName} (${status})`; if (planStep) line += ` [${planStep}]`; - if (text && text !== `Executed action: ${actionName}`) line += `: ${text}`; + if (text && text !== `Executed action: ${actionName}`) + line += `: ${text}`; return line; }) .join("\n"); @@ -81,23 +94,33 @@ function formatActionMemories(memories: Memory[]): string { export const actionStateProvider: Provider = { name: "ACTION_STATE", - description: "Previous action results and working memory from the current execution run", + description: + "Previous action results and working memory from the current execution run", position: 150, get: async (runtime: IAgentRuntime, message: Memory, state: State) => { // Check state.data first, then message metadata as fallback // This handles the timing issue where actionResults may be in message metadata // before state is fully composed - const messageMetadata = (message.content?.metadata || {}) as Record; + const messageMetadata = (message.content?.metadata || {}) as Record< + string, + unknown + >; const actionResults = (state.data?.actionResults || messageMetadata.actionResults || []) as MultiStepActionResult[]; - const workingMemory = (state.data?.workingMemory || {}) as Record; + const workingMemory = (state.data?.workingMemory || {}) as Record< + string, + unknown + >; // Format action results const resultsText = actionResults.length > 0 - ? addHeader("# Previous Action Results", actionResults.map(formatActionResult).join("\n\n")) + ? addHeader( + "# Previous Action Results", + actionResults.map(formatActionResult).join("\n\n"), + ) : "No previous action results available."; // Format working memory @@ -128,10 +151,15 @@ export const actionStateProvider: Provider = { // Format action history const actionMemoriesText = recentActionMemories.length > 0 - ? addHeader("# Recent Action History", formatActionMemories(recentActionMemories)) + ? addHeader( + "# Recent Action History", + formatActionMemories(recentActionMemories), + ) : ""; - const allText = [resultsText, memoryText, actionMemoriesText].filter(Boolean).join("\n\n"); + const allText = [resultsText, memoryText, actionMemoriesText] + .filter(Boolean) + .join("\n\n"); return { data: { actionResults, workingMemory, recentActionMemories }, diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/providers/actions.ts b/packages/lib/eliza/plugin-cloud-bootstrap/providers/actions.ts index a94cb2fb8..636c1976c 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/providers/actions.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/providers/actions.ts @@ -1,10 +1,26 @@ /** ACTIONS Provider - Provides available actions with parameter schemas to the LLM. */ -import type { Action, IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; -import { addHeader, composeActionExamples, formatActionNames, logger } from "@elizaos/core"; -import { filterActionsByRouting, getContextRoutingFromMessage } from "../utils/context-routing"; +import type { + Action, + IAgentRuntime, + Memory, + Provider, + State, +} from "@elizaos/core"; +import { + addHeader, + composeActionExamples, + formatActionNames, + logger, +} from "@elizaos/core"; +import { + filterActionsByRouting, + getContextRoutingFromMessage, +} from "../utils/context-routing"; function formatActionsWithoutParams(actions: Action[]): string { - return actions.map((a) => `## ${a.name}\n${a.description}`).join("\n\n---\n\n"); + return actions + .map((a) => `## ${a.name}\n${a.description}`) + .join("\n\n---\n\n"); } function formatActionsWithParams(actions: Action[]): string { @@ -14,7 +30,10 @@ function formatActionsWithParams(actions: Action[]): string { let formatted = `## ${action.name}\n${action.description}`; if (!params || params.length === 0) { - return formatted + "\n\n**Parameters:** None (can be called directly without parameters)"; + return ( + formatted + + "\n\n**Parameters:** None (can be called directly without parameters)" + ); } formatted += "\n\n**Parameters:**"; @@ -63,10 +82,15 @@ export const actionsProvider: Provider = { await Promise.all( runtime.actions.map(async (action: Action) => { try { - return (await action.validate(runtime, message, state)) ? action : null; + return (await action.validate(runtime, message, state)) + ? action + : null; } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); - logger.error(`[ACTIONS] validate error: ${action.name}`, errorMessage); + logger.error( + `[ACTIONS] validate error: ${action.name}`, + errorMessage, + ); return null; } }), @@ -89,8 +113,14 @@ export const actionsProvider: Provider = { cached = { actions: actionsData, discoverableToolCount }; if (cacheKey) { - const timeoutHandle = setTimeout(() => validationCache.delete(cacheKey), 120_000); - if (typeof timeoutHandle === "object" && typeof timeoutHandle?.unref === "function") { + const timeoutHandle = setTimeout( + () => validationCache.delete(cacheKey), + 120_000, + ); + if ( + typeof timeoutHandle === "object" && + typeof timeoutHandle?.unref === "function" + ) { timeoutHandle.unref(); } cached.timeoutHandle = timeoutHandle; @@ -111,10 +141,16 @@ export const actionsProvider: Provider = { values: { actionNames, actionExamples: hasActions - ? addHeader("# Action Examples", composeActionExamples(actionsData, 10)) + ? addHeader( + "# Action Examples", + composeActionExamples(actionsData, 10), + ) : "", actionsWithDescriptions: hasActions - ? addHeader("# Available Actions", formatActionsWithoutParams(actionsData)) + ? addHeader( + "# Available Actions", + formatActionsWithoutParams(actionsData), + ) : "", actionsWithParams: hasActions ? addHeader( @@ -122,13 +158,20 @@ export const actionsProvider: Provider = { formatActionsWithParams(actionsData), ) : "", - discoverableToolCount: discoverableToolCount > 0 ? String(discoverableToolCount) : "", + discoverableToolCount: + discoverableToolCount > 0 ? String(discoverableToolCount) : "", }, text: hasActions ? [ actionNames, - addHeader("# Available Actions", formatActionsWithoutParams(actionsData)), - addHeader("# Action Examples", composeActionExamples(actionsData, 10)), + addHeader( + "# Available Actions", + formatActionsWithoutParams(actionsData), + ), + addHeader( + "# Action Examples", + composeActionExamples(actionsData, 10), + ), ].join("\n\n") : actionNames, }; diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/providers/character.ts b/packages/lib/eliza/plugin-cloud-bootstrap/providers/character.ts index 5ee5dc5b1..97f0a34c4 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/providers/character.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/providers/character.ts @@ -8,7 +8,9 @@ import type { } from "@elizaos/core"; import { addHeader, ChannelType } from "@elizaos/core"; -function getExampleMessages(example: MessageExampleGroup | MessageExample[]): MessageExample[] { +function getExampleMessages( + example: MessageExampleGroup | MessageExample[], +): MessageExample[] { return Array.isArray(example) ? example : example.examples; } @@ -81,7 +83,9 @@ export const characterProvider: Provider = { // Select random adjective if available const adjectiveString = character.adjectives && character.adjectives.length > 0 - ? character.adjectives[Math.floor(Math.random() * character.adjectives.length)] + ? character.adjectives[ + Math.floor(Math.random() * character.adjectives.length) + ] : ""; const adjective = adjectiveString || ""; @@ -101,7 +105,10 @@ export const characterProvider: Provider = { const characterPostExamples = formattedCharacterPostExamples && formattedCharacterPostExamples.replaceAll("\n", "").length > 0 - ? addHeader(`# Example Posts for ${character.name}`, formattedCharacterPostExamples) + ? addHeader( + `# Example Posts for ${character.name}`, + formattedCharacterPostExamples, + ) : ""; // Format message examples @@ -143,7 +150,8 @@ export const characterProvider: Provider = { const room = state.data?.room ?? (await runtime.getRoom(message.roomId)); - const isPostFormat = room?.type === ChannelType.FEED || room?.type === ChannelType.THREAD; + const isPostFormat = + room?.type === ChannelType.FEED || room?.type === ChannelType.THREAD; // Style directions const postDirections = @@ -179,7 +187,9 @@ export const characterProvider: Provider = { : ""; const directions = isPostFormat ? postDirections : messageDirections; - const examples = isPostFormat ? characterPostExamples : characterMessageExamples; + const examples = isPostFormat + ? characterPostExamples + : characterMessageExamples; const values = { agentName, @@ -211,9 +221,19 @@ export const characterProvider: Provider = { const topicSentence = topicString ? `${character.name} is currently interested in ${topicString}` : ""; - const adjectiveSentence = adjectiveString ? `${character.name} is ${adjectiveString}` : ""; + const adjectiveSentence = adjectiveString + ? `${character.name} is ${adjectiveString}` + : ""; // Combine all text sections - const text = [bio, adjectiveSentence, topicSentence, topics, directions, examples, system] + const text = [ + bio, + adjectiveSentence, + topicSentence, + topics, + directions, + examples, + system, + ] .filter(Boolean) .join("\n\n"); diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/services/cloud-bootstrap-message-service.ts b/packages/lib/eliza/plugin-cloud-bootstrap/services/cloud-bootstrap-message-service.ts index ef0540b9a..06666c461 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/services/cloud-bootstrap-message-service.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/services/cloud-bootstrap-message-service.ts @@ -58,7 +58,10 @@ import { isLatestResponseId, setLatestResponseId, } from "../utils/race-tracking"; -import { getActionResultsFromCache, refreshStateAfterAction } from "../utils/state"; +import { + getActionResultsFromCache, + refreshStateAfterAction, +} from "../utils/state"; const RETRY_CONFIG = { baseDelayMs: 200, @@ -73,7 +76,10 @@ type ScopedSettingOverride = { value: string; }; -function readTrimmedSetting(runtime: IAgentRuntime, key: string): string | undefined { +function readTrimmedSetting( + runtime: IAgentRuntime, + key: string, +): string | undefined { const value = runtime.getSetting(key); if (typeof value !== "string") { return undefined; @@ -108,7 +114,9 @@ function resolveLargeModel(runtime: IAgentRuntime): string | undefined { ); } -function resolveShouldRespondStepModel(runtime: IAgentRuntime): string | undefined { +function resolveShouldRespondStepModel( + runtime: IAgentRuntime, +): string | undefined { return ( readTrimmedSetting(runtime, "ELIZAOS_CLOUD_RESPONSE_HANDLER_MODEL") || readTrimmedSetting(runtime, "ELIZAOS_CLOUD_SHOULD_RESPOND_MODEL") || @@ -117,7 +125,9 @@ function resolveShouldRespondStepModel(runtime: IAgentRuntime): string | undefin ); } -function resolveActionPlannerStepModel(runtime: IAgentRuntime): string | undefined { +function resolveActionPlannerStepModel( + runtime: IAgentRuntime, +): string | undefined { return ( readTrimmedSetting(runtime, "ELIZAOS_CLOUD_ACTION_PLANNER_MODEL") || readTrimmedSetting(runtime, "ELIZAOS_CLOUD_PLANNER_MODEL") || @@ -127,7 +137,10 @@ function resolveActionPlannerStepModel(runtime: IAgentRuntime): string | undefin } function resolveResponseStepModel(runtime: IAgentRuntime): string | undefined { - return readTrimmedSetting(runtime, "ELIZAOS_CLOUD_RESPONSE_MODEL") || resolveLargeModel(runtime); + return ( + readTrimmedSetting(runtime, "ELIZAOS_CLOUD_RESPONSE_MODEL") || + resolveLargeModel(runtime) + ); } async function withScopedSettings( @@ -213,7 +226,8 @@ Respond using XML format: `; function getRetryDelay(attempt: number): number { - const delay = RETRY_CONFIG.baseDelayMs * RETRY_CONFIG.backoffMultiplier ** (attempt - 1); + const delay = + RETRY_CONFIG.baseDelayMs * RETRY_CONFIG.backoffMultiplier ** (attempt - 1); return Math.min(delay, RETRY_CONFIG.maxDelayMs); } @@ -230,10 +244,16 @@ async function withRetry( logger.debug(`[MultiStep] ${label} succeeded on attempt ${attempt}`); return result; } - logger.warn(`[MultiStep] ${label} validation failed on attempt ${attempt}/${maxRetries}`); + logger.warn( + `[MultiStep] ${label} validation failed on attempt ${attempt}/${maxRetries}`, + ); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`[MultiStep] ${label} error on attempt ${attempt}/${maxRetries}:`, errorMessage); + const errorMessage = + error instanceof Error ? error.message : String(error); + logger.error( + `[MultiStep] ${label} error on attempt ${attempt}/${maxRetries}:`, + errorMessage, + ); if (attempt >= maxRetries) throw error; } @@ -275,11 +295,15 @@ export class CloudBootstrapMessageService implements IMessageService { const shouldRespondPrompt = composePromptFromState({ state: evalState, - template: runtime.character.templates?.shouldRespondTemplate || shouldRespondTemplate, + template: + runtime.character.templates?.shouldRespondTemplate || + shouldRespondTemplate, }); logger.info("========== LLM CALL: shouldRespond =========="); - logger.info(`[LLM:shouldRespond] System Prompt:\n${runtime.character.system || "(none)"}`); + logger.info( + `[LLM:shouldRespond] System Prompt:\n${runtime.character.system || "(none)"}`, + ); logger.info(`[LLM:shouldRespond] User Prompt:\n${shouldRespondPrompt}`); logger.info("=============================================="); @@ -294,7 +318,10 @@ export class CloudBootstrapMessageService implements IMessageService { logger.info(`[LLM:shouldRespond] Response:\n${response}`); - const responseObject = parseKeyValueXml(String(response)) as Record | null; + const responseObject = parseKeyValueXml(String(response)) as Record< + string, + unknown + > | null; return { responseObject, @@ -321,9 +348,14 @@ export class CloudBootstrapMessageService implements IMessageService { ); // Set up response tracking - const previousResponseId = await getLatestResponseId(runtime.agentId, message.roomId); + const previousResponseId = await getLatestResponseId( + runtime.agentId, + message.roomId, + ); if (previousResponseId) { - logger.debug(`[CloudBootstrap] Updating response ID for room ${message.roomId}`); + logger.debug( + `[CloudBootstrap] Updating response ID for room ${message.roomId}`, + ); } await setLatestResponseId(runtime.agentId, message.roomId, responseId); @@ -376,7 +408,11 @@ export class CloudBootstrapMessageService implements IMessageService { clearTimeout(timeoutId); return result; } catch (error) { - await cleanupLatestResponseId(runtime.agentId, message.roomId, responseId); + await cleanupLatestResponseId( + runtime.agentId, + message.roomId, + responseId, + ); // Emit RUN_ENDED event on error so tracking is complete if (runId && startTime) { @@ -450,8 +486,13 @@ export class CloudBootstrapMessageService implements IMessageService { } // Check LLM off by default setting - const agentUserState = await runtime.getParticipantUserState(message.roomId, runtime.agentId); - const defLlmOff = parseBooleanFromText(String(runtime.getSetting("BOOTSTRAP_DEFLLMOFF") ?? "")); + const agentUserState = await runtime.getParticipantUserState( + message.roomId, + runtime.agentId, + ); + const defLlmOff = parseBooleanFromText( + String(runtime.getSetting("BOOTSTRAP_DEFLLMOFF") ?? ""), + ); if (defLlmOff && agentUserState === null) { logger.debug("[CloudBootstrap] LLM is off by default"); @@ -468,7 +509,9 @@ export class CloudBootstrapMessageService implements IMessageService { // Check if room is muted const isMuted = agentUserState === "MUTED" && - !message.content.text?.toLowerCase().includes(runtime.character.name?.toLowerCase() ?? ""); + !message.content.text + ?.toLowerCase() + .includes(runtime.character.name?.toLowerCase() ?? ""); if (isMuted) { logger.debug(`[CloudBootstrap] Ignoring muted room ${message.roomId}`); await this.emitRunEnded(runtime, runId, message, startTime, "muted"); @@ -483,7 +526,9 @@ export class CloudBootstrapMessageService implements IMessageService { // Process attachments if any if (message.content.attachments && message.content.attachments.length > 0) { - logger.debug(`[CloudBootstrap] Processing ${message.content.attachments.length} attachments`); + logger.debug( + `[CloudBootstrap] Processing ${message.content.attachments.length} attachments`, + ); message.content.attachments = await this.processAttachments( runtime, message.content.attachments, @@ -494,7 +539,9 @@ export class CloudBootstrapMessageService implements IMessageService { const room = await runtime.getRoom(message.roomId); // Extract mention context from message metadata - const metadata = message.content.metadata as Record | undefined; + const metadata = message.content.metadata as + | Record + | undefined; const mentionContext: MentionContext | undefined = metadata ? { isMention: !!metadata.isMention, @@ -505,7 +552,12 @@ export class CloudBootstrapMessageService implements IMessageService { : undefined; // Check if we should respond - const respondDecision = this.shouldRespond(runtime, message, room ?? undefined, mentionContext); + const respondDecision = this.shouldRespond( + runtime, + message, + room ?? undefined, + mentionContext, + ); logger.debug( `[CloudBootstrap] shouldRespond: ${respondDecision.shouldRespond} (${respondDecision.reason})`, ); @@ -523,14 +575,18 @@ export class CloudBootstrapMessageService implements IMessageService { setContextRoutingMetadata(message, routedDecision); } } else { - const { responseObject, routing } = await this.evaluateShouldRespond(runtime, message); + const { responseObject, routing } = await this.evaluateShouldRespond( + runtime, + message, + ); routedDecision = routing; setContextRoutingMetadata(message, routedDecision); const nonResponseActions = ["IGNORE", "NONE", "STOP"]; const actionValue = responseObject?.action; shouldRespondToMessage = - typeof actionValue === "string" && !nonResponseActions.includes(actionValue.toUpperCase()); + typeof actionValue === "string" && + !nonResponseActions.includes(actionValue.toUpperCase()); logger.debug( `[CloudBootstrap] LLM decided: ${shouldRespondToMessage ? "RESPOND" : "IGNORE"}`, @@ -539,7 +595,13 @@ export class CloudBootstrapMessageService implements IMessageService { if (!shouldRespondToMessage) { logger.debug(`[CloudBootstrap] Not responding based on evaluation`); - await this.emitRunEnded(runtime, runId, message, startTime, "shouldRespond:no"); + await this.emitRunEnded( + runtime, + runId, + message, + startTime, + "shouldRespond:no", + ); return { didRespond: false, responseContent: null, @@ -554,23 +616,42 @@ export class CloudBootstrapMessageService implements IMessageService { // runMultiStepCore fetches the full provider set (RECENT_MESSAGES, ACTIONS, etc.) // at the start of its decision loop. runSingleShotCore fetches them before prompt // composition. This avoids double-fetching in the multi-step path. - let state = await runtime.composeState(message, ["ENTITIES", "CHARACTER"], true); + let state = await runtime.composeState( + message, + ["ENTITIES", "CHARACTER"], + true, + ); state = attachAvailableContexts(state, runtime as never); // Determine processing mode - default to multi-step for cloud const useMultiStep = options?.useMultiStep ?? - parseBooleanFromText(String(runtime.getSetting("USE_MULTI_STEP") ?? "true")); + parseBooleanFromText( + String(runtime.getSetting("USE_MULTI_STEP") ?? "true"), + ); perfTrace.mark("llm-processing"); // Run appropriate processing strategy let result: StrategyResult; if (useMultiStep) { logger.debug("[CloudBootstrap] Using multi-step processing"); - result = await this.runMultiStepCore(runtime, message, state, responseId, callback, options); + result = await this.runMultiStepCore( + runtime, + message, + state, + responseId, + callback, + options, + ); } else { logger.debug("[CloudBootstrap] Using single-shot processing"); - result = await this.runSingleShotCore(runtime, message, state, callback, options); + result = await this.runSingleShotCore( + runtime, + message, + state, + callback, + options, + ); } const responseContent = result.responseContent; @@ -578,9 +659,19 @@ export class CloudBootstrapMessageService implements IMessageService { state = result.state; // Race check before sending response - if (!(await isLatestResponseId(runtime.agentId, message.roomId, responseId))) { - logger.info(`[CloudBootstrap] Response discarded - newer message being processed`); - await this.emitRunEnded(runtime, runId, message, startTime, "race-discarded"); + if ( + !(await isLatestResponseId(runtime.agentId, message.roomId, responseId)) + ) { + logger.info( + `[CloudBootstrap] Response discarded - newer message being processed`, + ); + await this.emitRunEnded( + runtime, + runId, + message, + startTime, + "race-discarded", + ); return { didRespond: false, responseContent: null, @@ -604,13 +695,18 @@ export class CloudBootstrapMessageService implements IMessageService { } } else if (mode === "actions") { // Actions mode - run processActions (though in multi-step we already did this) - await runtime.processActions(message, responseMessages, state, async (content) => { - responseContent!.actionCallbacks = content; - if (callback) { - return callback(content); - } - return []; - }); + await runtime.processActions( + message, + responseMessages, + state, + async (content) => { + responseContent!.actionCallbacks = content; + if (callback) { + return callback(content); + } + return []; + }, + ); } } @@ -747,7 +843,8 @@ export class CloudBootstrapMessageService implements IMessageService { ]; for (const key of stableProviderKeys) { const val = - accumulatedState.values?.[key] ?? (accumulatedState as Record)[key]; + accumulatedState.values?.[key] ?? + (accumulatedState as Record)[key]; if (val !== undefined) { cachedStableValues[key] = val; } @@ -757,7 +854,10 @@ export class CloudBootstrapMessageService implements IMessageService { ? { actionsData: accumulatedState.data.actionsData } : {}; - const streamThinking = async (phase: string, content: string): Promise => { + const streamThinking = async ( + phase: string, + content: string, + ): Promise => { if (options?.onReasoningChunk) { await options.onReasoningChunk( content, @@ -768,16 +868,29 @@ export class CloudBootstrapMessageService implements IMessageService { }; while (iterationCount < maxIterations) { - if (!(await isLatestResponseId(runtime.agentId, message.roomId, responseId))) { - logger.info("[MultiStep] Newer message detected, cancelling stale execution"); + if ( + !(await isLatestResponseId( + runtime.agentId, + message.roomId, + responseId, + )) + ) { + logger.info( + "[MultiStep] Newer message detected, cancelling stale execution", + ); wasCancelled = true; break; } iterationCount++; - logger.debug(`[MultiStep] Starting iteration ${iterationCount}/${maxIterations}`); + logger.debug( + `[MultiStep] Starting iteration ${iterationCount}/${maxIterations}`, + ); - await streamThinking("thinking", `\n--- Step ${iterationCount}/${maxIterations} ---\n`); + await streamThinking( + "thinking", + `\n--- Step ${iterationCount}/${maxIterations} ---\n`, + ); // Inject actionResults into message metadata BEFORE composeState // so ACTION_STATE provider can read it during state composition @@ -816,7 +929,8 @@ export class CloudBootstrapMessageService implements IMessageService { maxIterations, traceActionResult, totalActionsExecuted, - discoveredActions: discoveredActions.size > 0 ? [...discoveredActions].join(", ") : "", + discoveredActions: + discoveredActions.size > 0 ? [...discoveredActions].join(", ") : "", stepsWarning: remainingSteps <= 2, remainingSteps, }; @@ -824,14 +938,17 @@ export class CloudBootstrapMessageService implements IMessageService { const prompt = composePromptFromState({ state: stateWithIterationContext, template: - runtime.character.templates?.multiStepDecisionTemplate || multiStepDecisionTemplate, + runtime.character.templates?.multiStepDecisionTemplate || + multiStepDecisionTemplate, }); // === LLM CALL LOG: multiStepDecision === logger.info( `========== LLM CALL: multiStepDecision (iteration ${iterationCount}/${maxIterations}) ==========`, ); - logger.info(`[LLM:multiStepDecision] System Prompt:\n${runtime.character.system}`); + logger.info( + `[LLM:multiStepDecision] System Prompt:\n${runtime.character.system}`, + ); logger.info(`[LLM:multiStepDecision] User Prompt:\n${prompt}`); logger.info("=============================================="); @@ -844,7 +961,11 @@ export class CloudBootstrapMessageService implements IMessageService { let stepResultRaw = ""; let parsedStep: ValidatedMultiStepDecision | null = null; - for (let parseAttempt = 1; parseAttempt <= maxParseRetries; parseAttempt++) { + for ( + let parseAttempt = 1; + parseAttempt <= maxParseRetries; + parseAttempt++ + ) { try { logger.debug( `[MultiStep] Decision model call attempt ${parseAttempt}/${maxParseRetries}`, @@ -862,7 +983,9 @@ export class CloudBootstrapMessageService implements IMessageService { logger.info( `[LLM:multiStepDecision] Response (attempt ${parseAttempt}):\n${stepResultRaw}`, ); - const rawParsedStep = parseKeyValueXml(stepResultRaw) as ParsedMultiStepDecision | null; + const rawParsedStep = parseKeyValueXml( + stepResultRaw, + ) as ParsedMultiStepDecision | null; if (rawParsedStep) { const validation = validateMultiStepDecision( rawParsedStep, @@ -882,7 +1005,9 @@ export class CloudBootstrapMessageService implements IMessageService { } if (parsedStep) { - logger.debug(`[MultiStep] Successfully parsed on attempt ${parseAttempt}`); + logger.debug( + `[MultiStep] Successfully parsed on attempt ${parseAttempt}`, + ); if (parsedStep.thought && options?.onReasoningChunk) { await streamThinking("planning", parsedStep.thought); @@ -898,7 +1023,8 @@ export class CloudBootstrapMessageService implements IMessageService { } } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); logger.error( `[MultiStep] Error during model call attempt ${parseAttempt}:`, errorMessage, @@ -912,7 +1038,9 @@ export class CloudBootstrapMessageService implements IMessageService { } if (!parsedStep) { - logger.warn(`[MultiStep] Failed to parse step result after ${maxParseRetries} attempts`); + logger.warn( + `[MultiStep] Failed to parse step result after ${maxParseRetries} attempts`, + ); incompleteReason = `The planner produced invalid output ${maxParseRetries} time(s) in a row.`; traceActionResult.push({ data: { actionName: "parse_error" }, @@ -950,12 +1078,16 @@ export class CloudBootstrapMessageService implements IMessageService { if (!action) { // Deprecated fallback: isFinish without an action if (isFinish) { - logger.info(`[MultiStep] Task complete (isFinish) at iteration ${iterationCount}`); + logger.info( + `[MultiStep] Task complete (isFinish) at iteration ${iterationCount}`, + ); await streamThinking("response", "\n--- Completing task ---\n"); break; } - logger.warn(`[MultiStep] No action at iteration ${iterationCount}, forcing completion`); + logger.warn( + `[MultiStep] No action at iteration ${iterationCount}, forcing completion`, + ); break; } @@ -971,18 +1103,29 @@ export class CloudBootstrapMessageService implements IMessageService { } try { - if (!(await isLatestResponseId(runtime.agentId, message.roomId, responseId))) { - logger.info("[MultiStep] Newer message detected before action execution, cancelling"); + if ( + !(await isLatestResponseId( + runtime.agentId, + message.roomId, + responseId, + )) + ) { + logger.info( + "[MultiStep] Newer message detected before action execution, cancelling", + ); wasCancelled = true; break; } if (!accumulatedState.data) accumulatedState.data = {}; - if (!accumulatedState.data.workingMemory) accumulatedState.data.workingMemory = {}; + if (!accumulatedState.data.workingMemory) + accumulatedState.data.workingMemory = {}; const actionParams = parameters || {}; if (Object.keys(actionParams).length > 0) { - logger.debug(`[MultiStep] Parsed parameters: ${JSON.stringify(actionParams)}`); + logger.debug( + `[MultiStep] Parsed parameters: ${JSON.stringify(actionParams)}`, + ); } const hasActionParams = Object.keys(actionParams).length > 0; @@ -1047,7 +1190,10 @@ export class CloudBootstrapMessageService implements IMessageService { const result = capturedResult || (() => { - const actionResults = getActionResultsFromCache(runtime, message.id as string); + const actionResults = getActionResultsFromCache( + runtime, + message.id as string, + ); return actionResults.length > 0 ? (actionResults[0] as Record) : null; @@ -1066,7 +1212,8 @@ export class CloudBootstrapMessageService implements IMessageService { // # Previous Action Results on success — their side-effects (registering // new actions) are sufficient. Failures are still recorded so the LLM // can retry with different parameters. - const isTransparent = TRANSPARENT_META_ACTIONS.has(action) && actionResult.success; + const isTransparent = + TRANSPARENT_META_ACTIONS.has(action) && actionResult.success; if (!isTransparent) { traceActionResult.push(actionResult); } @@ -1078,13 +1225,17 @@ export class CloudBootstrapMessageService implements IMessageService { const data = (result as Record).data as | Record | undefined; - const newlyRegistered = data?.newlyRegistered as string[] | undefined; + const newlyRegistered = data?.newlyRegistered as + | string[] + | undefined; if (newlyRegistered?.length) { newlyRegistered.forEach((name) => discoveredActions.add(name)); if (message.id) { invalidateActionValidationCache(String(message.id)); } - logger.info(`[MultiStep] Discovered actions: ${newlyRegistered.join(", ")}`); + logger.info( + `[MultiStep] Discovered actions: ${newlyRegistered.join(", ")}`, + ); } } @@ -1101,21 +1252,30 @@ export class CloudBootstrapMessageService implements IMessageService { ); // Check if action requires user input before continuing - const resultData = result?.data as Record | undefined; + const resultData = result?.data as + | Record + | undefined; if (resultData?.awaitingUserInput === true) { - logger.info(`[MultiStep] Action ${action} awaiting user input, pausing loop`); + logger.info( + `[MultiStep] Action ${action} awaiting user input, pausing loop`, + ); break; } } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); - logger.error(`[MultiStep] Error executing action ${action}: ${errorMessage}`); + logger.error( + `[MultiStep] Error executing action ${action}: ${errorMessage}`, + ); traceActionResult.push({ data: { actionName: action || "unknown" }, success: false, error: errorMessage, }); - await streamThinking("actions", `\nAction ${action} error: ${errorMessage}\n`); + await streamThinking( + "actions", + `\nAction ${action} error: ${errorMessage}\n`, + ); consecutiveFailures++; } @@ -1137,7 +1297,9 @@ export class CloudBootstrapMessageService implements IMessageService { } if (iterationCount >= maxIterations) { - logger.warn(`[MultiStep] Reached maximum iterations (${maxIterations})`); + logger.warn( + `[MultiStep] Reached maximum iterations (${maxIterations})`, + ); if (!finishResponse && !incompleteReason) { incompleteReason = `The task hit the ${maxIterations}-step limit before it finished.`; } @@ -1154,7 +1316,9 @@ export class CloudBootstrapMessageService implements IMessageService { // If FINISH was called, use its response directly — skip summary LLM call if (finishResponse !== null) { - logger.info("[MultiStep] Using FINISH response, skipping summary LLM call"); + logger.info( + "[MultiStep] Using FINISH response, skipping summary LLM call", + ); const responseContent: Content = { actions: ["FINISH"], @@ -1236,12 +1400,16 @@ export class CloudBootstrapMessageService implements IMessageService { const summaryPrompt = composePromptFromState({ state: accumulatedState, - template: runtime.character.templates?.multiStepSummaryTemplate || multiStepSummaryTemplate, + template: + runtime.character.templates?.multiStepSummaryTemplate || + multiStepSummaryTemplate, }); // === LLM CALL LOG: multiStepSummary === logger.info("========== LLM CALL: multiStepSummary =========="); - logger.info(`[LLM:multiStepSummary] System Prompt:\n${runtime.character.system || "(none)"}`); + logger.info( + `[LLM:multiStepSummary] System Prompt:\n${runtime.character.system || "(none)"}`, + ); logger.info(`[LLM:multiStepSummary] User Prompt:\n${summaryPrompt}`); logger.info("=============================================="); @@ -1251,9 +1419,15 @@ export class CloudBootstrapMessageService implements IMessageService { let finalOutput = ""; let summary: Record | null = null; - for (let summaryAttempt = 1; summaryAttempt <= maxSummaryRetries; summaryAttempt++) { + for ( + let summaryAttempt = 1; + summaryAttempt <= maxSummaryRetries; + summaryAttempt++ + ) { try { - logger.debug(`[MultiStep] Summary generation attempt ${summaryAttempt}`); + logger.debug( + `[MultiStep] Summary generation attempt ${summaryAttempt}`, + ); finalOutput = await withScopedTextModel( "large", resolveResponseStepModel(runtime), @@ -1269,7 +1443,9 @@ export class CloudBootstrapMessageService implements IMessageService { summary = parseKeyValueXml(finalOutput); if (summary?.text) { - logger.debug(`[MultiStep] Parsed summary on attempt ${summaryAttempt}`); + logger.debug( + `[MultiStep] Parsed summary on attempt ${summaryAttempt}`, + ); break; } else { logger.warn( @@ -1281,13 +1457,16 @@ export class CloudBootstrapMessageService implements IMessageService { } } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); logger.error( `[MultiStep] Summary generation error on attempt ${summaryAttempt}:`, errorMessage, ); if (summaryAttempt >= maxSummaryRetries) { - logger.warn("[MultiStep] Failed to generate summary after all retries"); + logger.warn( + "[MultiStep] Failed to generate summary after all retries", + ); break; } const delay = getRetryDelay(summaryAttempt); @@ -1301,12 +1480,16 @@ export class CloudBootstrapMessageService implements IMessageService { actions: ["MULTI_STEP_SUMMARY"], text: summary.text as string, thought: - (summary.thought as string) || "Final user-facing message after task completion.", + (summary.thought as string) || + "Final user-facing message after task completion.", simple: true, }; if (options?.onStreamChunk) { - await options.onStreamChunk(summary.text as string, message.id as UUID); + await options.onStreamChunk( + summary.text as string, + message.id as UUID, + ); } } else { logger.warn(`[MultiStep] No valid summary generated, using fallback`); @@ -1362,7 +1545,8 @@ export class CloudBootstrapMessageService implements IMessageService { // doesn't send an empty system message (which OpenAI rejects). const originalSystemPrompt = runtime.character.system; if (!runtime.character.system) { - runtime.character.system = "You are a helpful AI assistant that responds to user messages."; + runtime.character.system = + "You are a helpful AI assistant that responds to user messages."; } try { @@ -1372,11 +1556,15 @@ export class CloudBootstrapMessageService implements IMessageService { true, ); - const template = runtime.character.templates?.messageHandlerTemplate || SINGLE_SHOT_TEMPLATE; + const template = + runtime.character.templates?.messageHandlerTemplate || + SINGLE_SHOT_TEMPLATE; const prompt = composePromptFromState({ state, template }); logger.info("========== LLM CALL: singleShot =========="); - logger.info(`[LLM:singleShot] System Prompt:\n${runtime.character.system || "(none)"}`); + logger.info( + `[LLM:singleShot] System Prompt:\n${runtime.character.system || "(none)"}`, + ); logger.info(`[LLM:singleShot] User Prompt:\n${prompt}`); logger.info("=============================================="); @@ -1423,7 +1611,9 @@ export class CloudBootstrapMessageService implements IMessageService { thought: String(parsedResponse.thought || ""), actions, source: message.content.source, - inReplyTo: message.id ? createUniqueUuid(runtime, message.id) : undefined, + inReplyTo: message.id + ? createUniqueUuid(runtime, message.id) + : undefined, }; if (options?.onStreamChunk && responseContent.text) { @@ -1496,13 +1686,14 @@ export class CloudBootstrapMessageService implements IMessageService { ); const respondChannels = new Set( - [...alwaysRespondChannels.map((t) => t.toString()), ...customChannels].map((s) => - s.trim().toLowerCase(), - ), + [ + ...alwaysRespondChannels.map((t) => t.toString()), + ...customChannels, + ].map((s) => s.trim().toLowerCase()), ); - const respondSources = [...alwaysRespondSources, ...customSources].map((s) => - s.trim().toLowerCase(), + const respondSources = [...alwaysRespondSources, ...customSources].map( + (s) => s.trim().toLowerCase(), ); const roomType = room.type?.toString().toLowerCase(); @@ -1527,7 +1718,9 @@ export class CloudBootstrapMessageService implements IMessageService { } // Platform mentions and replies: always respond - const hasPlatformMention = !!(mentionContext?.isMention || mentionContext?.isReply); + const hasPlatformMention = !!( + mentionContext?.isMention || mentionContext?.isReply + ); if (hasPlatformMention) { const mentionType = mentionContext?.isMention ? "mention" : "reply"; return { @@ -1545,7 +1738,10 @@ export class CloudBootstrapMessageService implements IMessageService { }; } - async processAttachments(runtime: IAgentRuntime, attachments: Media[]): Promise { + async processAttachments( + runtime: IAgentRuntime, + attachments: Media[], + ): Promise { if (!attachments?.length) return attachments; return Promise.all( @@ -1564,7 +1760,8 @@ export class CloudBootstrapMessageService implements IMessageService { attachment.description = typeof result === "string" ? result - : (result as { description?: string })?.description || "Image attachment"; + : (result as { description?: string })?.description || + "Image attachment"; } catch (error) { logger.warn( `[CloudBootstrap] Failed to generate image description for ${label}: ${error}`, @@ -1590,7 +1787,9 @@ export class CloudBootstrapMessageService implements IMessageService { async deleteMessage(runtime: IAgentRuntime, message: Memory): Promise { if (!message.id) { - logger.error("[CloudBootstrap] Cannot delete memory: message ID is missing"); + logger.error( + "[CloudBootstrap] Cannot delete memory: message ID is missing", + ); return; } @@ -1600,7 +1799,11 @@ export class CloudBootstrapMessageService implements IMessageService { await runtime.deleteMemory(message.id); } - async clearChannel(runtime: IAgentRuntime, roomId: UUID, channelId: string): Promise { + async clearChannel( + runtime: IAgentRuntime, + roomId: UUID, + channelId: string, + ): Promise { logger.info( `[CloudBootstrap] Clearing message memories from channel ${channelId} -> room ${roomId}`, ); @@ -1617,7 +1820,9 @@ export class CloudBootstrapMessageService implements IMessageService { await runtime.deleteMemory(memory.id); deletedCount++; } catch (error) { - logger.warn(`[CloudBootstrap] Failed to delete memory ${memory.id}: ${error}`); + logger.warn( + `[CloudBootstrap] Failed to delete memory ${memory.id}: ${error}`, + ); } } } diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/types.ts b/packages/lib/eliza/plugin-cloud-bootstrap/types.ts index 340aa2f00..d40f4f7ab 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/types.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/types.ts @@ -57,7 +57,9 @@ export function defineActionParameters( required: parameter.required, schema: { type: parameter.type, - default: (parameter.default as CoreActionParameter["schema"]["default"]) ?? undefined, + default: + (parameter.default as CoreActionParameter["schema"]["default"]) ?? + undefined, enum: parameter.enum?.map((value) => String(value), ) as CoreActionParameter["schema"]["enum"], @@ -76,7 +78,10 @@ export interface ParsedMultiStepDecision { isFinish?: string | boolean; } -export type StreamChunkCallback = (chunk: string, messageId?: UUID) => Promise; +export type StreamChunkCallback = ( + chunk: string, + messageId?: UUID, +) => Promise; export type ReasoningChunkCallback = ( chunk: string, diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/utils/context-routing.ts b/packages/lib/eliza/plugin-cloud-bootstrap/utils/context-routing.ts index def7f1388..02931889e 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/utils/context-routing.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/utils/context-routing.ts @@ -144,7 +144,9 @@ function normalizeContext(value: unknown): AgentContext | undefined { return undefined; } - return AGENT_CONTEXTS.includes(trimmed as AgentContext) ? (trimmed as AgentContext) : undefined; + return AGENT_CONTEXTS.includes(trimmed as AgentContext) + ? (trimmed as AgentContext) + : undefined; } function dedupeStringValues(values: string[]): string[] { @@ -177,7 +179,9 @@ function parseDelimitedList(value: unknown): string[] { if (Array.isArray(value)) { return dedupeStringValues( value.flatMap((entry) => - typeof entry === "string" ? entry.split(LIST_SPLIT_RE) : [String(entry)], + typeof entry === "string" + ? entry.split(LIST_SPLIT_RE) + : [String(entry)], ), ); } @@ -195,7 +199,9 @@ export function parseContextList(value: unknown): AgentContext[] { .filter((entry): entry is AgentContext => Boolean(entry)); } -export function parseContextRoutingMetadata(raw: unknown): ContextRoutingDecision { +export function parseContextRoutingMetadata( + raw: unknown, +): ContextRoutingDecision { if (!raw || typeof raw !== "object") { return {}; } @@ -203,7 +209,9 @@ export function parseContextRoutingMetadata(raw: unknown): ContextRoutingDecisio const value = raw as Record; const primaryContext = normalizeContext(value.primaryContext); const secondaryContexts = parseContextList(value.secondaryContexts); - const evidenceTurnIds = dedupeStringValues(parseDelimitedList(value.evidenceTurnIds)); + const evidenceTurnIds = dedupeStringValues( + parseDelimitedList(value.evidenceTurnIds), + ); return { primaryContext, @@ -212,7 +220,9 @@ export function parseContextRoutingMetadata(raw: unknown): ContextRoutingDecisio }; } -export function getContextRoutingFromMessage(message: Memory): ContextRoutingDecision { +export function getContextRoutingFromMessage( + message: Memory, +): ContextRoutingDecision { const metadata = message.content?.metadata; if (!metadata || typeof metadata !== "object") { return {}; @@ -223,7 +233,9 @@ export function getContextRoutingFromMessage(message: Memory): ContextRoutingDec ); } -export function getActiveRoutingContexts(routing: ContextRoutingDecision): AgentContext[] { +export function getActiveRoutingContexts( + routing: ContextRoutingDecision, +): AgentContext[] { const contextSet = new Set(["general"]); if (routing.primaryContext) { @@ -237,7 +249,10 @@ export function getActiveRoutingContexts(routing: ContextRoutingDecision): Agent return [...contextSet]; } -export function setContextRoutingMetadata(message: Memory, routing: ContextRoutingDecision): void { +export function setContextRoutingMetadata( + message: Memory, + routing: ContextRoutingDecision, +): void { const existingMetadata = message.content && typeof message.content.metadata === "object" ? (message.content.metadata as Record) @@ -257,7 +272,9 @@ export function setContextRoutingMetadata(message: Memory, routing: ContextRouti } export function resolveActionContexts(action: Action): AgentContext[] { - const declared = parseContextList((action as { contexts?: unknown }).contexts); + const declared = parseContextList( + (action as { contexts?: unknown }).contexts, + ); if (declared.length > 0) { return declared; } @@ -266,7 +283,9 @@ export function resolveActionContexts(action: Action): AgentContext[] { } function resolveProviderContexts(provider: Provider): AgentContext[] { - const declared = parseContextList((provider as { contexts?: unknown }).contexts); + const declared = parseContextList( + (provider as { contexts?: unknown }).contexts, + ); if (declared.length > 0) { return declared; } @@ -274,7 +293,10 @@ function resolveProviderContexts(provider: Provider): AgentContext[] { return PROVIDER_CONTEXT_MAP[provider.name] ?? ["general"]; } -export function deriveAvailableContexts(actions: Action[], providers: Provider[]): AgentContext[] { +export function deriveAvailableContexts( + actions: Action[], + providers: Provider[], +): AgentContext[] { const contexts = new Set(["general"]); for (const action of actions) { @@ -296,7 +318,10 @@ export function attachAvailableContexts( state: State, runtime: { actions: Action[]; providers: Provider[] }, ): State { - const availableContexts = deriveAvailableContexts(runtime.actions, runtime.providers); + const availableContexts = deriveAvailableContexts( + runtime.actions, + runtime.providers, + ); return { ...state, @@ -320,6 +345,8 @@ export function filterActionsByRouting( const activeContextSet = new Set(activeContexts); return actions.filter((action) => - resolveActionContexts(action).some((context) => activeContextSet.has(context)), + resolveActionContexts(action).some((context) => + activeContextSet.has(context), + ), ); } diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/utils/multi-step-guards.ts b/packages/lib/eliza/plugin-cloud-bootstrap/utils/multi-step-guards.ts index aa9ba5f56..bdd9d4ee3 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/utils/multi-step-guards.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/utils/multi-step-guards.ts @@ -7,7 +7,9 @@ export interface ValidatedMultiStepDecision { parameters: Record; } -function normalizeBoolean(value: string | boolean | undefined): boolean | undefined { +function normalizeBoolean( + value: string | boolean | undefined, +): boolean | undefined { if (typeof value === "boolean") { return value; } @@ -76,7 +78,10 @@ export function validateMultiStepDecision( parsedStep: ParsedMultiStepDecision, availableActionNames: Set, ): { decision?: ValidatedMultiStepDecision; error?: string } { - const action = typeof parsedStep.action === "string" ? parsedStep.action.trim() : undefined; + const action = + typeof parsedStep.action === "string" + ? parsedStep.action.trim() + : undefined; const normalizedIsFinish = normalizeBoolean(parsedStep.isFinish); if (parsedStep.isFinish !== undefined && normalizedIsFinish === undefined) { diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/utils/race-tracking.ts b/packages/lib/eliza/plugin-cloud-bootstrap/utils/race-tracking.ts index 33bfc784b..059fb6b12 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/utils/race-tracking.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/utils/race-tracking.ts @@ -7,11 +7,18 @@ function raceKey(agentId: string, roomId: string): string { return `cloud-bootstrap:latest-response:${agentId}:${roomId}`; } -function getLocalResponseId(agentId: string, roomId: string): string | undefined { +function getLocalResponseId( + agentId: string, + roomId: string, +): string | undefined { return localLatestResponseIds.get(agentId)?.get(roomId); } -function setLocalResponseId(agentId: string, roomId: string, responseId: string): void { +function setLocalResponseId( + agentId: string, + roomId: string, + responseId: string, +): void { if (!localLatestResponseIds.has(agentId)) { localLatestResponseIds.set(agentId, new Map()); } @@ -37,7 +44,11 @@ export async function setLatestResponseId( responseId: string, ): Promise { setLocalResponseId(agentId, roomId, responseId); - await cache.set(raceKey(agentId, roomId), responseId, RESPONSE_ID_TTL_SECONDS); + await cache.set( + raceKey(agentId, roomId), + responseId, + RESPONSE_ID_TTL_SECONDS, + ); } export async function getLatestResponseId( diff --git a/packages/lib/eliza/plugin-cloud-bootstrap/utils/state.ts b/packages/lib/eliza/plugin-cloud-bootstrap/utils/state.ts index 5f778cc1c..08b645bdc 100644 --- a/packages/lib/eliza/plugin-cloud-bootstrap/utils/state.ts +++ b/packages/lib/eliza/plugin-cloud-bootstrap/utils/state.ts @@ -9,7 +9,10 @@ export async function refreshStateAfterAction( currentState: State, actionResults: MultiStepActionResult[], ): Promise { - const refreshedState = await runtime.composeState(message, ["RECENT_MESSAGES", "ACTION_STATE"]); + const refreshedState = await runtime.composeState(message, [ + "RECENT_MESSAGES", + "ACTION_STATE", + ]); refreshedState.data.actionResults = actionResults as ActionResult[]; @@ -25,7 +28,10 @@ export async function refreshStateAfterAction( * WARNING: Uses internal elizaOS API - may break on core version upgrades. * Returns empty array on failure with warning logged. */ -export function getActionResultsFromCache(runtime: IAgentRuntime, messageId: string): unknown[] { +export function getActionResultsFromCache( + runtime: IAgentRuntime, + messageId: string, +): unknown[] { const runtimeWithCache = runtime as unknown as { stateCache?: Map; }; @@ -42,7 +48,9 @@ export function getActionResultsFromCache(runtime: IAgentRuntime, messageId: str const cachedState = runtimeWithCache.stateCache.get(cacheKey); if (!cachedState) { - logger.debug(`[getActionResultsFromCache] No cached state for key: ${cacheKey}`); + logger.debug( + `[getActionResultsFromCache] No cached state for key: ${cacheKey}`, + ); return []; } diff --git a/packages/lib/eliza/plugin-mcp/actions/dynamic-tool-actions.ts b/packages/lib/eliza/plugin-mcp/actions/dynamic-tool-actions.ts index 8e5c8f372..d488f19d1 100644 --- a/packages/lib/eliza/plugin-mcp/actions/dynamic-tool-actions.ts +++ b/packages/lib/eliza/plugin-mcp/actions/dynamic-tool-actions.ts @@ -20,10 +20,17 @@ import { export interface McpToolAction extends Omit { parameters?: ActionParameter[]; - _mcpMeta: { serverName: string; toolName: string; originalSchema: Tool["inputSchema"] }; + _mcpMeta: { + serverName: string; + toolName: string; + originalSchema: Tool["inputSchema"]; + }; } -function extractParams(message: Memory, state?: State): Record { +function extractParams( + message: Memory, + state?: State, +): Record { const content = message.content as Record; return ( (content.actionParams as Record) || @@ -60,10 +67,19 @@ export function createMcpToolAction( if (svc.isLazyConnection(serverName)) return true; const server = svc.getServers().find((s) => s.name === serverName); - return server?.status === "connected" && !!server.tools?.some((t) => t.name === tool.name); + return ( + server?.status === "connected" && + !!server.tools?.some((t) => t.name === tool.name) + ); }, - handler: async (runtime, message, state, _options, callback): Promise => { + handler: async ( + runtime, + message, + state, + _options, + callback, + ): Promise => { const svc = runtime.getService(MCP_SERVICE_NAME); if (!svc) { return { @@ -74,12 +90,18 @@ export function createMcpToolAction( } const params = extractParams(message, state); - logger.info({ serverName, toolName: tool.name, params }, `[MCP] Executing ${actionName}`); + logger.info( + { serverName, toolName: tool.name, params }, + `[MCP] Executing ${actionName}`, + ); const errors = validateParamsAgainstSchema(params, tool.inputSchema); const missing = errors.filter((e) => e.startsWith("Missing required")); if (missing.length > 0) { - logger.error({ missing, params }, `[MCP] Missing required params for ${actionName}`); + logger.error( + { missing, params }, + `[MCP] Missing required params for ${actionName}`, + ); return { success: false, error: missing.join(", "), @@ -89,7 +111,10 @@ export function createMcpToolAction( const warnings = errors.filter((e) => !e.startsWith("Missing required")); if (warnings.length > 0) { - logger.warn({ warnings, params }, `[MCP] Type warnings for ${actionName}`); + logger.warn( + { warnings, params }, + `[MCP] Type warnings for ${actionName}`, + ); } const result = await svc.callTool(serverName, tool.name, params); @@ -102,7 +127,10 @@ export function createMcpToolAction( ); if (result.isError) { - logger.error({ serverName, toolName: tool.name, output: toolOutput }, "[MCP] Tool error"); + logger.error( + { serverName, toolName: tool.name, output: toolOutput }, + "[MCP] Tool error", + ); return { success: false, error: toolOutput || "Tool execution failed", @@ -118,7 +146,10 @@ export function createMcpToolAction( } if (callback && hasAttachments && attachments.length > 0) { - await callback({ text: `Executed ${serverName}/${tool.name}`, attachments }); + await callback({ + text: `Executed ${serverName}/${tool.name}`, + attachments, + }); } return { @@ -147,12 +178,19 @@ export function createMcpToolAction( { name: "{{user}}", content: { text: `Can you use ${tool.name}?` } }, { name: "{{assistant}}", - content: { text: `I'll execute ${tool.name} for you.`, actions: [actionName] }, + content: { + text: `I'll execute ${tool.name} for you.`, + actions: [actionName], + }, }, ], ], - _mcpMeta: { serverName, toolName: tool.name, originalSchema: tool.inputSchema }, + _mcpMeta: { + serverName, + toolName: tool.name, + originalSchema: tool.inputSchema, + }, }; } @@ -178,8 +216,13 @@ export function createMcpToolActions( return actions; } -export function isMcpToolAction(action: Action | McpToolAction): action is McpToolAction { - return "_mcpMeta" in action && typeof (action as McpToolAction)._mcpMeta === "object"; +export function isMcpToolAction( + action: Action | McpToolAction, +): action is McpToolAction { + return ( + "_mcpMeta" in action && + typeof (action as McpToolAction)._mcpMeta === "object" + ); } export function getMcpToolActionsForServer( @@ -187,6 +230,7 @@ export function getMcpToolActionsForServer( serverName: string, ): McpToolAction[] { return actions.filter( - (a): a is McpToolAction => isMcpToolAction(a) && a._mcpMeta.serverName === serverName, + (a): a is McpToolAction => + isMcpToolAction(a) && a._mcpMeta.serverName === serverName, ); } diff --git a/packages/lib/eliza/plugin-mcp/actions/readResourceAction.ts b/packages/lib/eliza/plugin-mcp/actions/readResourceAction.ts index f7b65bd63..ac7fdce42 100644 --- a/packages/lib/eliza/plugin-mcp/actions/readResourceAction.ts +++ b/packages/lib/eliza/plugin-mcp/actions/readResourceAction.ts @@ -26,8 +26,14 @@ import { } from "../utils/validation"; import { withModelRetry } from "../utils/wrapper"; -function createResourceSelectionPrompt(composedState: State, userMessage: string): string { - const mcpData = (composedState.values.mcp || {}) as Record; +function createResourceSelectionPrompt( + composedState: State, + userMessage: string, +): string { + const mcpData = (composedState.values.mcp || {}) as Record< + string, + McpServerInfo + >; const serverNames = Object.keys(mcpData); let resourcesDescription = ""; @@ -76,7 +82,11 @@ export const readResourceAction: Action = { ], description: "Reads a resource from an MCP server", - validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State): Promise => { + validate: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ): Promise => { const mcpService = runtime.getService(MCP_SERVICE_NAME); if (!mcpService) return false; @@ -89,7 +99,9 @@ export const readResourceAction: Action = { servers.length > 0 && servers.some( (server: McpServer) => - server.status === "connected" && server.resources && server.resources.length > 0, + server.status === "connected" && + server.resources && + server.resources.length > 0, ) ); }, @@ -101,7 +113,10 @@ export const readResourceAction: Action = { _options?: { [key: string]: unknown }, callback?: HandlerCallback, ): Promise => { - const composedState = await runtime.composeState(message, ["RECENT_MESSAGES", "MCP"]); + const composedState = await runtime.composeState(message, [ + "RECENT_MESSAGES", + "MCP", + ]); const mcpService = runtime.getService(MCP_SERVICE_NAME); if (!mcpService) { @@ -129,7 +144,12 @@ export const readResourceAction: Action = { callback, input: resourceSelection, validationFn: (data) => validateResourceSelection(data), - createFeedbackPromptFn: (originalResponse, errorMessage, state, userMessage) => + createFeedbackPromptFn: ( + originalResponse, + errorMessage, + state, + userMessage, + ) => createResourceSelectionFeedbackPrompt( originalResponse as string, errorMessage, @@ -163,7 +183,8 @@ export const readResourceAction: Action = { data: { actionName: "READ_MCP_RESOURCE", noResourceAvailable: true, - reason: parsedSelection?.reasoning || "No appropriate resource available", + reason: + parsedSelection?.reasoning || "No appropriate resource available", }, success: true, }; @@ -174,14 +195,21 @@ export const readResourceAction: Action = { return { text: "No resource selected.", success: false }; } - logger.debug(`Selected resource "${uri}" on server "${serverName}" because: ${reasoning}`); + logger.debug( + `Selected resource "${uri}" on server "${serverName}" because: ${reasoning}`, + ); const result = await mcpService.readResource(serverName, uri); logger.debug(`Read resource ${uri} from server ${serverName}`); const { resourceContent, resourceMeta } = processResourceResult( result as { - contents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }>; + contents: Array<{ + uri: string; + mimeType?: string; + text?: string; + blob?: string; + }>; }, uri, ); diff --git a/packages/lib/eliza/plugin-mcp/actions/search-actions.ts b/packages/lib/eliza/plugin-mcp/actions/search-actions.ts index 72c16ee54..5f6b1e7f7 100644 --- a/packages/lib/eliza/plugin-mcp/actions/search-actions.ts +++ b/packages/lib/eliza/plugin-mcp/actions/search-actions.ts @@ -7,7 +7,10 @@ import { type Memory, type State, } from "@elizaos/core"; -import { type ActionWithParams, defineActionParameters } from "../../plugin-cloud-bootstrap/types"; +import { + type ActionWithParams, + defineActionParameters, +} from "../../plugin-cloud-bootstrap/types"; import type { McpService } from "../service"; import { MCP_SERVICE_NAME } from "../types"; import { createMcpToolAction } from "./dynamic-tool-actions"; @@ -41,7 +44,8 @@ export const searchActionsAction: ActionWithParams = { }, platform: { type: "string", - description: "Filter results to a single connected platform. Omit to search all.", + description: + "Filter results to a single connected platform. Omit to search all.", required: false, enum: [ "google", @@ -141,7 +145,11 @@ export const searchActionsAction: ActionWithParams = { alreadyRegistered.push(entry.actionName); continue; } - const action = createMcpToolAction(entry.serverName, entry.tool, existingNames); + const action = createMcpToolAction( + entry.serverName, + entry.tool, + existingNames, + ); runtime.registerAction(action); existingNames.add(action.name); newlyRegistered.push(action.name); @@ -269,7 +277,10 @@ export const listConnectionsAction: Action = { }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); - logger.error({ error: msg }, "[LIST_CONNECTIONS] Failed to fetch connections"); + logger.error( + { error: msg }, + "[LIST_CONNECTIONS] Failed to fetch connections", + ); if (msg.includes("Cannot find module")) { return { success: false, error: "OAuth service not available" }; } diff --git a/packages/lib/eliza/plugin-mcp/index.ts b/packages/lib/eliza/plugin-mcp/index.ts index 623c0216a..f1720f8a0 100644 --- a/packages/lib/eliza/plugin-mcp/index.ts +++ b/packages/lib/eliza/plugin-mcp/index.ts @@ -1,6 +1,9 @@ import { type IAgentRuntime, logger, type Plugin } from "@elizaos/core"; import { readResourceAction } from "./actions/readResourceAction"; -import { listConnectionsAction, searchActionsAction } from "./actions/search-actions"; +import { + listConnectionsAction, + searchActionsAction, +} from "./actions/search-actions"; import { provider } from "./provider"; import { McpService } from "./service"; diff --git a/packages/lib/eliza/plugin-mcp/search/bm25-index.ts b/packages/lib/eliza/plugin-mcp/search/bm25-index.ts index 5e6124453..cabe10439 100644 --- a/packages/lib/eliza/plugin-mcp/search/bm25-index.ts +++ b/packages/lib/eliza/plugin-mcp/search/bm25-index.ts @@ -45,7 +45,12 @@ export class Tier2ToolIndex { * Optionally filters by platform (e.g., "linear", "github"). * Returns up to `limit` matching entries sorted by BM25 relevance. */ - search(query: string, platform?: string, limit = 10, offset = 0): Tier2ToolEntry[] { + search( + query: string, + platform?: string, + limit = 10, + offset = 0, + ): Tier2ToolEntry[] { if (!this.bm25 || this.tools.length === 0) return []; // Over-fetch to allow for platform filtering and offset @@ -53,7 +58,9 @@ export class Tier2ToolIndex { let entries = results.map((r) => this.tools[r.index]); if (platform) { - entries = entries.filter((e) => e.platform.toLowerCase() === platform.toLowerCase()); + entries = entries.filter( + (e) => e.platform.toLowerCase() === platform.toLowerCase(), + ); } return entries.slice(offset, offset + limit); diff --git a/packages/lib/eliza/plugin-mcp/service.ts b/packages/lib/eliza/plugin-mcp/service.ts index 5a6c169eb..1229edc9c 100644 --- a/packages/lib/eliza/plugin-mcp/service.ts +++ b/packages/lib/eliza/plugin-mcp/service.ts @@ -22,7 +22,10 @@ const logMcpBootstrap = (level: "info" | "warn", message: string): void => { const requestContextReady = (async () => { try { const mod = await import("@elizaos/core"); - if ("getRequestContext" in mod && typeof mod.getRequestContext === "function") { + if ( + "getRequestContext" in mod && + typeof mod.getRequestContext === "function" + ) { getRequestContext = mod.getRequestContext as typeof getRequestContext; logMcpBootstrap( "info", @@ -52,10 +55,16 @@ import type { Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { createMcpToolActions, type McpToolAction } from "./actions/dynamic-tool-actions"; +import { + createMcpToolActions, + type McpToolAction, +} from "./actions/dynamic-tool-actions"; import { getSchemaCache, McpSchemaCache } from "./cache/schema-cache"; import { type Tier2ToolEntry, Tier2ToolIndex } from "./search/bm25-index"; -import { createMcpToolCompatibilitySync, type McpToolCompatibility } from "./tool-compatibility"; +import { + createMcpToolCompatibilitySync, + type McpToolCompatibility, +} from "./tool-compatibility"; import { getCrucialToolsForServer, isCrucialTool } from "./tool-visibility"; import { BACKOFF_MULTIPLIER, @@ -77,7 +86,8 @@ import { import { toActionName } from "./utils/action-naming"; import { buildMcpProviderData } from "./utils/mcp"; -const err = (e: unknown): string => (e instanceof Error ? e.message : String(e)); +const err = (e: unknown): string => + e instanceof Error ? e.message : String(e); export class McpService extends Service { static serviceType = MCP_SERVICE_NAME; @@ -147,7 +157,10 @@ export class McpService extends Service { try { // In production, only streamable-http is supported (SSE is deprecated on Vercel). // stdio is only allowed in development/local environments. - if (process.env.NODE_ENV === "production" && config.type !== "streamable-http") { + if ( + process.env.NODE_ENV === "production" && + config.type !== "streamable-http" + ) { logger.warn( `[MCP] Skipping server "${name}": transport "${config.type}" is not supported in production (only streamable-http is allowed)`, ); @@ -159,9 +172,16 @@ export class McpService extends Service { // Try cache first if (this.schemaCache.isEnabled) { - const cached = await this.schemaCache.getSchemas(this.runtime.agentId, name, hash); + const cached = await this.schemaCache.getSchemas( + this.runtime.agentId, + name, + hash, + ); if (cached) { - this.registerToolsAsActions(name, McpSchemaCache.toTools(cached)); + this.registerToolsAsActions( + name, + McpSchemaCache.toTools(cached), + ); this.lazyConnections.set(name, config); results.cached.push(`${name}:${cached.tools.length}`); return; @@ -175,7 +195,12 @@ export class McpService extends Service { await this.connect(name, config); const server = this.connections.get(name)?.server; if (this.schemaCache.isEnabled && server?.tools?.length) { - await this.schemaCache.setSchemas(this.runtime.agentId, name, hash, server.tools); + await this.schemaCache.setSchemas( + this.runtime.agentId, + name, + hash, + server.tools, + ); } if (isHttpTransport) { @@ -190,7 +215,10 @@ export class McpService extends Service { results.connected.push(`${name}:${server?.tools?.length || 0}`); } } catch (e) { - logger.error({ error: err(e), server: name }, `[MCP] Failed: ${name}`); + logger.error( + { error: err(e), server: name }, + `[MCP] Failed: ${name}`, + ); results.failed.push(name); } }), @@ -210,7 +238,12 @@ export class McpService extends Service { } private getSettings(): McpSettings | undefined { - let s = this.runtime.getSetting("mcp") as McpSettings | string | boolean | number | null; + let s = this.runtime.getSetting("mcp") as + | McpSettings + | string + | boolean + | number + | null; if (!s || typeof s !== "object" || !("servers" in s)) { const rt = this.runtime as IAgentRuntime & { character?: { settings?: { mcp?: McpSettings } }; @@ -221,7 +254,9 @@ export class McpService extends Service { s = rt.settings?.mcp ?? null; } } - return s && typeof s === "object" && "servers" in s ? (s as McpSettings) : undefined; + return s && typeof s === "object" && "servers" in s + ? (s as McpSettings) + : undefined; } // ─── Connection Management ───────────────────────────────────────────────── @@ -240,7 +275,10 @@ export class McpService extends Service { this.connectionStates.set(name, state); try { - const client = new Client({ name: "ElizaOS", version: "1.0.0" }, { capabilities: {} }); + const client = new Client( + { name: "ElizaOS", version: "1.0.0" }, + { capabilities: {} }, + ); const transport = config.type === "stdio" ? this.createStdioTransport(name, config) @@ -264,7 +302,9 @@ export class McpService extends Service { const caps = client.getServerCapabilities(); const tools = await this.fetchTools(name); const resources = caps?.resources ? await this.fetchResources(name) : []; - const resourceTemplates = caps?.resources ? await this.fetchResourceTemplates(name) : []; + const resourceTemplates = caps?.resources + ? await this.fetchResourceTemplates(name) + : []; conn.server = { status: "connected", @@ -318,8 +358,12 @@ export class McpService extends Service { } } - private createStdioTransport(name: string, config: StdioMcpServerConfig): StdioClientTransport { - if (!config.command) throw new Error(`Missing command for stdio server ${name}`); + private createStdioTransport( + name: string, + config: StdioMcpServerConfig, + ): StdioClientTransport { + if (!config.command) + throw new Error(`Missing command for stdio server ${name}`); return new StdioClientTransport({ command: config.command, args: config.args, @@ -346,7 +390,9 @@ export class McpService extends Service { if (apiKey && typeof apiKey === "string" && !headers["X-API-Key"]) { // Only inject for same-origin to prevent leaking to external domains const baseUrl = - process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000"; + process.env.NEXT_PUBLIC_APP_URL || + process.env.APP_URL || + "http://localhost:3000"; try { if (url.origin === new URL(baseUrl).origin) { headers["X-API-Key"] = apiKey; @@ -361,7 +407,10 @@ export class McpService extends Service { this.connectionApiKeys.set(name, apiKey); } - const opts = Object.keys(headers).length > 0 ? { requestInit: { headers } } : undefined; + const opts = + Object.keys(headers).length > 0 + ? { requestInit: { headers } } + : undefined; return new StreamableHTTPClientTransport(url, opts); } @@ -374,13 +423,24 @@ export class McpService extends Service { conn.transport.onerror = async (e) => { const isAbortError = e?.name === "AbortError"; if (!isStdio && isAbortError) { - logger.debug({ error: e, server: name }, `[MCP] Suppressed transport AbortError: ${name}`); + logger.debug( + { error: e, server: name }, + `[MCP] Suppressed transport AbortError: ${name}`, + ); return; } const msg = e?.message || ""; - if (isStdio || (!msg.includes("SSE") && !msg.includes("timeout") && msg !== "undefined")) { - logger.error({ error: e, server: name }, `[MCP] Transport error: ${name}`); + if ( + isStdio || + (!msg.includes("SSE") && + !msg.includes("timeout") && + msg !== "undefined") + ) { + logger.error( + { error: e, server: name }, + `[MCP] Transport error: ${name}`, + ); conn.server.status = "disconnected"; conn.server.error = `${conn.server.error || ""}\n${msg}`; } @@ -409,13 +469,19 @@ export class McpService extends Service { await Promise.race([ conn.client.listTools(), new Promise((_, r) => - setTimeout(() => r(new Error("Ping timeout")), this.pingConfig.timeoutMs), + setTimeout( + () => r(new Error("Ping timeout")), + this.pingConfig.timeoutMs, + ), ), ]); state.consecutivePingFailures = 0; } catch (e) { state.consecutivePingFailures++; - if (state.consecutivePingFailures >= this.pingConfig.failuresBeforeDisconnect) { + if ( + state.consecutivePingFailures >= + this.pingConfig.failuresBeforeDisconnect + ) { this.handleDisconnect(name, e); } } @@ -435,7 +501,8 @@ export class McpService extends Service { return; } - const delay = INITIAL_RETRY_DELAY * BACKOFF_MULTIPLIER ** state.reconnectAttempts; + const delay = + INITIAL_RETRY_DELAY * BACKOFF_MULTIPLIER ** state.reconnectAttempts; state.reconnectTimeout = setTimeout(async () => { state.reconnectAttempts++; const config = this.connections.get(name)?.server.config; @@ -459,7 +526,8 @@ export class McpService extends Service { return (res?.tools || []).map((t) => { if (!t.inputSchema) return t; const toolCompatibility = - this.toolCompatibility ?? createMcpToolCompatibilitySync(this.runtime); + this.toolCompatibility ?? + createMcpToolCompatibilitySync(this.runtime); this.toolCompatibility = toolCompatibility; if (!toolCompatibility) return t; try { @@ -476,27 +544,42 @@ export class McpService extends Service { } }); } catch (e) { - logger.warn({ error: err(e), server: name }, "[MCP] Failed to fetch tools"); + logger.warn( + { error: err(e), server: name }, + "[MCP] Failed to fetch tools", + ); return []; } } private async fetchResources(name: string): Promise { try { - return (await this.connections.get(name)?.client.listResources())?.resources || []; + return ( + (await this.connections.get(name)?.client.listResources())?.resources || + [] + ); } catch (e) { - logger.debug({ error: err(e), server: name }, "[MCP] Failed to fetch resources"); + logger.debug( + { error: err(e), server: name }, + "[MCP] Failed to fetch resources", + ); return []; } } - private async fetchResourceTemplates(name: string): Promise { + private async fetchResourceTemplates( + name: string, + ): Promise { try { return ( - (await this.connections.get(name)?.client.listResourceTemplates())?.resourceTemplates || [] + (await this.connections.get(name)?.client.listResourceTemplates()) + ?.resourceTemplates || [] ); } catch (e) { - logger.debug({ error: err(e), server: name }, "[MCP] Failed to fetch resource templates"); + logger.debug( + { error: err(e), server: name }, + "[MCP] Failed to fetch resource templates", + ); return []; } } @@ -572,7 +655,9 @@ export class McpService extends Service { // Remove Tier-2 entries and rebuild index const hadTier2 = this.tier2Tools.some((t) => t.serverName === serverName); if (hadTier2) { - this.tier2Tools = this.tier2Tools.filter((t) => t.serverName !== serverName); + this.tier2Tools = this.tier2Tools.filter( + (t) => t.serverName !== serverName, + ); this.tier2Index.build(this.tier2Tools); } } @@ -670,9 +755,13 @@ export class McpService extends Service { } } - private async doConnect(connectionKey: string, serverName: string): Promise { + private async doConnect( + connectionKey: string, + serverName: string, + ): Promise { // Re-check after acquiring the "lock" - if (this.connections.has(connectionKey) || this.connections.has(serverName)) return; + if (this.connections.has(connectionKey) || this.connections.has(serverName)) + return; const config = this.lazyConnections.get(serverName); if (!config) throw new Error(`Unknown server: ${serverName}`); @@ -711,9 +800,13 @@ export class McpService extends Service { const config = JSON.parse(conn.server.config); const timeout = config.timeoutInMillis || DEFAULT_MCP_TIMEOUT_MS; - const result = await conn.client.callTool({ name: toolName, arguments: args }, undefined, { - timeout, - }); + const result = await conn.client.callTool( + { name: toolName, arguments: args }, + undefined, + { + timeout, + }, + ); if (!result.content) throw new Error("Invalid tool result"); return result as CallToolResult; } diff --git a/packages/lib/eliza/plugin-mcp/tool-compatibility/base.ts b/packages/lib/eliza/plugin-mcp/tool-compatibility/base.ts index cc190873b..bbc81e76e 100644 --- a/packages/lib/eliza/plugin-mcp/tool-compatibility/base.ts +++ b/packages/lib/eliza/plugin-mcp/tool-compatibility/base.ts @@ -1,6 +1,11 @@ import type { JSONSchema7 } from "json-schema"; -export type ModelProvider = "openai" | "anthropic" | "google" | "openrouter" | "unknown"; +export type ModelProvider = + | "openai" + | "anthropic" + | "google" + | "openrouter" + | "unknown"; export interface ModelInfo { provider: ModelProvider; @@ -18,8 +23,12 @@ export abstract class McpToolCompatibility { abstract shouldApply(): boolean; - transformToolSchema(toolSchema: TSchema): TSchema { - return this.shouldApply() ? (this.processSchema(toolSchema) as TSchema) : toolSchema; + transformToolSchema( + toolSchema: TSchema, + ): TSchema { + return this.shouldApply() + ? (this.processSchema(toolSchema) as TSchema) + : toolSchema; } protected processSchema(schema: JSONSchema7): JSONSchema7 { @@ -27,10 +36,16 @@ export abstract class McpToolCompatibility { switch (processed.type) { case "string": - return this.processTypeSchema(processed, this.getUnsupportedStringProperties()); + return this.processTypeSchema( + processed, + this.getUnsupportedStringProperties(), + ); case "number": case "integer": - return this.processTypeSchema(processed, this.getUnsupportedNumberProperties()); + return this.processTypeSchema( + processed, + this.getUnsupportedNumberProperties(), + ); case "array": return this.processArraySchema(processed); case "object": @@ -40,7 +55,10 @@ export abstract class McpToolCompatibility { } } - protected processTypeSchema(schema: JSONSchema7, unsupported: string[]): JSONSchema7 { + protected processTypeSchema( + schema: JSONSchema7, + unsupported: string[], + ): JSONSchema7 { const processed = { ...schema }; const constraints: Record = {}; @@ -70,16 +88,26 @@ export abstract class McpToolCompatibility { } if (Object.keys(constraints).length > 0) { - processed.description = this.mergeDescription(schema.description, constraints); + processed.description = this.mergeDescription( + schema.description, + constraints, + ); } return processed; } protected processArraySchema(schema: JSONSchema7): JSONSchema7 { - const processed = this.processTypeSchema(schema, this.getUnsupportedArrayProperties()); - - if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) { + const processed = this.processTypeSchema( + schema, + this.getUnsupportedArrayProperties(), + ); + + if ( + schema.items && + typeof schema.items === "object" && + !Array.isArray(schema.items) + ) { processed.items = this.processSchema(schema.items as JSONSchema7); } @@ -87,7 +115,10 @@ export abstract class McpToolCompatibility { } protected processObjectSchema(schema: JSONSchema7): JSONSchema7 { - const processed = this.processTypeSchema(schema, this.getUnsupportedObjectProperties()); + const processed = this.processTypeSchema( + schema, + this.getUnsupportedObjectProperties(), + ); if (schema.properties) { processed.properties = {}; @@ -107,8 +138,9 @@ export abstract class McpToolCompatibility { for (const key of ["oneOf", "anyOf", "allOf"] as const) { if (Array.isArray(schema[key])) { - (processed as Record)[key] = schema[key]!.map((s: any) => - typeof s === "object" ? this.processSchema(s as JSONSchema7) : s, + (processed as Record)[key] = schema[key]!.map( + (s: any) => + typeof s === "object" ? this.processSchema(s as JSONSchema7) : s, ); } } diff --git a/packages/lib/eliza/plugin-mcp/tool-compatibility/index.ts b/packages/lib/eliza/plugin-mcp/tool-compatibility/index.ts index 6e98c0b6e..6590d8af7 100644 --- a/packages/lib/eliza/plugin-mcp/tool-compatibility/index.ts +++ b/packages/lib/eliza/plugin-mcp/tool-compatibility/index.ts @@ -1,14 +1,22 @@ -export { McpToolCompatibility, type ModelInfo, type ModelProvider } from "./base"; +export { + McpToolCompatibility, + type ModelInfo, + type ModelProvider, +} from "./base"; import type { IAgentRuntime } from "@elizaos/core"; import type { ModelInfo, ModelProvider } from "./base"; import { AnthropicMcpCompatibility } from "./providers/anthropic"; import { GoogleMcpCompatibility } from "./providers/google"; -import { OpenAIMcpCompatibility, OpenAIReasoningMcpCompatibility } from "./providers/openai"; +import { + OpenAIMcpCompatibility, + OpenAIReasoningMcpCompatibility, +} from "./providers/openai"; export function detectModelProvider(runtime: IAgentRuntime): ModelInfo { const providerString = String( - (runtime as IAgentRuntime & { modelProvider?: string })?.modelProvider || "", + (runtime as IAgentRuntime & { modelProvider?: string })?.modelProvider || + "", ).toLowerCase(); const modelString = String( (runtime as IAgentRuntime & { model?: string })?.model || "", @@ -26,15 +34,26 @@ export function detectModelProvider(runtime: IAgentRuntime): ModelInfo { ) { provider = "openai"; supportsStructuredOutputs = - modelString.includes("gpt-4") || modelString.includes("o1") || modelString.includes("o3"); + modelString.includes("gpt-4") || + modelString.includes("o1") || + modelString.includes("o3"); isReasoningModel = modelString.includes("o1") || modelString.includes("o3"); - } else if (providerString.includes("anthropic") || modelString.includes("claude")) { + } else if ( + providerString.includes("anthropic") || + modelString.includes("claude") + ) { provider = "anthropic"; supportsStructuredOutputs = true; - } else if (providerString.includes("google") || modelString.includes("gemini")) { + } else if ( + providerString.includes("google") || + modelString.includes("gemini") + ) { provider = "google"; supportsStructuredOutputs = true; - } else if (providerString.includes("openrouter") || modelString.includes("openrouter")) { + } else if ( + providerString.includes("openrouter") || + modelString.includes("openrouter") + ) { provider = "openrouter"; } diff --git a/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/anthropic.ts b/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/anthropic.ts index cde1d6d5d..978e3b022 100644 --- a/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/anthropic.ts +++ b/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/anthropic.ts @@ -27,7 +27,8 @@ export class AnthropicMcpCompatibility extends McpToolCompatibility { constraints: Record, ): string { const hints: string[] = []; - if (constraints.additionalProperties === false) hints.push("Only use the specified properties"); + if (constraints.additionalProperties === false) + hints.push("Only use the specified properties"); if (constraints.format === "date-time") hints.push("Use ISO 8601 format"); if (constraints.pattern) hints.push(`Must match: ${constraints.pattern}`); diff --git a/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/google.ts b/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/google.ts index 43a1f79f9..299170f34 100644 --- a/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/google.ts +++ b/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/google.ts @@ -14,7 +14,13 @@ export class GoogleMcpCompatibility extends McpToolCompatibility { } protected getUnsupportedNumberProperties(): string[] { - return ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"]; + return [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + ]; } protected getUnsupportedArrayProperties(): string[] { @@ -30,14 +36,20 @@ export class GoogleMcpCompatibility extends McpToolCompatibility { constraints: Record, ): string { const rules: string[] = []; - if (constraints.minLength) rules.push(`at least ${constraints.minLength} chars`); - if (constraints.maxLength) rules.push(`at most ${constraints.maxLength} chars`); - if (constraints.minimum !== undefined) rules.push(`>= ${constraints.minimum}`); - if (constraints.maximum !== undefined) rules.push(`<= ${constraints.maximum}`); + if (constraints.minLength) + rules.push(`at least ${constraints.minLength} chars`); + if (constraints.maxLength) + rules.push(`at most ${constraints.maxLength} chars`); + if (constraints.minimum !== undefined) + rules.push(`>= ${constraints.minimum}`); + if (constraints.maximum !== undefined) + rules.push(`<= ${constraints.maximum}`); if (constraints.format === "email") rules.push(`valid email`); - if (constraints.format === "uri" || constraints.format === "url") rules.push(`valid URL`); + if (constraints.format === "uri" || constraints.format === "url") + rules.push(`valid URL`); if (constraints.pattern) rules.push(`matches ${constraints.pattern}`); - if (constraints.enum) rules.push(`one of: ${(constraints.enum as string[]).join(", ")}`); + if (constraints.enum) + rules.push(`one of: ${(constraints.enum as string[]).join(", ")}`); if (constraints.minItems) rules.push(`>= ${constraints.minItems} items`); if (constraints.maxItems) rules.push(`<= ${constraints.maxItems} items`); if (constraints.uniqueItems) rules.push(`unique items`); diff --git a/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/openai.ts b/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/openai.ts index c5f1f0082..97b81c877 100644 --- a/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/openai.ts +++ b/packages/lib/eliza/plugin-mcp/tool-compatibility/providers/openai.ts @@ -8,12 +8,14 @@ export class OpenAIMcpCompatibility extends McpToolCompatibility { shouldApply(): boolean { return ( this.modelInfo.provider === "openai" && - (!this.modelInfo.supportsStructuredOutputs || this.modelInfo.isReasoningModel === true) + (!this.modelInfo.supportsStructuredOutputs || + this.modelInfo.isReasoningModel === true) ); } protected getUnsupportedStringProperties(): string[] { - return this.modelInfo.isReasoningModel || this.modelInfo.modelId.includes("gpt-3.5") + return this.modelInfo.isReasoningModel || + this.modelInfo.modelId.includes("gpt-3.5") ? ["format", "pattern"] : ["format"]; } @@ -39,7 +41,10 @@ export class OpenAIReasoningMcpCompatibility extends McpToolCompatibility { } shouldApply(): boolean { - return this.modelInfo.provider === "openai" && this.modelInfo.isReasoningModel === true; + return ( + this.modelInfo.provider === "openai" && + this.modelInfo.isReasoningModel === true + ); } protected getUnsupportedStringProperties(): string[] { @@ -63,20 +68,31 @@ export class OpenAIReasoningMcpCompatibility extends McpToolCompatibility { constraints: Record, ): string { const rules: string[] = []; - if (constraints.minLength) rules.push(`minimum ${constraints.minLength} characters`); - if (constraints.maxLength) rules.push(`maximum ${constraints.maxLength} characters`); - if (constraints.minimum !== undefined) rules.push(`must be >= ${constraints.minimum}`); - if (constraints.maximum !== undefined) rules.push(`must be <= ${constraints.maximum}`); + if (constraints.minLength) + rules.push(`minimum ${constraints.minLength} characters`); + if (constraints.maxLength) + rules.push(`maximum ${constraints.maxLength} characters`); + if (constraints.minimum !== undefined) + rules.push(`must be >= ${constraints.minimum}`); + if (constraints.maximum !== undefined) + rules.push(`must be <= ${constraints.maximum}`); if (constraints.format === "email") rules.push(`must be a valid email`); if (constraints.format === "uri" || constraints.format === "url") rules.push(`must be a valid URL`); if (constraints.pattern) rules.push(`must match: ${constraints.pattern}`); if (constraints.enum) - rules.push(`must be one of: ${(constraints.enum as string[]).join(", ")}`); - if (constraints.minItems) rules.push(`at least ${constraints.minItems} items`); - if (constraints.maxItems) rules.push(`at most ${constraints.maxItems} items`); + rules.push( + `must be one of: ${(constraints.enum as string[]).join(", ")}`, + ); + if (constraints.minItems) + rules.push(`at least ${constraints.minItems} items`); + if (constraints.maxItems) + rules.push(`at most ${constraints.maxItems} items`); - const text = rules.length > 0 ? `IMPORTANT: ${rules.join(", ")}` : JSON.stringify(constraints); + const text = + rules.length > 0 + ? `IMPORTANT: ${rules.join(", ")}` + : JSON.stringify(constraints); return original ? `${original}\n\n${text}` : text; } } diff --git a/packages/lib/eliza/plugin-mcp/types.ts b/packages/lib/eliza/plugin-mcp/types.ts index 3ccf0baac..ae96fd130 100644 --- a/packages/lib/eliza/plugin-mcp/types.ts +++ b/packages/lib/eliza/plugin-mcp/types.ts @@ -1,7 +1,11 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import type { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import type { Resource, ResourceTemplate, Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { + Resource, + ResourceTemplate, + Tool, +} from "@modelcontextprotocol/sdk/types.js"; // ─── Constants ─────────────────────────────────────────────────────────────── @@ -135,11 +139,16 @@ export interface CachedServerSchema { // ─── Validation ────────────────────────────────────────────────────────────── -export type ValidationResult = { success: true; data: T } | { success: false; error: string }; +export type ValidationResult = + | { success: true; data: T } + | { success: false; error: string }; export const ResourceSelectionSchema = { type: "object", - oneOf: [{ required: ["serverName", "uri"] }, { required: ["noResourceAvailable"] }], + oneOf: [ + { required: ["serverName", "uri"] }, + { required: ["noResourceAvailable"] }, + ], properties: { serverName: { type: "string", minLength: 1 }, uri: { type: "string", minLength: 1 }, diff --git a/packages/lib/eliza/plugin-mcp/utils/action-naming.ts b/packages/lib/eliza/plugin-mcp/utils/action-naming.ts index 483423b37..47c9e6cc5 100644 --- a/packages/lib/eliza/plugin-mcp/utils/action-naming.ts +++ b/packages/lib/eliza/plugin-mcp/utils/action-naming.ts @@ -17,7 +17,10 @@ export function toActionName(serverName: string, toolName: string): string { return `${server}_${tool}`; } -export function generateSimiles(serverName: string, toolName: string): string[] { +export function generateSimiles( + serverName: string, + toolName: string, +): string[] { const tool = normalize(toolName); const fullName = toActionName(serverName, toolName); diff --git a/packages/lib/eliza/plugin-mcp/utils/json.ts b/packages/lib/eliza/plugin-mcp/utils/json.ts index 3a5c8a5db..b4c91f38c 100644 --- a/packages/lib/eliza/plugin-mcp/utils/json.ts +++ b/packages/lib/eliza/plugin-mcp/utils/json.ts @@ -2,7 +2,12 @@ import Ajv from "ajv"; import JSON5 from "json5"; /** Find matching closing bracket/brace using depth counting */ -function findMatchingClose(str: string, start: number, open: string, close: string): number { +function findMatchingClose( + str: string, + start: number, + open: string, + close: string, +): number { let depth = 0; let inString = false; let escape = false; @@ -96,7 +101,9 @@ export function validateJsonSchema( if (!valid) { const errors = (validate.errors || []).map( (err: { instancePath?: string; message?: string }) => { - const path = err.instancePath ? `${err.instancePath.replace(/^\//, "")}` : "value"; + const path = err.instancePath + ? `${err.instancePath.replace(/^\//, "")}` + : "value"; return `${path}: ${err.message}`; }, ); diff --git a/packages/lib/eliza/plugin-mcp/utils/mcp.ts b/packages/lib/eliza/plugin-mcp/utils/mcp.ts index 72f4ab704..a087addd8 100644 --- a/packages/lib/eliza/plugin-mcp/utils/mcp.ts +++ b/packages/lib/eliza/plugin-mcp/utils/mcp.ts @@ -12,7 +12,10 @@ import type { * Returns true if access is allowed, false if denied. * When not set (CLI / non-cloud), returns true (fail-open by design). */ -export function checkMcpOAuthAccess(runtime: IAgentRuntime, serverName?: string): boolean { +export function checkMcpOAuthAccess( + runtime: IAgentRuntime, + serverName?: string, +): boolean { const raw = runtime.getSetting("MCP_ENABLED_SERVERS"); if (typeof raw !== "string") return true; // not set → fail-open @@ -20,12 +23,18 @@ export function checkMcpOAuthAccess(runtime: IAgentRuntime, serverName?: string) try { enabled = JSON.parse(raw); } catch { - logger.warn({ serverName, raw }, "[MCP] Malformed MCP_ENABLED_SERVERS JSON, denying access"); + logger.warn( + { serverName, raw }, + "[MCP] Malformed MCP_ENABLED_SERVERS JSON, denying access", + ); return false; } if (!Array.isArray(enabled)) { - logger.warn({ serverName, raw }, "[MCP] MCP_ENABLED_SERVERS is not an array, denying access"); + logger.warn( + { serverName, raw }, + "[MCP] MCP_ENABLED_SERVERS is not an array, denying access", + ); return false; } @@ -64,12 +73,20 @@ export async function createMcpMemory( metadata: { ...metadata, serverName }, }, }); - await runtime.createMemory(memory, type === "resource" ? "resources" : "tools", true); + await runtime.createMemory( + memory, + type === "resource" ? "resources" : "tools", + true, + ); } export function buildMcpProviderData(servers: McpServer[]): McpProvider { if (servers.length === 0) { - return { values: { mcp: {} }, data: { mcp: {} }, text: "No MCP servers connected." }; + return { + values: { mcp: {} }, + data: { mcp: {} }, + text: "No MCP servers connected.", + }; } const mcpData: McpProviderData = {}; @@ -84,7 +101,10 @@ export function buildMcpProviderData(servers: McpServer[]): McpProvider { if (server.tools?.length) { lines.push("### Tools\n"); for (const t of server.tools) { - tools[t.name] = { description: t.description || NO_DESC, inputSchema: t.inputSchema || {} }; + tools[t.name] = { + description: t.description || NO_DESC, + inputSchema: t.inputSchema || {}, + }; lines.push(`- **${t.name}**: ${t.description || NO_DESC}`); } lines.push(""); @@ -107,5 +127,9 @@ export function buildMcpProviderData(servers: McpServer[]): McpProvider { } const text = lines.join("\n"); - return { values: { mcp: mcpData, mcpText: text }, data: { mcp: mcpData }, text }; + return { + values: { mcp: mcpData, mcpText: text }, + data: { mcp: mcpData }, + text, + }; } diff --git a/packages/lib/eliza/plugin-mcp/utils/processing.ts b/packages/lib/eliza/plugin-mcp/utils/processing.ts index dac91a2e9..31260a4f1 100644 --- a/packages/lib/eliza/plugin-mcp/utils/processing.ts +++ b/packages/lib/eliza/plugin-mcp/utils/processing.ts @@ -16,13 +16,21 @@ function getMimeTypeToContentType(mimeType?: string): ContentType | undefined { if (mimeType.startsWith("image/")) return ContentType.IMAGE; if (mimeType.startsWith("video/")) return ContentType.VIDEO; if (mimeType.startsWith("audio/")) return ContentType.AUDIO; - if (mimeType.includes("pdf") || mimeType.includes("document")) return ContentType.DOCUMENT; + if (mimeType.includes("pdf") || mimeType.includes("document")) + return ContentType.DOCUMENT; return undefined; } /** Process resource result from MCP */ export function processResourceResult( - result: { contents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }> }, + result: { + contents: Array<{ + uri: string; + mimeType?: string; + text?: string; + blob?: string; + }>; + }, uri: string, ): { resourceContent: string; resourceMeta: string } { let resourceContent = ""; @@ -30,7 +38,8 @@ export function processResourceResult( for (const content of result.contents) { resourceContent += - content.text || (content.blob ? `[Binary: ${content.mimeType || "unknown"}]` : ""); + content.text || + (content.blob ? `[Binary: ${content.mimeType || "unknown"}]` : ""); resourceMeta += `Resource: ${content.uri || uri}\n`; if (content.mimeType) resourceMeta += `Type: ${content.mimeType}\n`; } @@ -68,7 +77,10 @@ export function processToolResult( attachments.push({ contentType: getMimeTypeToContentType(content.mimeType), url: `data:${content.mimeType};base64,${content.data}`, - id: createUniqueUuid(runtime, `${messageEntityId}-attachment-${attachmentIndex++}`), + id: createUniqueUuid( + runtime, + `${messageEntityId}-attachment-${attachmentIndex++}`, + ), title: "Generated image", source: `${serverName}/${toolName}`, description: "Tool-generated image", @@ -95,16 +107,28 @@ export async function handleResourceAnalysis( resourceMeta: string, callback?: HandlerCallback, ): Promise { - await createMcpMemory(runtime, message, "resource", serverName, resourceContent, { - uri, - isResourceAccess: true, - }); + await createMcpMemory( + runtime, + message, + "resource", + serverName, + resourceContent, + { + uri, + isResourceAccess: true, + }, + ); const prompt = composePromptFromState({ state: { data: {}, text: "", - values: { uri, userMessage: message.content.text || "", resourceContent, resourceMeta }, + values: { + uri, + userMessage: message.content.text || "", + resourceContent, + resourceMeta, + }, }, template: resourceAnalysisTemplate, }); @@ -121,7 +145,9 @@ export async function handleResourceAnalysis( } /** Send initial response for readResourceAction */ -export async function sendInitialResponse(callback?: HandlerCallback): Promise { +export async function sendInitialResponse( + callback?: HandlerCallback, +): Promise { if (callback) { await callback({ thought: "Retrieving MCP resource...", diff --git a/packages/lib/eliza/plugin-mcp/utils/schema-converter.ts b/packages/lib/eliza/plugin-mcp/utils/schema-converter.ts index 6112bd01a..92c0b6ec5 100644 --- a/packages/lib/eliza/plugin-mcp/utils/schema-converter.ts +++ b/packages/lib/eliza/plugin-mcp/utils/schema-converter.ts @@ -47,7 +47,9 @@ function buildDescription(name: string, prop: JsonSchemaProperty): string { } if (prop.enum?.length) - parts.push(`Allowed: ${prop.enum.map((v) => JSON.stringify(v)).join(", ")}`); + parts.push( + `Allowed: ${prop.enum.map((v) => JSON.stringify(v)).join(", ")}`, + ); if (prop.format) parts.push(`Format: ${prop.format}`); if (prop.minimum !== undefined || prop.maximum !== undefined) { parts.push( @@ -60,21 +62,27 @@ function buildDescription(name: string, prop: JsonSchemaProperty): string { ); } if (prop.pattern) parts.push(`Pattern: ${prop.pattern}`); - if (prop.default !== undefined) parts.push(`Default: ${JSON.stringify(prop.default)}`); - if (prop.type === "array" && prop.items) parts.push(`Array of ${prop.items.type || "any"}`); + if (prop.default !== undefined) + parts.push(`Default: ${JSON.stringify(prop.default)}`); + if (prop.type === "array" && prop.items) + parts.push(`Array of ${prop.items.type || "any"}`); if (prop.type === "object" && prop.properties) { const keys = Object.keys(prop.properties); parts.push(`Object with keys: ${keys.join(", ")}`); } // If no description and no constraints, provide a minimal type-based hint - return parts.length > 0 ? parts.join(". ") : `(${mapJsonSchemaType(prop.type)})`; + return parts.length > 0 + ? parts.join(". ") + : `(${mapJsonSchemaType(prop.type)})`; } export function convertJsonSchemaToActionParams( schema?: Tool["inputSchema"], ): ActionParameter[] | undefined { - const properties = schema?.properties as Record | undefined; + const properties = schema?.properties as + | Record + | undefined; if (!properties || Object.keys(properties).length === 0) return undefined; const required = new Set((schema?.required as string[]) || []); @@ -104,7 +112,9 @@ export function validateParamsAgainstSchema( if (!schema) return []; const errors: string[] = []; - const properties = schema.properties as Record | undefined; + const properties = schema.properties as + | Record + | undefined; const required = new Set((schema.required as string[]) || []); for (const field of required) { diff --git a/packages/lib/eliza/plugin-mcp/utils/validation.ts b/packages/lib/eliza/plugin-mcp/utils/validation.ts index c2a11e41a..787556986 100644 --- a/packages/lib/eliza/plugin-mcp/utils/validation.ts +++ b/packages/lib/eliza/plugin-mcp/utils/validation.ts @@ -1,5 +1,9 @@ import type { State } from "@elizaos/core"; -import { type McpProviderData, ResourceSelectionSchema, type ValidationResult } from "../types"; +import { + type McpProviderData, + ResourceSelectionSchema, + type ValidationResult, +} from "../types"; import { validateJsonSchema } from "./json"; export interface ResourceSelection { @@ -9,8 +13,13 @@ export interface ResourceSelection { noResourceAvailable?: boolean; } -export function validateResourceSelection(selection: unknown): ValidationResult { - return validateJsonSchema(selection, ResourceSelectionSchema); +export function validateResourceSelection( + selection: unknown, +): ValidationResult { + return validateJsonSchema( + selection, + ResourceSelectionSchema, + ); } export function createResourceSelectionFeedbackPrompt( @@ -21,10 +30,9 @@ export function createResourceSelectionFeedbackPrompt( ): string { let description = ""; - for (const [serverName, server] of Object.entries(composedState.values.mcp || {}) as [ - string, - McpProviderData[string], - ][]) { + for (const [serverName, server] of Object.entries( + composedState.values.mcp || {}, + ) as [string, McpProviderData[string]][]) { if (server.status !== "connected") continue; for (const [uri, resource] of Object.entries(server.resources || {}) as [ diff --git a/packages/lib/eliza/plugin-mcp/utils/wrapper.ts b/packages/lib/eliza/plugin-mcp/utils/wrapper.ts index ce26923a4..071191c03 100644 --- a/packages/lib/eliza/plugin-mcp/utils/wrapper.ts +++ b/packages/lib/eliza/plugin-mcp/utils/wrapper.ts @@ -43,7 +43,8 @@ export async function withModelRetry({ const maxRetries = getMaxRetries(runtime); try { - const parsed = typeof input === "string" ? parseJSON(input) : input; + const parsed = + typeof input === "string" ? parseJSON(input) : input; const result = validationFn(parsed); if (!result.success) throw new Error(result.error); return result.data as T; @@ -52,8 +53,15 @@ export async function withModelRetry({ logger.error({ error }, "[Retry] Parse failed"); if (retryCount < maxRetries) { - const feedback = createFeedbackPromptFn(input, error, state, message.content.text || ""); - const retry = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt: feedback }); + const feedback = createFeedbackPromptFn( + input, + error, + state, + message.content.text || "", + ); + const retry = await runtime.useModel(ModelType.OBJECT_LARGE, { + prompt: feedback, + }); return withModelRetry({ runtime, input: retry, @@ -86,7 +94,12 @@ function getMaxRetries(runtime: IAgentRuntime): number { | boolean | number | null; - if (mcp && typeof mcp === "object" && "maxRetries" in mcp && mcp.maxRetries !== undefined) { + if ( + mcp && + typeof mcp === "object" && + "maxRetries" in mcp && + mcp.maxRetries !== undefined + ) { const val = Number(mcp.maxRetries); if (!isNaN(val) && val >= 0) return val; } diff --git a/packages/lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts b/packages/lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts index 2f4c57674..ad16c3077 100644 --- a/packages/lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts +++ b/packages/lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts @@ -27,7 +27,11 @@ import { isUserLookupError, lookupUser } from "@/lib/eliza/plugin-oauth/utils"; import { apiKeysService } from "@/lib/services/api-keys"; import { elizaAppConfig } from "@/lib/services/eliza-app/config"; import { oauthService } from "@/lib/services/oauth"; -import { getClientId, getClientSecret, getProvider } from "@/lib/services/oauth/provider-registry"; +import { + getClientId, + getClientSecret, + getProvider, +} from "@/lib/services/oauth/provider-registry"; import { secretsService } from "@/lib/services/secrets"; import { telegramAutomationService } from "@/lib/services/telegram-automation"; import { logger } from "@/lib/utils/logger"; @@ -67,13 +71,19 @@ export class N8nCredentialBridge extends Service { * Called by plugin-n8n-workflow's credentialResolver during workflow deployment. * Returns credential data — the plugin creates the n8n credential itself. */ - async resolve(userId: string, credType: string): Promise { + async resolve( + userId: string, + credType: string, + ): Promise { const platform = mapCredTypeToCloudPlatform(credType); - if (platform) return this.resolveOAuthCredential(credType, platform, userId); + if (platform) + return this.resolveOAuthCredential(credType, platform, userId); - if (credType in API_KEY_CRED_TYPES) return this.resolveApiKeyCredential(credType, userId); + if (credType in API_KEY_CRED_TYPES) + return this.resolveApiKeyCredential(credType, userId); - if (TELEGRAM_CRED_TYPES.has(credType)) return this.resolveTelegramCredential(userId); + if (TELEGRAM_CRED_TYPES.has(credType)) + return this.resolveTelegramCredential(userId); return null; } @@ -134,10 +144,14 @@ export class N8nCredentialBridge extends Service { } const { user, organizationId } = userResult; - const connected = await oauthService.isPlatformConnected(organizationId, platform); + const connected = await oauthService.isPlatformConnected( + organizationId, + platform, + ); if (!connected) { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const result = await oauthService.initiateAuth({ organizationId, userId: user.id, @@ -150,17 +164,22 @@ export class N8nCredentialBridge extends Service { let token: { accessToken: string; expiresAt?: Date }; let connectionId: string; try { - ({ token, connectionId } = await oauthService.getValidTokenByPlatformWithConnectionId({ - organizationId, - platform, - })); + ({ token, connectionId } = + await oauthService.getValidTokenByPlatformWithConnectionId({ + organizationId, + platform, + })); } catch (error) { // Connection revoked between isPlatformConnected check and token retrieval - logger.warn("[N8nCredentialBridge] Token retrieval failed after connection check", { - platform, - error: error instanceof Error ? error.message : String(error), - }); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + logger.warn( + "[N8nCredentialBridge] Token retrieval failed after connection check", + { + platform, + error: error instanceof Error ? error.message : String(error), + }, + ); + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const result = await oauthService.initiateAuth({ organizationId, userId: user.id, @@ -253,7 +272,8 @@ export class N8nCredentialBridge extends Service { const apiKey = await this.getUserApiKey(user.id, organizationId); if (!apiKey) return null; - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; logger.info("[N8nCredentialBridge] API key credential resolved", { credType, @@ -272,18 +292,27 @@ export class N8nCredentialBridge extends Service { * Uses the Eliza App bot token (env var) or the org's stored bot token. * n8n's telegramApi credential expects { accessToken: botToken }. */ - private async resolveTelegramCredential(userId: string): Promise { + private async resolveTelegramCredential( + userId: string, + ): Promise { const userResult = await lookupUser(userId, "N8N_CREDENTIAL_BRIDGE"); if (isUserLookupError(userResult)) { - logger.warn("[N8nCredentialBridge] User lookup failed, cannot resolve Telegram credential", { - userId, - }); + logger.warn( + "[N8nCredentialBridge] User lookup failed, cannot resolve Telegram credential", + { + userId, + }, + ); return null; } - const orgBotToken = await telegramAutomationService.getBotToken(userResult.organizationId); + const orgBotToken = await telegramAutomationService.getBotToken( + userResult.organizationId, + ); if (orgBotToken) { - logger.info("[N8nCredentialBridge] Telegram credential resolved from org bot token"); + logger.info( + "[N8nCredentialBridge] Telegram credential resolved from org bot token", + ); return { status: "credential_data", data: { accessToken: orgBotToken }, @@ -301,7 +330,9 @@ export class N8nCredentialBridge extends Service { }; } - logger.warn("[N8nCredentialBridge] No Telegram bot token available", { userId }); + logger.warn("[N8nCredentialBridge] No Telegram bot token available", { + userId, + }); return null; } @@ -309,11 +340,17 @@ export class N8nCredentialBridge extends Service { * Get the user's active, non-expired cloud API key. * Every user has one (auto-created on signup via ensureUserHasApiKey). */ - private async getUserApiKey(userId: string, organizationId: string): Promise { + private async getUserApiKey( + userId: string, + organizationId: string, + ): Promise { const keys = await apiKeysService.listByOrganization(organizationId); const now = new Date(); const userKey = keys.find( - (k) => k.user_id === userId && k.is_active && (!k.expires_at || k.expires_at > now), + (k) => + k.user_id === userId && + k.is_active && + (!k.expires_at || k.expires_at > now), ); if (!userKey) { diff --git a/packages/lib/eliza/plugin-n8n-bridge/oauth-cred-map.ts b/packages/lib/eliza/plugin-n8n-bridge/oauth-cred-map.ts index 128f7f7c2..71f31a00d 100644 --- a/packages/lib/eliza/plugin-n8n-bridge/oauth-cred-map.ts +++ b/packages/lib/eliza/plugin-n8n-bridge/oauth-cred-map.ts @@ -28,7 +28,9 @@ const PLATFORM_PREFIXES: Record = { // Pre-computed reverse: sorted longest-prefix-first for correct matching const PREFIX_TO_PLATFORM: [string, string][] = Object.entries(PLATFORM_PREFIXES) - .flatMap(([platform, prefixes]) => prefixes.map((p): [string, string] => [p, platform])) + .flatMap(([platform, prefixes]) => + prefixes.map((p): [string, string] => [p, platform]), + ) .sort((a, b) => b[0].length - a[0].length); /** diff --git a/packages/lib/eliza/plugin-oauth/actions/oauth-connect.ts b/packages/lib/eliza/plugin-oauth/actions/oauth-connect.ts index 8978572b9..e0ed35430 100644 --- a/packages/lib/eliza/plugin-oauth/actions/oauth-connect.ts +++ b/packages/lib/eliza/plugin-oauth/actions/oauth-connect.ts @@ -12,7 +12,10 @@ import { type State, } from "@elizaos/core"; import { oauthService } from "@/lib/services/oauth"; -import { type ActionWithParams, defineActionParameters } from "../../plugin-cloud-bootstrap/types"; +import { + type ActionWithParams, + defineActionParameters, +} from "../../plugin-cloud-bootstrap/types"; import { capitalize, extractPlatform, @@ -78,7 +81,10 @@ export const oauthConnectAction: ActionWithParams = { }, }), - validate: async (_runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + _runtime: IAgentRuntime, + message: Memory, + ): Promise => { return !!message.entityId; }, @@ -92,7 +98,9 @@ export const oauthConnectAction: ActionWithParams = { const platform = extractPlatform(message, state); const actionName = "OAUTH_CONNECT"; - logger.info(`[${actionName}] platform=${platform}, entityId=${message.entityId}`); + logger.info( + `[${actionName}] platform=${platform}, entityId=${message.entityId}`, + ); if (!platform) { const supported = getSupportedPlatforms(); @@ -120,7 +128,9 @@ export const oauthConnectAction: ActionWithParams = { const { organizationId, user } = userResult; const platformName = capitalize(platform); - if (await oauthService.isPlatformConnected(organizationId, platform, user.id)) { + if ( + await oauthService.isPlatformConnected(organizationId, platform, user.id) + ) { const connections = await oauthService.listConnections({ organizationId, userId: user.id, @@ -134,7 +144,8 @@ export const oauthConnectAction: ActionWithParams = { }; } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; let result; try { @@ -171,7 +182,11 @@ export const oauthConnectAction: ActionWithParams = { if (callback) await callback({ text, actions: [actionName] }); - return { text, success: true, data: { actionName, authUrl: result.authUrl } }; + return { + text, + success: true, + data: { actionName, authUrl: result.authUrl }, + }; }, examples: [ diff --git a/packages/lib/eliza/plugin-oauth/actions/oauth-get.ts b/packages/lib/eliza/plugin-oauth/actions/oauth-get.ts index 086c289bc..cccb1ebe5 100644 --- a/packages/lib/eliza/plugin-oauth/actions/oauth-get.ts +++ b/packages/lib/eliza/plugin-oauth/actions/oauth-get.ts @@ -12,7 +12,10 @@ import { type State, } from "@elizaos/core"; import { oauthService } from "@/lib/services/oauth"; -import { type ActionWithParams, defineActionParameters } from "../../plugin-cloud-bootstrap/types"; +import { + type ActionWithParams, + defineActionParameters, +} from "../../plugin-cloud-bootstrap/types"; import { capitalize, extractPlatform, @@ -65,7 +68,10 @@ export const oauthGetAction: ActionWithParams = { }, }), - validate: async (_runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + _runtime: IAgentRuntime, + message: Memory, + ): Promise => { return !!message.entityId; }, @@ -79,7 +85,9 @@ export const oauthGetAction: ActionWithParams = { const platform = extractPlatform(message, state); const actionName = "OAUTH_GET"; - logger.info(`[${actionName}] platform=${platform || "all"}, entityId=${message.entityId}`); + logger.info( + `[${actionName}] platform=${platform || "all"}, entityId=${message.entityId}`, + ); const userResult = await lookupUser(message.entityId as string, actionName); if (isUserLookupError(userResult)) return userResult; @@ -88,7 +96,11 @@ export const oauthGetAction: ActionWithParams = { // Check specific platform if (platform) { - const isConnected = await oauthService.isPlatformConnected(organizationId, platform, user.id); + const isConnected = await oauthService.isPlatformConnected( + organizationId, + platform, + user.id, + ); const platformName = capitalize(platform); if (isConnected) { @@ -113,7 +125,10 @@ export const oauthGetAction: ActionWithParams = { } // Check all connections - const connections = await oauthService.listConnections({ organizationId, userId: user.id }); + const connections = await oauthService.listConnections({ + organizationId, + userId: user.id, + }); const active = connections.filter((c) => c.status === "active"); if (active.length === 0) { @@ -126,7 +141,9 @@ export const oauthGetAction: ActionWithParams = { const list = active .map((c) => { const id = formatConnectionIdentifier(c); - return id ? `${capitalize(c.platform)} (${id})` : capitalize(c.platform); + return id + ? `${capitalize(c.platform)} (${id})` + : capitalize(c.platform); }) .join(", "); diff --git a/packages/lib/eliza/plugin-oauth/actions/oauth-list.ts b/packages/lib/eliza/plugin-oauth/actions/oauth-list.ts index d814e2923..3c2b53c4e 100644 --- a/packages/lib/eliza/plugin-oauth/actions/oauth-list.ts +++ b/packages/lib/eliza/plugin-oauth/actions/oauth-list.ts @@ -12,8 +12,16 @@ import { type State, } from "@elizaos/core"; import { oauthService } from "@/lib/services/oauth"; -import { type ActionWithParams, defineActionParameters } from "../../plugin-cloud-bootstrap/types"; -import { capitalize, formatConnectionIdentifier, isUserLookupError, lookupUser } from "../utils"; +import { + type ActionWithParams, + defineActionParameters, +} from "../../plugin-cloud-bootstrap/types"; +import { + capitalize, + formatConnectionIdentifier, + isUserLookupError, + lookupUser, +} from "../utils"; export const oauthListAction: ActionWithParams = { name: "OAUTH_LIST", @@ -26,11 +34,15 @@ export const oauthListAction: ActionWithParams = { "MY_INTEGRATIONS", "SHOW_INTEGRATIONS", ], - description: "List all OAuth connections for the user. Shows which platforms are connected.", + description: + "List all OAuth connections for the user. Shows which platforms are connected.", parameters: defineActionParameters({}), - validate: async (_runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + _runtime: IAgentRuntime, + message: Memory, + ): Promise => { return !!message.entityId; }, @@ -49,10 +61,14 @@ export const oauthListAction: ActionWithParams = { if (isUserLookupError(userResult)) return userResult; const { organizationId, user } = userResult; - const connections = await oauthService.listConnections({ organizationId, userId: user.id }); + const connections = await oauthService.listConnections({ + organizationId, + userId: user.id, + }); if (connections.length === 0) { - const text = "You don't have any connected accounts. Say 'connect google' to get started."; + const text = + "You don't have any connected accounts. Say 'connect google' to get started."; if (callback) await callback({ text, actions: [actionName] }); return { text, success: true, data: { actionName, count: 0 } }; } @@ -75,7 +91,11 @@ export const oauthListAction: ActionWithParams = { logger.info(`[${actionName}] Found ${connections.length} connections`); if (callback) await callback({ text, actions: [actionName] }); - return { text, success: true, data: { actionName, count: connections.length, activeCount } }; + return { + text, + success: true, + data: { actionName, count: connections.length, activeCount }, + }; }, examples: [ diff --git a/packages/lib/eliza/plugin-oauth/actions/oauth-revoke.ts b/packages/lib/eliza/plugin-oauth/actions/oauth-revoke.ts index c079a869d..89ddc3dc3 100644 --- a/packages/lib/eliza/plugin-oauth/actions/oauth-revoke.ts +++ b/packages/lib/eliza/plugin-oauth/actions/oauth-revoke.ts @@ -13,7 +13,10 @@ import { } from "@elizaos/core"; import { oauthService } from "@/lib/services/oauth"; import { invalidateOAuthState } from "@/lib/services/oauth/invalidation"; -import { type ActionWithParams, defineActionParameters } from "../../plugin-cloud-bootstrap/types"; +import { + type ActionWithParams, + defineActionParameters, +} from "../../plugin-cloud-bootstrap/types"; import { capitalize, extractPlatform, @@ -71,7 +74,10 @@ export const oauthRevokeAction: ActionWithParams = { }, }), - validate: async (_runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + _runtime: IAgentRuntime, + message: Memory, + ): Promise => { return !!message.entityId; }, @@ -85,7 +91,9 @@ export const oauthRevokeAction: ActionWithParams = { const platform = extractPlatform(message, state); const actionName = "OAUTH_REVOKE"; - logger.info(`[${actionName}] platform=${platform}, entityId=${message.entityId}`); + logger.info( + `[${actionName}] platform=${platform}, entityId=${message.entityId}`, + ); if (!platform) { const supported = getSupportedPlatforms(); @@ -165,35 +173,50 @@ export const oauthRevokeAction: ActionWithParams = { { name: "{{name1}}", content: { text: "unlink my gmail" } }, { name: "{{name2}}", - content: { text: "Google has been disconnected.", actions: ["OAUTH_REVOKE"] }, + content: { + text: "Google has been disconnected.", + actions: ["OAUTH_REVOKE"], + }, }, ], [ { name: "{{name1}}", content: { text: "disconnect hubspot" } }, { name: "{{name2}}", - content: { text: "HubSpot has been disconnected.", actions: ["OAUTH_REVOKE"] }, + content: { + text: "HubSpot has been disconnected.", + actions: ["OAUTH_REVOKE"], + }, }, ], [ { name: "{{name1}}", content: { text: "disconnect my twitter" } }, { name: "{{name2}}", - content: { text: "Twitter has been disconnected.", actions: ["OAUTH_REVOKE"] }, + content: { + text: "Twitter has been disconnected.", + actions: ["OAUTH_REVOKE"], + }, }, ], [ { name: "{{name1}}", content: { text: "remove my x account" } }, { name: "{{name2}}", - content: { text: "Twitter has been disconnected.", actions: ["OAUTH_REVOKE"] }, + content: { + text: "Twitter has been disconnected.", + actions: ["OAUTH_REVOKE"], + }, }, ], [ { name: "{{name1}}", content: { text: "remove my slack connection" } }, { name: "{{name2}}", - content: { text: "Slack has been disconnected.", actions: ["OAUTH_REVOKE"] }, + content: { + text: "Slack has been disconnected.", + actions: ["OAUTH_REVOKE"], + }, }, ], ] as ActionExample[][], diff --git a/packages/lib/eliza/plugin-oauth/providers/user-auth-status.ts b/packages/lib/eliza/plugin-oauth/providers/user-auth-status.ts index 5f995ba9e..f33ef89f4 100644 --- a/packages/lib/eliza/plugin-oauth/providers/user-auth-status.ts +++ b/packages/lib/eliza/plugin-oauth/providers/user-auth-status.ts @@ -18,15 +18,23 @@ export const userAuthStatusProvider: Provider = { name: "USER_AUTH_STATUS", description: "Provides user OAuth connection status and credits balance", - get: async (_runtime: IAgentRuntime, message: Memory, _state: State): Promise => { + get: async ( + _runtime: IAgentRuntime, + message: Memory, + _state: State, + ): Promise => { if (!message.entityId) { return { text: "", values: {}, data: {} }; } - const user = await usersRepository.findWithOrganization(message.entityId as string); + const user = await usersRepository.findWithOrganization( + message.entityId as string, + ); if (!user || !user.organization_id) { - logger.debug(`[USER_AUTH_STATUS] No user/org for entityId: ${message.entityId}`); + logger.debug( + `[USER_AUTH_STATUS] No user/org for entityId: ${message.entityId}`, + ); return { text: "# User Status\n- Status: Unknown user", values: { userAuthenticated: false, hasOrganization: false }, @@ -35,7 +43,10 @@ export const userAuthStatusProvider: Provider = { } const { organization_id: organizationId, id: userId } = user; - const connections = await oauthService.listConnections({ organizationId, userId }); + const connections = await oauthService.listConnections({ + organizationId, + userId, + }); const active = connections.filter((c) => c.status === "active"); const creditBalance = user.organization?.credit_balance @@ -48,7 +59,9 @@ export const userAuthStatusProvider: Provider = { ? active .map((c) => { const id = formatConnectionIdentifier(c); - return id ? `${capitalize(c.platform)} (${id})` : capitalize(c.platform); + return id + ? `${capitalize(c.platform)} (${id})` + : capitalize(c.platform); }) .join(", ") : "None"; diff --git a/packages/lib/eliza/plugin-oauth/utils.ts b/packages/lib/eliza/plugin-oauth/utils.ts index 1d89d4cb7..04189b35a 100644 --- a/packages/lib/eliza/plugin-oauth/utils.ts +++ b/packages/lib/eliza/plugin-oauth/utils.ts @@ -4,7 +4,10 @@ import type { ActionResult, Memory, State } from "@elizaos/core"; import { logger } from "@elizaos/core"; -import { type UserWithOrganization, usersRepository } from "@/db/repositories/users"; +import { + type UserWithOrganization, + usersRepository, +} from "@/db/repositories/users"; import { getConfiguredOAuthProviders } from "@/lib/services/oauth/provider-registry"; /** Configured OAuth platform IDs (platforms with valid credentials). */ @@ -21,12 +24,15 @@ export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } -export function extractParams(message: Memory, state?: State): Record { +export function extractParams( + message: Memory, + state?: State, +): Record { const content = message.content as Record; - return (content.actionParams || content.actionInput || state?.data?.actionParams || {}) as Record< - string, - unknown - >; + return (content.actionParams || + content.actionInput || + state?.data?.actionParams || + {}) as Record; } const PLATFORM_ALIASES: Record = { @@ -44,8 +50,13 @@ const PLATFORM_ALIASES: Record = { gh: "github", }; -export function extractPlatform(message: Memory, state?: State): string | undefined { - const raw = (extractParams(message, state).platform as string)?.toLowerCase()?.trim(); +export function extractPlatform( + message: Memory, + state?: State, +): string | undefined { + const raw = (extractParams(message, state).platform as string) + ?.toLowerCase() + ?.trim(); if (!raw) return undefined; return PLATFORM_ALIASES[raw] || raw; } @@ -84,7 +95,9 @@ export async function lookupUser( return { user, organizationId: user.organization_id }; } -export function isUserLookupError(result: UserLookupResult | ActionResult): result is ActionResult { +export function isUserLookupError( + result: UserLookupResult | ActionResult, +): result is ActionResult { return "success" in result && result.success === false; } @@ -93,5 +106,7 @@ export function formatConnectionIdentifier(connection: { displayName?: string; username?: string; }): string { - return connection.email || connection.displayName || connection.username || ""; + return ( + connection.email || connection.displayName || connection.username || "" + ); } diff --git a/packages/lib/eliza/plugin-web-search/src/actions/webSearch.ts b/packages/lib/eliza/plugin-web-search/src/actions/webSearch.ts index 0a7bcbdf9..21b984060 100644 --- a/packages/lib/eliza/plugin-web-search/src/actions/webSearch.ts +++ b/packages/lib/eliza/plugin-web-search/src/actions/webSearch.ts @@ -58,7 +58,10 @@ Respond ONLY with the following XML format: `; } -function MaxTokens(data: string, maxTokens: number = DEFAULT_MAX_WEB_SEARCH_CHARS): string { +function MaxTokens( + data: string, + maxTokens: number = DEFAULT_MAX_WEB_SEARCH_CHARS, +): string { // Character-based truncation to cap response length return data.length > maxTokens ? data.slice(0, maxTokens) : data; } @@ -75,7 +78,11 @@ async function extractSearchParams( _state?: State, ): Promise { // First, try to get params from state (custom bootstrap pattern) - const composedState = await runtime.composeState(message, ["ACTION_STATE"], true); + const composedState = await runtime.composeState( + message, + ["ACTION_STATE"], + true, + ); const stateParamSources = [ { source: "actionParams", value: composedState?.data?.actionParams }, { source: "webSearch", value: composedState?.data?.webSearch }, @@ -83,7 +90,9 @@ async function extractSearchParams( ]; const matchedStateParams = stateParamSources.find( (candidate) => - candidate.value && typeof candidate.value === "object" && !Array.isArray(candidate.value), + candidate.value && + typeof candidate.value === "object" && + !Array.isArray(candidate.value), ); const stateParams = (matchedStateParams?.value || {}) as WebSearchParams; @@ -100,11 +109,18 @@ async function extractSearchParams( } // Otherwise, extract from conversation using LLM - logger.info({ src: "webSearch:extractParams", source: "llm" }, "Extracting params via LLM"); + logger.info( + { src: "webSearch:extractParams", source: "llm" }, + "Extracting params via LLM", + ); try { // Compose state - try to get both conversationLog and recentMessages - const extractionState = await runtime.composeState(message, ["RECENT_MESSAGES"], true); + const extractionState = await runtime.composeState( + message, + ["RECENT_MESSAGES"], + true, + ); // Prefer conversationLog if available, fallback to recentMessages const conversationLog = extractionState?.values?.conversationLog; @@ -120,7 +136,10 @@ async function extractSearchParams( ? "conversationLog" : "recentMessages"; - logger.debug({ src: "webSearch:extractParams", contextSource }, "Using conversation context"); + logger.debug( + { src: "webSearch:extractParams", contextSource }, + "Using conversation context", + ); const template = buildExtractionTemplate(conversationContext); const prompt = composePromptFromState({ @@ -134,7 +153,10 @@ async function extractSearchParams( if (parsed?.query) { const extractedParams: WebSearchParams = { query: String(parsed.query).trim(), - topic: parsed.topic === "finance" ? "finance" : ("general" as "general" | "finance"), + topic: + parsed.topic === "finance" + ? "finance" + : ("general" as "general" | "finance"), }; logger.info( @@ -218,22 +240,29 @@ export const webSearch: Action & Record = { }, time_range: { type: "string", - description: "Time range filter: 'day', 'week', 'month', 'year' (or 'd', 'w', 'm', 'y')", + description: + "Time range filter: 'day', 'week', 'month', 'year' (or 'd', 'w', 'm', 'y')", required: false, }, start_date: { type: "string", - description: "Start date filter in YYYY-MM-DD format (returns results after this date)", + description: + "Start date filter in YYYY-MM-DD format (returns results after this date)", required: false, }, end_date: { type: "string", - description: "End date filter in YYYY-MM-DD format (returns results before this date)", + description: + "End date filter in YYYY-MM-DD format (returns results before this date)", required: false, }, } as any, - validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State) => { + validate: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State, + ) => { try { const service = runtime.getService("TAVILY"); return !!service; @@ -266,7 +295,8 @@ export const webSearch: Action & Record = { const query: string | undefined = params?.query?.trim(); if (!query) { - const errorMsg = "Missing required parameter 'query'. Please specify what to search for."; + const errorMsg = + "Missing required parameter 'query'. Please specify what to search for."; logger.error({ src: "webSearch:handler" }, errorMsg); const emptyResult: ActionResult = { text: errorMsg, @@ -287,14 +317,20 @@ export const webSearch: Action & Record = { const source = params?.source?.trim(); const topic = params?.topic === "finance" ? "finance" : "general"; - const maxResults = params?.max_results ? Math.min(Math.max(1, params.max_results), 20) : 5; - const searchDepth = params?.search_depth === "advanced" ? "advanced" : "basic"; + const maxResults = params?.max_results + ? Math.min(Math.max(1, params.max_results), 20) + : 5; + const searchDepth = + params?.search_depth === "advanced" ? "advanced" : "basic"; // Build enhanced query with source if provided let enhancedQuery = query; if (source) { enhancedQuery = `${query} site:${source}`; - logger.info({ src: "webSearch:handler", source }, "Searching with source filter"); + logger.info( + { src: "webSearch:handler", source }, + "Searching with source filter", + ); } logger.info( @@ -329,7 +365,8 @@ export const webSearch: Action & Record = { if (searchResponse && searchResponse.results.length) { const responseList = searchResponse.answer ? `${searchResponse.answer}${ - Array.isArray(searchResponse.results) && searchResponse.results.length > 0 + Array.isArray(searchResponse.results) && + searchResponse.results.length > 0 ? `\n\nFor more details, you can check out these resources:\n${searchResponse.results .map( (result: SearchResult, index: number) => @@ -395,7 +432,10 @@ export const webSearch: Action & Record = { return noResult; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); - logger.error({ src: "webSearch:handler", error: errMsg }, "Action failed"); + logger.error( + { src: "webSearch:handler", error: errMsg }, + "Action failed", + ); const errorResult: ActionResult = { text: `Web search failed: ${errMsg}`, diff --git a/packages/lib/eliza/plugin-web-search/src/services/tavilyService.ts b/packages/lib/eliza/plugin-web-search/src/services/tavilyService.ts index c83865192..5e5ebee26 100644 --- a/packages/lib/eliza/plugin-web-search/src/services/tavilyService.ts +++ b/packages/lib/eliza/plugin-web-search/src/services/tavilyService.ts @@ -35,7 +35,10 @@ export class TavilyService extends Service implements ITavilyService { // No persistent connections to close for Tavily client } - async search(query: string, options?: SearchOptions): Promise { + async search( + query: string, + options?: SearchOptions, + ): Promise { try { if (!this.tavilyClient) { throw new Error("TavilyService not initialized"); diff --git a/packages/lib/eliza/prompt-presets.ts b/packages/lib/eliza/prompt-presets.ts index f5b3fb2b7..561a9b7d7 100644 --- a/packages/lib/eliza/prompt-presets.ts +++ b/packages/lib/eliza/prompt-presets.ts @@ -209,7 +209,9 @@ export function getPromptPreset(name: PromptPresetName): PromptPreset { * Get preset from environment variable or return null */ export function getPresetFromEnv(): PromptPreset | null { - const presetName = process.env.APP_PROMPT_PRESET as PromptPresetName | undefined; + const presetName = process.env.APP_PROMPT_PRESET as + | PromptPresetName + | undefined; if (!presetName) return null; if (!(presetName in promptPresets)) { @@ -240,16 +242,24 @@ export function mergePromptConfig( ...preset, ...config, // Merge text fields by concatenation if both exist - systemPrefix: [preset?.systemPrefix, config?.systemPrefix].filter(Boolean).join("\n"), - systemSuffix: [preset?.systemSuffix, config?.systemSuffix].filter(Boolean).join("\n"), - responseStyle: [preset?.responseStyle, config?.responseStyle].filter(Boolean).join("\n"), + systemPrefix: [preset?.systemPrefix, config?.systemPrefix] + .filter(Boolean) + .join("\n"), + systemSuffix: [preset?.systemSuffix, config?.systemSuffix] + .filter(Boolean) + .join("\n"), + responseStyle: [preset?.responseStyle, config?.responseStyle] + .filter(Boolean) + .join("\n"), }; } /** * Generate flirtiness style directive based on level */ -export function getFlirtinessDirective(level: PromptConfig["flirtiness"]): string { +export function getFlirtinessDirective( + level: PromptConfig["flirtiness"], +): string { switch (level) { case "high": return ` @@ -302,8 +312,14 @@ This isn't a professional interaction - it's a personal, romantic one. : ""; return { - appSystemPrefix: [config.systemPrefix, romanticDirective].filter(Boolean).join("\n").trim(), - appSystemSuffix: [config.systemSuffix, flirtinessDirective].filter(Boolean).join("\n").trim(), + appSystemPrefix: [config.systemPrefix, romanticDirective] + .filter(Boolean) + .join("\n") + .trim(), + appSystemSuffix: [config.systemSuffix, flirtinessDirective] + .filter(Boolean) + .join("\n") + .trim(), appResponseStyle: (config.responseStyle || "").trim(), }; } diff --git a/packages/lib/eliza/providers/agent-credits-provider.ts b/packages/lib/eliza/providers/agent-credits-provider.ts index d06bf31f9..bebd95fda 100644 --- a/packages/lib/eliza/providers/agent-credits-provider.ts +++ b/packages/lib/eliza/providers/agent-credits-provider.ts @@ -75,7 +75,9 @@ export const agentCreditsProvider: Provider = { if (!budget) { // No dedicated budget - agent uses org credits directly // Try to get org credits from runtime settings - const orgId = runtime.character.settings?.organizationId as string | undefined; + const orgId = runtime.character.settings?.organizationId as + | string + | undefined; let orgBalance = 0; if (orgId) { @@ -98,7 +100,10 @@ export const agentCreditsProvider: Provider = { canAffordVideo: orgBalance >= OPERATION_COSTS.video, canAffordMcp: orgBalance >= OPERATION_COSTS.mcp, statusText: `Using organization credits: $${orgBalance.toFixed(2)} available`, - budgetWarning: orgBalance < 1 ? "⚠️ Low organization credits - consider topping up" : null, + budgetWarning: + orgBalance < 1 + ? "⚠️ Low organization credits - consider topping up" + : null, }; } else { const allocated = Number(budget.allocated_budget); @@ -110,7 +115,9 @@ export const agentCreditsProvider: Provider = { // Determine effective available (minimum of budget and daily remaining) const effectiveAvailable = - dailyRemaining !== null ? Math.min(available, dailyRemaining) : available; + dailyRemaining !== null + ? Math.min(available, dailyRemaining) + : available; creditsState = { hasBudget: true, @@ -122,14 +129,22 @@ export const agentCreditsProvider: Provider = { dailyRemaining, isPaused: budget.is_paused, pauseReason: budget.pause_reason, - canAffordChat: !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.chat, - canAffordImage: !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.image, - canAffordVideo: !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.video, - canAffordMcp: !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.mcp, + canAffordChat: + !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.chat, + canAffordImage: + !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.image, + canAffordVideo: + !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.video, + canAffordMcp: + !budget.is_paused && effectiveAvailable >= OPERATION_COSTS.mcp, statusText: budget.is_paused ? `⚠️ Budget paused: ${budget.pause_reason || "Unknown reason"}` : `Budget: $${available.toFixed(2)} available (${dailyRemaining !== null ? `$${dailyRemaining.toFixed(2)} daily remaining` : "no daily limit"})`, - budgetWarning: getBudgetWarning(available, dailyRemaining, budget.is_paused), + budgetWarning: getBudgetWarning( + available, + dailyRemaining, + budget.is_paused, + ), }; } @@ -208,17 +223,23 @@ export function getCreditsPromptSection(credits: AgentCreditsState): string { if (!credits.canAffordImage) { lines.push(""); - lines.push("⚠️ You cannot afford image generation. If asked, politely decline."); + lines.push( + "⚠️ You cannot afford image generation. If asked, politely decline.", + ); } if (!credits.canAffordVideo) { lines.push(""); - lines.push("⚠️ You cannot afford video generation. If asked, politely decline."); + lines.push( + "⚠️ You cannot afford video generation. If asked, politely decline.", + ); } if (credits.isPaused) { lines.push(""); - lines.push("🛑 Your budget is PAUSED. You can only provide basic text responses."); + lines.push( + "🛑 Your budget is PAUSED. You can only provide basic text responses.", + ); lines.push("Politely inform users that you're temporarily limited."); } diff --git a/packages/lib/eliza/runtime-factory.ts b/packages/lib/eliza/runtime-factory.ts index 58265e146..c55329c81 100644 --- a/packages/lib/eliza/runtime-factory.ts +++ b/packages/lib/eliza/runtime-factory.ts @@ -19,7 +19,11 @@ import * as sqlPluginNode from "@elizaos/plugin-sql/node"; import { DEFAULT_IMAGE_MODEL } from "@/lib/models"; import { logger } from "@/lib/utils/logger"; import { agentLoader } from "./agent-loader"; -import { buildElevenLabsSettings, getDefaultModels, getElizaCloudApiUrl } from "./config"; +import { + buildElevenLabsSettings, + getDefaultModels, + getElizaCloudApiUrl, +} from "./config"; import mcpPlugin from "./plugin-mcp"; import type { UserContext } from "./user-context"; import "@/lib/polyfills/dom-polyfills"; @@ -69,11 +73,16 @@ function defineCompatMethod( addedMethods.push(name); } -function makeCompatUuid(...parts: Array): UUID { +function makeCompatUuid( + ...parts: Array +): UUID { return stringToUuid(parts.filter(Boolean).join(":")) as UUID; } -function matchesDataFilter(value: unknown, filter: Record): boolean { +function matchesDataFilter( + value: unknown, + filter: Record, +): boolean { if (typeof value !== "object" || value === null) { return false; } @@ -86,7 +95,8 @@ function matchesDataFilter(value: unknown, filter: Record): boo Array.isArray(actual) && expected.every((expectedItem) => actual.some( - (actualItem) => stableSerialize(actualItem) === stableSerialize(expectedItem), + (actualItem) => + stableSerialize(actualItem) === stableSerialize(expectedItem), ), ) ); @@ -100,7 +110,9 @@ function matchesDataFilter(value: unknown, filter: Record): boo }); } -function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseAdapter { +function applyLegacyDatabaseAdapterCompat( + adapter: IDatabaseAdapter, +): IDatabaseAdapter { const compat = adapter as CompatDatabaseAdapter; const addedMethods: string[] = []; @@ -111,8 +123,13 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA callback: (tx: IDatabaseAdapter) => Promise, options?: { entityContext?: UUID }, ) => { - if (options?.entityContext && hasAdapterMethod(compat, "withIsolationContext")) { - return compat.withIsolationContext(options.entityContext, async () => callback(compat)); + if ( + options?.entityContext && + hasAdapterMethod(compat, "withIsolationContext") + ) { + return compat.withIsolationContext(options.entityContext, async () => + callback(compat), + ); } return callback(compat); @@ -128,7 +145,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return []; } - const agents = await Promise.all(agentIds.map((agentId) => compat.getAgent(agentId))); + const agents = await Promise.all( + agentIds.map((agentId) => compat.getAgent(agentId)), + ); return agents.filter(Boolean); }, addedMethods, @@ -151,12 +170,16 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA defineCompatMethod( compat, "updateAgents", - async (updates: Array<{ agentId: UUID; agent: Record }>) => { + async ( + updates: Array<{ agentId: UUID; agent: Record }>, + ) => { if (!hasAdapterMethod(compat, "updateAgent")) { return false; } - await Promise.all(updates.map(({ agentId, agent }) => compat.updateAgent(agentId, agent))); + await Promise.all( + updates.map(({ agentId, agent }) => compat.updateAgent(agentId, agent)), + ); return true; }, addedMethods, @@ -205,7 +228,10 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - if (existingById.has(agent.id as string) && hasAdapterMethod(compat, "updateAgent")) { + if ( + existingById.has(agent.id as string) && + hasAdapterMethod(compat, "updateAgent") + ) { await compat.updateAgent(agent.id, agent); return; } @@ -257,7 +283,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - await Promise.all(entityIds.map((entityId) => compat.deleteEntity(entityId))); + await Promise.all( + entityIds.map((entityId) => compat.deleteEntity(entityId)), + ); }, addedMethods, ); @@ -266,10 +294,15 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "upsertEntities", async (entities: Array & { id?: UUID }>) => { - const entityIds = entities.flatMap((entity: any) => (entity.id ? [entity.id] : [])); + const entityIds = entities.flatMap((entity: any) => + entity.id ? [entity.id] : [], + ); const existingById = new Set(); - if (hasAdapterMethod(compat, "getEntitiesByIds") && entityIds.length > 0) { + if ( + hasAdapterMethod(compat, "getEntitiesByIds") && + entityIds.length > 0 + ) { const existingEntities = await compat.getEntitiesByIds(entityIds); for (const existingEntity of existingEntities) { const existingId = (existingEntity as { id?: UUID }).id; @@ -288,7 +321,10 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - if (existingById.has(entity.id as string) && hasAdapterMethod(compat, "updateEntity")) { + if ( + existingById.has(entity.id as string) && + hasAdapterMethod(compat, "updateEntity") + ) { await compat.updateEntity(entity as any); return; } @@ -306,7 +342,12 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "getComponentsByNaturalKeys", async ( - keys: Array<{ entityId: UUID; type: string; worldId?: UUID; sourceEntityId?: UUID }>, + keys: Array<{ + entityId: UUID; + type: string; + worldId?: UUID; + sourceEntityId?: UUID; + }>, ) => { if (!hasAdapterMethod(compat, "getComponent")) { return keys.map(() => null); @@ -314,7 +355,12 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return Promise.all( keys.map((key) => - compat.getComponent(key.entityId, key.type, key.worldId, key.sourceEntityId), + compat.getComponent( + key.entityId, + key.type, + key.worldId, + key.sourceEntityId, + ), ), ); }, @@ -330,7 +376,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA } const nestedComponents = await Promise.all( - entityIds.map((entityId) => compat.getComponents(entityId, worldId, sourceEntityId)), + entityIds.map((entityId) => + compat.getComponents(entityId, worldId, sourceEntityId), + ), ); return nestedComponents.flat(); }, @@ -345,13 +393,22 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return []; } - await Promise.all(components.map((component) => compat.createComponent(component))); - return components.flatMap((component) => (component.id ? [component.id] : [])); + await Promise.all( + components.map((component) => compat.createComponent(component)), + ); + return components.flatMap((component) => + component.id ? [component.id] : [], + ); }, addedMethods, ); - defineCompatMethod(compat, "getComponentsByIds", async () => [], addedMethods); + defineCompatMethod( + compat, + "getComponentsByIds", + async () => [], + addedMethods, + ); defineCompatMethod( compat, @@ -361,7 +418,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - await Promise.all(components.map((component) => compat.updateComponent(component))); + await Promise.all( + components.map((component) => compat.updateComponent(component)), + ); }, addedMethods, ); @@ -374,7 +433,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - await Promise.all(componentIds.map((componentId) => compat.deleteComponent(componentId))); + await Promise.all( + componentIds.map((componentId) => compat.deleteComponent(componentId)), + ); }, addedMethods, ); @@ -383,7 +444,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "upsertComponents", async ( - components: Array & { entityId: UUID; type: string; id?: UUID }>, + components: Array< + Record & { entityId: UUID; type: string; id?: UUID } + >, ) => { await Promise.all( components.map(async (component) => { @@ -425,7 +488,10 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA includeAllComponents?: boolean; }) => { const entityIds = params.entityIds ?? []; - if (entityIds.length === 0 || !hasAdapterMethod(compat, "getEntitiesByIds")) { + if ( + entityIds.length === 0 || + !hasAdapterMethod(compat, "getEntitiesByIds") + ) { return []; } @@ -462,7 +528,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA }); if ( - (params.componentType || params.componentDataFilter || params.worldId !== undefined) && + (params.componentType || + params.componentDataFilter || + params.worldId !== undefined) && matchedComponents.length === 0 ) { return null; @@ -470,7 +538,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return { ...entity, - components: params.includeAllComponents ? allComponents : matchedComponents, + components: params.includeAllComponents + ? allComponents + : matchedComponents, }; }), ); @@ -486,7 +556,12 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "createLogs", async ( - entries: Array<{ body: Record; entityId: UUID; roomId: UUID; type: string }>, + entries: Array<{ + body: Record; + entityId: UUID; + roomId: UUID; + type: string; + }>, ) => { if (!hasAdapterMethod(compat, "log")) { return; @@ -518,7 +593,11 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "createMemories", async ( - entries: Array<{ memory: Record; tableName: string; unique?: boolean }>, + entries: Array<{ + memory: Record; + tableName: string; + unique?: boolean; + }>, ) => { if (!hasAdapterMethod(compat, "createMemory")) { return []; @@ -555,7 +634,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - await Promise.all(memoryIds.map((memoryId) => compat.deleteMemory(memoryId))); + await Promise.all( + memoryIds.map((memoryId) => compat.deleteMemory(memoryId)), + ); }, addedMethods, ); @@ -564,7 +645,10 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "upsertMemories", async ( - entries: Array<{ memory: Record & { id?: UUID }; tableName: string }>, + entries: Array<{ + memory: Record & { id?: UUID }; + tableName: string; + }>, ) => { await Promise.all( entries.map(async ({ memory, tableName }) => { @@ -596,7 +680,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA value: async (roomIdsOrRoomId: UUID[] | UUID, tableName: string) => { if (Array.isArray(roomIdsOrRoomId)) { await Promise.all( - roomIdsOrRoomId.map((roomId: UUID) => deleteAllMemories(roomId as any, tableName)), + roomIdsOrRoomId.map((roomId: UUID) => + deleteAllMemories(roomId as any, tableName), + ), ); return; } @@ -659,7 +745,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return []; } - const worlds = await Promise.all(worldIds.map((worldId) => compat.getWorld(worldId))); + const worlds = await Promise.all( + worldIds.map((worldId) => compat.getWorld(worldId)), + ); return worlds.filter(Boolean); }, addedMethods, @@ -673,7 +761,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return []; } - const ids = await Promise.all(worlds.map((world) => compat.createWorld(world))); + const ids = await Promise.all( + worlds.map((world) => compat.createWorld(world)), + ); return ids.filter(Boolean); }, addedMethods, @@ -731,7 +821,10 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - if (existingIds.has(world.id as string) && hasAdapterMethod(compat, "updateWorld")) { + if ( + existingIds.has(world.id as string) && + hasAdapterMethod(compat, "updateWorld") + ) { await compat.updateWorld(world); return; } @@ -753,7 +846,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - await Promise.all(worldIds.map((worldId) => compat.deleteRoomsByWorldId(worldId))); + await Promise.all( + worldIds.map((worldId) => compat.deleteRoomsByWorldId(worldId)), + ); }, addedMethods, ); @@ -767,7 +862,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA } const rooms = ( - await Promise.all(worldIds.map((worldId) => compat.getRoomsByWorld(worldId))) + await Promise.all( + worldIds.map((worldId) => compat.getRoomsByWorld(worldId)), + ) ).flat(); const slicedRooms = rooms.slice(offset ?? 0); return limit === undefined ? slicedRooms : slicedRooms.slice(0, limit); @@ -828,7 +925,10 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - if (existingIds.has(room.id as string) && hasAdapterMethod(compat, "updateRoom")) { + if ( + existingIds.has(room.id as string) && + hasAdapterMethod(compat, "updateRoom") + ) { await compat.updateRoom(room as any); return; } @@ -885,7 +985,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA } return Promise.all( - pairs.map(({ roomId, entityId }) => compat.isRoomParticipant(roomId, entityId)), + pairs.map(({ roomId, entityId }) => + compat.isRoomParticipant(roomId, entityId), + ), ); }, addedMethods, @@ -913,7 +1015,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA } await Promise.all( - participants.map(({ entityId, roomId }) => compat.removeParticipant(entityId, roomId)), + participants.map(({ entityId, roomId }) => + compat.removeParticipant(entityId, roomId), + ), ); return true; }, @@ -924,7 +1028,11 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA compat, "updateParticipants", async ( - participants: Array<{ entityId: UUID; roomId: UUID; updates: { roomState?: string | null } }>, + participants: Array<{ + entityId: UUID; + roomId: UUID; + updates: { roomState?: string | null }; + }>, ) => { if (!hasAdapterMethod(compat, "setParticipantUserState")) { return; @@ -933,7 +1041,11 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA await Promise.all( participants.map(async ({ entityId, roomId, updates }) => { if (updates.roomState !== undefined) { - await compat.setParticipantUserState(roomId, entityId, updates.roomState); + await compat.setParticipantUserState( + roomId, + entityId, + updates.roomState, + ); } }), ); @@ -950,7 +1062,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA } return Promise.all( - pairs.map(({ roomId, entityId }) => compat.getParticipantUserState(roomId, entityId)), + pairs.map(({ roomId, entityId }) => + compat.getParticipantUserState(roomId, entityId), + ), ); }, addedMethods, @@ -959,7 +1073,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA defineCompatMethod( compat, "updateParticipantUserStates", - async (updates: Array<{ roomId: UUID; entityId: UUID; state: string | null }>) => { + async ( + updates: Array<{ roomId: UUID; entityId: UUID; state: string | null }>, + ) => { if (!hasAdapterMethod(compat, "setParticipantUserState")) { return; } @@ -1003,7 +1119,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA ) => { if (hasAdapterMethod(compat, "createRelationship")) { await Promise.all( - relationships.map((relationship) => compat.createRelationship(relationship)), + relationships.map((relationship) => + compat.createRelationship(relationship), + ), ); } @@ -1014,7 +1132,12 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA addedMethods, ); - defineCompatMethod(compat, "getRelationshipsByIds", async () => [], addedMethods); + defineCompatMethod( + compat, + "getRelationshipsByIds", + async () => [], + addedMethods, + ); defineCompatMethod( compat, @@ -1025,13 +1148,20 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA } await Promise.all( - relationships.map((relationship) => compat.updateRelationship(relationship)), + relationships.map((relationship) => + compat.updateRelationship(relationship), + ), ); }, addedMethods, ); - defineCompatMethod(compat, "deleteRelationships", async () => {}, addedMethods); + defineCompatMethod( + compat, + "deleteRelationships", + async () => {}, + addedMethods, + ); defineCompatMethod( compat, @@ -1057,7 +1187,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return false; } - await Promise.all(entries.map(({ key, value }) => compat.setCache(key, value))); + await Promise.all( + entries.map(({ key, value }) => compat.setCache(key, value)), + ); return true; }, addedMethods, @@ -1085,7 +1217,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return []; } - const ids = await Promise.all(tasks.map((task) => compat.createTask(task))); + const ids = await Promise.all( + tasks.map((task) => compat.createTask(task)), + ); return ids.filter(Boolean); }, addedMethods, @@ -1099,7 +1233,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return []; } - const tasks = await Promise.all(taskIds.map((taskId) => compat.getTask(taskId))); + const tasks = await Promise.all( + taskIds.map((taskId) => compat.getTask(taskId)), + ); return tasks.filter(Boolean); }, addedMethods, @@ -1113,7 +1249,9 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA return; } - await Promise.all(updates.map(({ id, task }) => compat.updateTask(id, task))); + await Promise.all( + updates.map(({ id, task }) => compat.updateTask(id, task)), + ); }, addedMethods, ); @@ -1143,11 +1281,17 @@ function applyLegacyDatabaseAdapterCompat(adapter: IDatabaseAdapter): IDatabaseA function assertPersistentDatabaseRequired( runtime: Pick, ): void { - const raw = runtime.getSetting("ALLOW_NO_DATABASE") ?? process.env.ALLOW_NO_DATABASE; + const raw = + runtime.getSetting("ALLOW_NO_DATABASE") ?? process.env.ALLOW_NO_DATABASE; const normalized = String(raw ?? "") .trim() .toLowerCase(); - if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") { + if ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ) { throw new Error( `Milady cloud requires persistent database storage and does not permit ALLOW_NO_DATABASE (agent ${runtime.agentId}). Remove ALLOW_NO_DATABASE from config/env and keep plugin-sql configured.`, ); @@ -1164,7 +1308,10 @@ function stableSerialize(value: unknown): string { .filter(([, entryValue]) => entryValue !== undefined) .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)); return `{${entries - .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableSerialize(entryValue)}`) + .map( + ([key, entryValue]) => + `${JSON.stringify(key)}:${stableSerialize(entryValue)}`, + ) .join(",")}}`; } @@ -1179,19 +1326,40 @@ export const DEFAULT_AGENT_ID_STRING = "b850bc30-45f8-0041-a00a-83df46d8555d"; const MCP_SERVER_CONFIGS: Record = { google: { url: "/api/mcps/google/streamable-http", type: "streamable-http" }, - hubspot: { url: "/api/mcps/hubspot/streamable-http", type: "streamable-http" }, + hubspot: { + url: "/api/mcps/hubspot/streamable-http", + type: "streamable-http", + }, github: { url: "/api/mcps/github/streamable-http", type: "streamable-http" }, notion: { url: "/api/mcps/notion/streamable-http", type: "streamable-http" }, linear: { url: "/api/mcps/linear/streamable-http", type: "streamable-http" }, asana: { url: "/api/mcps/asana/streamable-http", type: "streamable-http" }, - dropbox: { url: "/api/mcps/dropbox/streamable-http", type: "streamable-http" }, - salesforce: { url: "/api/mcps/salesforce/streamable-http", type: "streamable-http" }, - airtable: { url: "/api/mcps/airtable/streamable-http", type: "streamable-http" }, + dropbox: { + url: "/api/mcps/dropbox/streamable-http", + type: "streamable-http", + }, + salesforce: { + url: "/api/mcps/salesforce/streamable-http", + type: "streamable-http", + }, + airtable: { + url: "/api/mcps/airtable/streamable-http", + type: "streamable-http", + }, zoom: { url: "/api/mcps/zoom/streamable-http", type: "streamable-http" }, jira: { url: "/api/mcps/jira/streamable-http", type: "streamable-http" }, - linkedin: { url: "/api/mcps/linkedin/streamable-http", type: "streamable-http" }, - microsoft: { url: "/api/mcps/microsoft/streamable-http", type: "streamable-http" }, - twitter: { url: "/api/mcps/twitter/streamable-http", type: "streamable-http" }, + linkedin: { + url: "/api/mcps/linkedin/streamable-http", + type: "streamable-http", + }, + microsoft: { + url: "/api/mcps/microsoft/streamable-http", + type: "streamable-http", + }, + twitter: { + url: "/api/mcps/twitter/streamable-http", + type: "streamable-http", + }, }; interface GlobalWithEliza { @@ -1238,7 +1406,9 @@ const safeClose = async ( label: string, id: string, ): Promise => { - await closeable.close().catch((e) => elizaLogger.debug(`[${label}] Close error for ${id}: ${e}`)); + await closeable + .close() + .catch((e) => elizaLogger.debug(`[${label}] Close error for ${id}: ${e}`)); }; /** Stop runtime services without closing the shared database adapter pool. */ @@ -1261,13 +1431,22 @@ class RuntimeCache { private readonly IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes idle timeout private isStale(entry: CachedRuntime, now: number): boolean { - return now - entry.createdAt > this.MAX_AGE_MS || now - entry.lastUsed > this.IDLE_TIMEOUT_MS; + return ( + now - entry.createdAt > this.MAX_AGE_MS || + now - entry.lastUsed > this.IDLE_TIMEOUT_MS + ); } - private async evictEntry(key: string, entry: CachedRuntime, reason: string): Promise { + private async evictEntry( + key: string, + entry: CachedRuntime, + reason: string, + ): Promise { await stopRuntimeServices(entry.runtime, key, "RuntimeCache"); this.cache.delete(key); - elizaLogger.debug(`[RuntimeCache] Evicted ${reason} runtime: ${key} (adapter kept alive)`); + elizaLogger.debug( + `[RuntimeCache] Evicted ${reason} runtime: ${key} (adapter kept alive)`, + ); } async get(agentId: string): Promise { @@ -1303,7 +1482,10 @@ class RuntimeCache { } // Cross-instance MCP version check: evict if OAuth changed on another instance - if (currentMcpVersion !== undefined && entry.mcpVersion < currentMcpVersion) { + if ( + currentMcpVersion !== undefined && + entry.mcpVersion < currentMcpVersion + ) { elizaLogger.info( `[RuntimeCache] MCP version stale: cached=${entry.mcpVersion}, current=${currentMcpVersion}, key=${agentId}`, ); @@ -1356,7 +1538,9 @@ class RuntimeCache { await stopRuntimeServices(entry.runtime, agentId, "RuntimeCache"); this.cache.delete(agentId); - elizaLogger.info(`[RuntimeCache] Removed runtime: ${agentId} (adapter kept alive)`); + elizaLogger.info( + `[RuntimeCache] Removed runtime: ${agentId} (adapter kept alive)`, + ); return true; } @@ -1375,7 +1559,9 @@ class RuntimeCache { if (entry) { await safeClose(entry.runtime, "RuntimeCache", agentId); this.cache.delete(agentId); - elizaLogger.info(`[RuntimeCache] Deleted runtime: ${agentId} (fully closed)`); + elizaLogger.info( + `[RuntimeCache] Deleted runtime: ${agentId} (fully closed)`, + ); return true; } return false; @@ -1417,7 +1603,8 @@ class RuntimeCache { /** Remove all runtimes for an organization. */ async removeByOrganization(organizationId: string): Promise { - const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!organizationId || !UUID_RE.test(organizationId)) { return 0; } @@ -1452,7 +1639,10 @@ class DbAdapterPool { private adapters = new Map(); private initPromises = new Map>(); - async getOrCreate(agentId: UUID, embeddingModel?: string): Promise { + async getOrCreate( + agentId: UUID, + embeddingModel?: string, + ): Promise { const key = agentId as string; if (this.adapters.has(key)) { @@ -1487,9 +1677,13 @@ class DbAdapterPool { } } - private async checkAdapterHealth(adapter: IDatabaseAdapter): Promise { + private async checkAdapterHealth( + adapter: IDatabaseAdapter, + ): Promise { try { - await adapter.getEntitiesByIds(["00000000-0000-0000-0000-000000000000" as UUID]); + await adapter.getEntitiesByIds([ + "00000000-0000-0000-0000-000000000000" as UUID, + ]); return true; } catch (error) { // Any error during health check indicates an unhealthy adapter. @@ -1511,7 +1705,10 @@ class DbAdapterPool { return isHealthy; } - private async createAdapter(agentId: UUID, embeddingModel?: string): Promise { + private async createAdapter( + agentId: UUID, + embeddingModel?: string, + ): Promise { if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL environment variable is required"); } @@ -1530,7 +1727,9 @@ class DbAdapterPool { try { await adapter.ensureEmbeddingDimension(dimension); adapterEmbeddingDimensions.set(key, dimension); - elizaLogger.info(`[DbAdapterPool] Set embedding dimension for ${agentId}: ${dimension}`); + elizaLogger.info( + `[DbAdapterPool] Set embedding dimension for ${agentId}: ${dimension}`, + ); } catch (e) { elizaLogger.debug(`[DbAdapterPool] Embedding dimension: ${e}`); adapterEmbeddingDimensions.set(key, dimension); @@ -1568,7 +1767,9 @@ const dbAdapterPool = new DbAdapterPool(); export class RuntimeFactory { private static instance: RuntimeFactory; - private readonly DEFAULT_AGENT_ID = stringToUuid(DEFAULT_AGENT_ID_STRING) as UUID; + private readonly DEFAULT_AGENT_ID = stringToUuid( + DEFAULT_AGENT_ID_STRING, + ) as UUID; private constructor() { this.initializeLoggers(); @@ -1642,7 +1843,11 @@ export class RuntimeFactory { const { character, plugins, modeResolution } = isDefaultCharacter ? await agentLoader.getDefaultCharacter(context.agentMode, loaderOptions) - : await agentLoader.loadCharacter(context.characterId!, context.agentMode, loaderOptions); + : await agentLoader.loadCharacter( + context.characterId!, + context.agentMode, + loaderOptions, + ); if (modeResolution.upgradeReason !== "none") { elizaLogger.info( @@ -1650,16 +1855,24 @@ export class RuntimeFactory { ); } - const agentId = (character.id ? stringToUuid(character.id) : this.DEFAULT_AGENT_ID) as UUID; + const agentId = ( + character.id ? stringToUuid(character.id) : this.DEFAULT_AGENT_ID + ) as UUID; const webSearchSuffix = context.webSearchEnabled ? ":ws" : ""; // Include MCP-relevant OAuth platforms so runtime is recreated when user connects // e.g. HubSpot; otherwise a cached runtime created with only Google never gets HubSpot tools const connectedMcp = this.getConnectedPlatforms(context); - const mcpPlatforms = Object.keys(MCP_SERVER_CONFIGS).filter((p) => connectedMcp.has(p)); - const mcpSuffix = mcpPlatforms.length > 0 ? `:mcp=${mcpPlatforms.sort().join(",")}` : ""; - const directContextSignature = this.buildDirectAccessContextSignature(context); - const contextSuffix = directContextSignature ? `:ctx=${directContextSignature}` : ""; + const mcpPlatforms = Object.keys(MCP_SERVER_CONFIGS).filter((p) => + connectedMcp.has(p), + ); + const mcpSuffix = + mcpPlatforms.length > 0 ? `:mcp=${mcpPlatforms.sort().join(",")}` : ""; + const directContextSignature = + this.buildDirectAccessContextSignature(context); + const contextSuffix = directContextSignature + ? `:ctx=${directContextSignature}` + : ""; // Include organizationId to prevent cross-org API key pollution const cacheKey = `${agentId}:${context.organizationId}${webSearchSuffix}${mcpSuffix}${contextSuffix}`; @@ -1697,13 +1910,21 @@ export class RuntimeFactory { // Build MCP settings from user's OAuth connections // Pass character.settings to preserve any pre-configured MCP servers - const mcpSettings = this.buildMcpSettings(character.settings || {}, context); + const mcpSettings = this.buildMcpSettings( + character.settings || {}, + context, + ); // Add MCP plugin if user has OAuth connections for any MCP server // This is necessary because plugin loading happens before MCP settings injection - if (this.shouldEnableMcp(context) && !filteredPlugins.some((p) => p.name === "mcp")) { + if ( + this.shouldEnableMcp(context) && + !filteredPlugins.some((p) => p.name === "mcp") + ) { filteredPlugins.push(mcpPlugin as Plugin); - elizaLogger.info("[RuntimeFactory] Added MCP plugin for OAuth-connected user"); + elizaLogger.info( + "[RuntimeFactory] Added MCP plugin for OAuth-connected user", + ); } // MCP settings go into character.settings so plugin-mcp can find them @@ -1715,7 +1936,10 @@ export class RuntimeFactory { // User-specific settings that should NOT be persisted to the database // These are passed via opts.settings so they're ephemeral per-request - const ephemeralSettings: Record> = { + const ephemeralSettings: Record< + string, + string | boolean | number | Record + > = { // API keys - must be per-user, not persisted ELIZAOS_API_KEY: context.apiKey, ELIZAOS_CLOUD_API_KEY: context.apiKey, @@ -1729,18 +1953,19 @@ export class RuntimeFactory { // Create runtime with user-specific settings in opts.settings (NOT character.settings) // runtime.getSetting() checks opts.settings as fallback, and these won't be persisted to DB // Note: Nested objects (like MCP settings) are JSON.stringified to preserve them - const runtimeSettings: Record = Object.fromEntries( - Object.entries(ephemeralSettings).map(([key, value]) => [ - key, - typeof value === "string" - ? value - : value === null || value === undefined - ? undefined - : typeof value === "object" - ? JSON.stringify(value) - : String(value), - ]), - ); + const runtimeSettings: Record = + Object.fromEntries( + Object.entries(ephemeralSettings).map(([key, value]) => [ + key, + typeof value === "string" + ? value + : value === null || value === undefined + ? undefined + : typeof value === "object" + ? JSON.stringify(value) + : String(value), + ]), + ); const runtime = new AgentRuntime({ character: { ...character, @@ -1760,7 +1985,13 @@ export class RuntimeFactory { this.setMcpEnabledServers(context); - await runtimeCache.set(cacheKey, runtime, character.name ?? "", agentId, currentMcpVersion); + await runtimeCache.set( + cacheKey, + runtime, + character.name ?? "", + agentId, + currentMcpVersion, + ); edgeRuntimeCache .markRuntimeWarm(agentId as string, { @@ -1797,15 +2028,20 @@ export class RuntimeFactory { // cache hit here only re-applies the same effective values. if (context.modelPreferences) { charSettings.ELIZAOS_CLOUD_NANO_MODEL = - context.modelPreferences.nanoModel || charSettings.ELIZAOS_CLOUD_NANO_MODEL; + context.modelPreferences.nanoModel || + charSettings.ELIZAOS_CLOUD_NANO_MODEL; charSettings.ELIZAOS_CLOUD_SMALL_MODEL = - context.modelPreferences.smallModel || charSettings.ELIZAOS_CLOUD_SMALL_MODEL; + context.modelPreferences.smallModel || + charSettings.ELIZAOS_CLOUD_SMALL_MODEL; charSettings.ELIZAOS_CLOUD_MEDIUM_MODEL = - context.modelPreferences.mediumModel || charSettings.ELIZAOS_CLOUD_MEDIUM_MODEL; + context.modelPreferences.mediumModel || + charSettings.ELIZAOS_CLOUD_MEDIUM_MODEL; charSettings.ELIZAOS_CLOUD_LARGE_MODEL = - context.modelPreferences.largeModel || charSettings.ELIZAOS_CLOUD_LARGE_MODEL; + context.modelPreferences.largeModel || + charSettings.ELIZAOS_CLOUD_LARGE_MODEL; charSettings.ELIZAOS_CLOUD_MEGA_MODEL = - context.modelPreferences.megaModel || charSettings.ELIZAOS_CLOUD_MEGA_MODEL; + context.modelPreferences.megaModel || + charSettings.ELIZAOS_CLOUD_MEGA_MODEL; charSettings.ELIZAOS_CLOUD_RESPONSE_HANDLER_MODEL = context.modelPreferences.responseHandlerModel || context.modelPreferences.shouldRespondModel || @@ -1823,7 +2059,8 @@ export class RuntimeFactory { context.modelPreferences.actionPlannerModel || charSettings.ELIZAOS_CLOUD_PLANNER_MODEL; charSettings.ELIZAOS_CLOUD_RESPONSE_MODEL = - context.modelPreferences.responseModel || charSettings.ELIZAOS_CLOUD_RESPONSE_MODEL; + context.modelPreferences.responseModel || + charSettings.ELIZAOS_CLOUD_RESPONSE_MODEL; charSettings.ELIZAOS_CLOUD_MEDIA_DESCRIPTION_MODEL = context.modelPreferences.mediaDescriptionModel || charSettings.ELIZAOS_CLOUD_MEDIA_DESCRIPTION_MODEL; @@ -1866,7 +2103,9 @@ export class RuntimeFactory { * by McpService.createHttpTransport() via getSetting("ELIZAOS_API_KEY"), * which reads from request context for per-user isolation. */ - private transformMcpSettings(mcpSettings: Record): Record { + private transformMcpSettings( + mcpSettings: Record, + ): Record { if (!mcpSettings?.servers) return mcpSettings; const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; @@ -1893,7 +2132,9 @@ export class RuntimeFactory { } private getConnectedPlatforms(context: UserContext): Set { - return new Set((context.oauthConnections || []).map((c) => c.platform.toLowerCase())); + return new Set( + (context.oauthConnections || []).map((c) => c.platform.toLowerCase()), + ); } private shouldEnableMcp(context: UserContext): boolean { @@ -1909,8 +2150,13 @@ export class RuntimeFactory { const requestCtx = (elizaCore as any).getRequestContext?.(); if (!requestCtx) return; const connected = this.getConnectedPlatforms(context); - const enabledServers = Object.keys(MCP_SERVER_CONFIGS).filter((p) => connected.has(p)); - requestCtx.entitySettings.set("MCP_ENABLED_SERVERS", JSON.stringify(enabledServers)); + const enabledServers = Object.keys(MCP_SERVER_CONFIGS).filter((p) => + connected.has(p), + ); + requestCtx.entitySettings.set( + "MCP_ENABLED_SERVERS", + JSON.stringify(enabledServers), + ); } private buildMcpSettings( @@ -1924,7 +2170,9 @@ export class RuntimeFactory { if (Object.keys(enabledServers).length === 0) return {}; - elizaLogger.debug(`[RuntimeFactory] MCP enabled: ${Object.keys(enabledServers).join(", ")}`); + elizaLogger.debug( + `[RuntimeFactory] MCP enabled: ${Object.keys(enabledServers).join(", ")}`, + ); // Only use servers from MCP_SERVER_CONFIGS — don't merge with DB-stored // server configs which can contain stale full URLs from previous ngrok sessions @@ -1952,7 +2200,10 @@ export class RuntimeFactory { return ""; } - return createHash("sha1").update(stableSerialize(signatureSource)).digest("hex").slice(0, 16); + return createHash("sha1") + .update(stableSerialize(signatureSource)) + .digest("hex") + .slice(0, 16); } private buildSettings( @@ -2045,7 +2296,10 @@ export class RuntimeFactory { "ELIZAOS_CLOUD_PLANNER_MODEL", context.modelPreferences?.mediumModel || context.modelPreferences?.smallModel || - getSetting("ELIZAOS_CLOUD_MEDIUM_MODEL", getDefaultModels().small), + getSetting( + "ELIZAOS_CLOUD_MEDIUM_MODEL", + getDefaultModels().small, + ), ), ), ELIZAOS_CLOUD_PLANNER_MODEL: @@ -2069,14 +2323,22 @@ export class RuntimeFactory { ), ELIZAOS_CLOUD_MEDIA_DESCRIPTION_MODEL: context.modelPreferences?.mediaDescriptionModel || - getSetting("ELIZAOS_CLOUD_MEDIA_DESCRIPTION_MODEL", "google/gemini-2.5-flash-lite"), + getSetting( + "ELIZAOS_CLOUD_MEDIA_DESCRIPTION_MODEL", + "google/gemini-2.5-flash-lite", + ), ELIZAOS_CLOUD_IMAGE_GENERATION_MODEL: context.imageModel || - getSetting("ELIZAOS_CLOUD_IMAGE_GENERATION_MODEL", DEFAULT_IMAGE_MODEL.modelId), + getSetting( + "ELIZAOS_CLOUD_IMAGE_GENERATION_MODEL", + DEFAULT_IMAGE_MODEL.modelId, + ), ...buildElevenLabsSettings(charSettings), // NOTE: User-specific API keys and context are passed via opts.settings // MCP is stripped here and re-injected via settingsWithMcp in createRuntimeForUser - ...(context.appPromptConfig ? { appPromptConfig: context.appPromptConfig } : {}), + ...(context.appPromptConfig + ? { appPromptConfig: context.appPromptConfig } + : {}), ...(context.webSearchEnabled && process.env.TAVILY_API_KEY ? { TAVILY_API_KEY: process.env.TAVILY_API_KEY } : {}), @@ -2095,7 +2357,9 @@ export class RuntimeFactory { const initStart = Date.now(); assertPersistentDatabaseRequired(runtime); await runtime.initialize({ skipMigrations: true }); - elizaLogger.info(`[RuntimeFactory] initialize() completed in ${Date.now() - initStart}ms`); + elizaLogger.info( + `[RuntimeFactory] initialize() completed in ${Date.now() - initStart}ms`, + ); initSucceeded = true; } catch (e) { const msg = e instanceof Error ? e.message : String(e); @@ -2106,7 +2370,9 @@ export class RuntimeFactory { msg.includes("Failed to create agent") || msg.includes("Failed to create room"); if (!isDuplicate) throw e; - elizaLogger.warn(`[RuntimeFactory] Init error: ${msg.substring(0, 50)}...`); + elizaLogger.warn( + `[RuntimeFactory] Init error: ${msg.substring(0, 50)}...`, + ); this.resolveInitPromise(runtime); } @@ -2143,7 +2409,9 @@ export class RuntimeFactory { if (parallelOps.length > 0) { const parallelStart = Date.now(); await Promise.all(parallelOps); - elizaLogger.debug(`[RuntimeFactory] Parallel ops: ${Date.now() - parallelStart}ms`); + elizaLogger.debug( + `[RuntimeFactory] Parallel ops: ${Date.now() - parallelStart}ms`, + ); } if (initSucceeded) { @@ -2206,7 +2474,10 @@ export class RuntimeFactory { elizaLogger.warn = console.warn.bind(console); elizaLogger.error = console.error.bind(console); elizaLogger.debug = console.debug.bind(console); - elizaLogger.success = (obj: string | Error | Record, msg?: string) => { + elizaLogger.success = ( + obj: string | Error | Record, + msg?: string, + ) => { logger.info(typeof obj === "string" ? `✓ ${obj}` : ["✓", obj, msg]); }; } @@ -2221,7 +2492,10 @@ export class RuntimeFactory { warn: console.warn.bind(console), error: console.error.bind(console), fatal: console.error.bind(console), - success: (obj: string | Error | Record, msg?: string) => { + success: ( + obj: string | Error | Record, + msg?: string, + ) => { logger.info(typeof obj === "string" ? `✓ ${obj}` : ["✓", obj, msg]); }, progress: logger.info.bind(console), @@ -2231,7 +2505,10 @@ export class RuntimeFactory { } } - private async waitForMcpServiceIfNeeded(runtime: AgentRuntime, plugins: Plugin[]): Promise { + private async waitForMcpServiceIfNeeded( + runtime: AgentRuntime, + plugins: Plugin[], + ): Promise { if (!plugins.some((p) => p.name === "mcp")) return; type McpService = { @@ -2257,7 +2534,9 @@ export class RuntimeFactory { const elapsed = Date.now() - startTime; if (!mcpService) { - elizaLogger.warn(`[RuntimeFactory] MCP service not available after ${elapsed}ms`); + elizaLogger.warn( + `[RuntimeFactory] MCP service not available after ${elapsed}ms`, + ); return; } @@ -2303,7 +2582,9 @@ export function isRuntimeCached(agentId: string): boolean { } /** Invalidate all cached runtimes for an organization. */ -export async function invalidateByOrganization(organizationId: string): Promise { +export async function invalidateByOrganization( + organizationId: string, +): Promise { return runtimeFactory.invalidateByOrganization(organizationId); } @@ -2333,7 +2614,10 @@ export const _testing = { } }, - getCacheEntries(): Map { + getCacheEntries(): Map< + string, + { runtime: AgentRuntime; lastUsed: number; createdAt: number } + > { return new Map(runtimeCache["cache"]); }, @@ -2349,7 +2633,11 @@ export const _testing = { ); for (const [cacheKey, entry] of matchingEntries) { - await stopRuntimeServices(entry.runtime, cacheKey, "TestCloseAdapterDirectly"); + await stopRuntimeServices( + entry.runtime, + cacheKey, + "TestCloseAdapterDirectly", + ); } const adapter = dbAdapterPool["adapters"].get(agentId); diff --git a/packages/lib/eliza/shared/evaluators/room-title.ts b/packages/lib/eliza/shared/evaluators/room-title.ts index 8c4dca85d..0ef03b6bd 100644 --- a/packages/lib/eliza/shared/evaluators/room-title.ts +++ b/packages/lib/eliza/shared/evaluators/room-title.ts @@ -76,7 +76,11 @@ async function handler(runtime: IAgentRuntime, message: Memory): Promise { /** * Background title generation - runs without blocking */ -async function generateTitleInBackground(runtime: IAgentRuntime, message: Memory, roomId: string) { +async function generateTitleInBackground( + runtime: IAgentRuntime, + message: Memory, + roomId: string, +) { try { // Check if room already has a title const existingRoom = await runtime.getRoom(roomId as UUID); @@ -101,7 +105,9 @@ async function generateTitleInBackground(runtime: IAgentRuntime, message: Memory }); if (recentMessages.length < 1) { - logger.debug(`[RoomTitle] Not enough messages yet (${recentMessages.length}/1)`); + logger.debug( + `[RoomTitle] Not enough messages yet (${recentMessages.length}/1)`, + ); return; } @@ -173,7 +179,10 @@ async function generateTitleInBackground(runtime: IAgentRuntime, message: Memory export const roomTitleEvaluator: Evaluator = { name: "ROOM_TITLE", similes: ["GENERATE_ROOM_TITLE", "CONVERSATION_TITLE"], - validate: async (runtime: IAgentRuntime, message: Memory): Promise => { + validate: async ( + runtime: IAgentRuntime, + message: Memory, + ): Promise => { if (!message.roomId || !message.entityId) { return false; } @@ -198,12 +207,15 @@ export const roomTitleEvaluator: Evaluator = { }); // Filter messages by entityId since DB filter might not work properly - const filteredUserMessages = userMessages.filter((msg) => msg.entityId === message.entityId); + const filteredUserMessages = userMessages.filter( + (msg) => msg.entityId === message.entityId, + ); const result = filteredUserMessages.length === 1; return result; }, - description: "Generates a concise, descriptive room title from the first user message.", + description: + "Generates a concise, descriptive room title from the first user message.", handler, examples: [], }; diff --git a/packages/lib/eliza/shared/providers/app-config.ts b/packages/lib/eliza/shared/providers/app-config.ts index 5518065d4..738f21aee 100644 --- a/packages/lib/eliza/shared/providers/app-config.ts +++ b/packages/lib/eliza/shared/providers/app-config.ts @@ -25,7 +25,9 @@ export const appConfigProvider: Provider = { description: "App-specific prompt configuration for behavior customization", get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => { // Get config from runtime settings (passed from userContext) - const runtimeConfig = runtime.character.settings?.appPromptConfig as PromptConfig | undefined; + const runtimeConfig = runtime.character.settings?.appPromptConfig as + | PromptConfig + | undefined; // Get preset from environment const envPreset = getPresetFromEnv(); diff --git a/packages/lib/eliza/shared/providers/character.ts b/packages/lib/eliza/shared/providers/character.ts index bb985adbd..a89deadc5 100644 --- a/packages/lib/eliza/shared/providers/character.ts +++ b/packages/lib/eliza/shared/providers/character.ts @@ -8,7 +8,9 @@ import type { } from "@elizaos/core"; import { addHeader } from "@elizaos/core"; -function getExampleMessages(example: MessageExampleGroup | MessageExample[]): MessageExample[] { +function getExampleMessages( + example: MessageExampleGroup | MessageExample[], +): MessageExample[] { return Array.isArray(example) ? example : example.examples; } @@ -25,7 +27,8 @@ function getExampleMessages(example: MessageExampleGroup | MessageExample[]): Me */ export const characterProvider: Provider = { name: "CHARACTER", - description: "Core character identity, personality, and behavioral directives", + description: + "Core character identity, personality, and behavioral directives", get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => { const character = runtime.character; @@ -56,11 +59,15 @@ export const characterProvider: Provider = { // Research: Random trait selection adds variety, can reference MBTI/Big Five const adjectiveString = character.adjectives && character.adjectives.length > 0 - ? character.adjectives[Math.floor(Math.random() * character.adjectives.length)] + ? character.adjectives[ + Math.floor(Math.random() * character.adjectives.length) + ] : ""; const adjective = adjectiveString || ""; - const adjectiveSentence = adjectiveString ? `${character.name} is ${adjectiveString}.` : ""; + const adjectiveSentence = adjectiveString + ? `${character.name} is ${adjectiveString}.` + : ""; // ======================================== // TOPICS (Interest Areas) @@ -144,7 +151,10 @@ export const characterProvider: Provider = { const messageDirections = styleDirectives.length > 0 - ? addHeader(`# Message Directions for ${character.name}`, styleDirectives) + ? addHeader( + `# Message Directions for ${character.name}`, + styleDirectives, + ) : ""; const directions = messageDirections; @@ -156,13 +166,18 @@ export const characterProvider: Provider = { // Contextual selection: Score examples by keyword overlap with current message // Current: Show 3 examples (balanced for context window) const messageExamplesText = (() => { - if (!character.messageExamples || character.messageExamples.length === 0) { + if ( + !character.messageExamples || + character.messageExamples.length === 0 + ) { return ""; } // Extract keywords from the current message for contextual matching const messageText = _message.content?.text?.toLowerCase() ?? ""; - const messageWords = new Set(messageText.split(/\s+/).filter((w) => w.length > 3)); + const messageWords = new Set( + messageText.split(/\s+/).filter((w) => w.length > 3), + ); // Score each example by keyword overlap const scoredExamples = character.messageExamples.map((example) => { @@ -171,7 +186,9 @@ export const characterProvider: Provider = { .map((msg: any) => msg.content?.text ?? "") .join(" ") .toLowerCase(); - const exampleWords = exampleText.split(/\s+/).filter((w) => w.length > 3); + const exampleWords = exampleText + .split(/\s+/) + .filter((w) => w.length > 3); // Count matching keywords let score = 0; diff --git a/packages/lib/eliza/shared/providers/recent-messages.ts b/packages/lib/eliza/shared/providers/recent-messages.ts index 22d96a369..684172a3c 100644 --- a/packages/lib/eliza/shared/providers/recent-messages.ts +++ b/packages/lib/eliza/shared/providers/recent-messages.ts @@ -105,7 +105,8 @@ export const recentMessagesProvider: Provider = { // Check if we have a summary to determine offset and whether to use summarization mode let hasSummary = false; if (memoryService?.hasStorage?.()) { - const currentSummary = await memoryService.getCurrentSessionSummary(roomId); + const currentSummary = + await memoryService.getCurrentSessionSummary(roomId); if (currentSummary) { hasSummary = true; // When we have a summary, fetch recent messages after the summary offset @@ -123,7 +124,8 @@ export const recentMessagesProvider: Provider = { unique: false, }); - const dialogueMessageCount = allMessages.filter(isVisibleMessage).length; + const dialogueMessageCount = + allMessages.filter(isVisibleMessage).length; // If below threshold, show all messages; otherwise show recent only if (dialogueMessageCount < config.shortTermSummarizationThreshold) { @@ -170,16 +172,24 @@ export const recentMessagesProvider: Provider = { } if (recentMessagesText) { - recentMessagesText = addHeader("# Recent Messages", recentMessagesText); + recentMessagesText = addHeader( + "# Recent Messages", + recentMessagesText, + ); } } // Format conversation logs (simple format without IDs) - const formatConversationLog = (messages: Memory[], includeThoughts: boolean): string => { + const formatConversationLog = ( + messages: Memory[], + includeThoughts: boolean, + ): string => { return messages .sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0)) .map((msg) => { - const entity = entitiesData.find((e: Entity) => e.id === msg.entityId); + const entity = entitiesData.find( + (e: Entity) => e.id === msg.entityId, + ); const entityName = entity ? getEntityDisplayName(entity) : msg.entityId === runtime.agentId @@ -211,7 +221,9 @@ export const recentMessagesProvider: Provider = { // Build received message header const metaData = message.metadata as CustomMetadata; - const senderEntity = entitiesData.find((entity: Entity) => entity.id === message.entityId); + const senderEntity = entitiesData.find( + (entity: Entity) => entity.id === message.entityId, + ); const senderName = senderEntity ? getEntityDisplayName(senderEntity) : metaData?.entityName || "Unknown User"; @@ -219,7 +231,10 @@ export const recentMessagesProvider: Provider = { const hasReceivedMessage = !!receivedMessageContent?.trim(); const receivedMessageHeader = hasReceivedMessage - ? addHeader("# Received Message", `${senderName}: ${receivedMessageContent}`) + ? addHeader( + "# Received Message", + `${senderName}: ${receivedMessageContent}`, + ) : ""; const focusHeader = hasReceivedMessage diff --git a/packages/lib/eliza/shared/types/index.ts b/packages/lib/eliza/shared/types/index.ts index 210f6025b..c60a69c7e 100644 --- a/packages/lib/eliza/shared/types/index.ts +++ b/packages/lib/eliza/shared/types/index.ts @@ -2,7 +2,12 @@ * Shared Types for Eliza Plugin System */ -import type { HandlerCallback, IAgentRuntime, Memory, UUID } from "@elizaos/core"; +import type { + HandlerCallback, + IAgentRuntime, + Memory, + UUID, +} from "@elizaos/core"; /** * Extended RUN_ENDED event payload that includes error status. @@ -29,7 +34,10 @@ export interface RunEndedEventPayload { * @param chunk - The text chunk * @param messageId - Optional message ID for coordination */ -export type StreamChunkCallback = (chunk: string, messageId?: UUID) => Promise; +export type StreamChunkCallback = ( + chunk: string, + messageId?: UUID, +) => Promise; /** * Callback for streaming reasoning/chain-of-thought. diff --git a/packages/lib/eliza/shared/utils/helpers.ts b/packages/lib/eliza/shared/utils/helpers.ts index d32bb638f..ec2b5355d 100644 --- a/packages/lib/eliza/shared/utils/helpers.ts +++ b/packages/lib/eliza/shared/utils/helpers.ts @@ -85,7 +85,10 @@ export function removeAISpeak(text: string): string { // Remove sentences containing AI-speak AI_SPEAK_PATTERNS.forEach((pattern) => { // Find and remove sentences containing the pattern - const sentencePattern = new RegExp(`[^.!?]*${pattern.source}[^.!?]*[.!?]?\\s*`, pattern.flags); + const sentencePattern = new RegExp( + `[^.!?]*${pattern.source}[^.!?]*[.!?]?\\s*`, + pattern.flags, + ); cleaned = cleaned.replace(sentencePattern, ""); }); @@ -179,7 +182,10 @@ export interface ProcessedResponse { warnings: string[]; } -export function postProcessResponse(text: string, roomId?: string): ProcessedResponse { +export function postProcessResponse( + text: string, + roomId?: string, +): ProcessedResponse { const warnings: string[] = []; let processed = text; let wasModified = false; @@ -195,7 +201,8 @@ export function postProcessResponse(text: string, roomId?: string): ProcessedRes // Check for repetitive greeting const isRepetitive = - isRepetitiveGreeting(processed) || (roomId ? isRepeatedOpening(roomId, processed) : false); + isRepetitiveGreeting(processed) || + (roomId ? isRepeatedOpening(roomId, processed) : false); if (isRepetitive) { warnings.push("Response starts with repetitive greeting"); @@ -241,7 +248,9 @@ function isBase64DataUrl(url: string): boolean { return url.startsWith("data:"); } -export function getAndClearCachedAttachments(roomId: string): CachedAttachment[] { +export function getAndClearCachedAttachments( + roomId: string, +): CachedAttachment[] { const attachments = actionAttachmentCache.get(roomId) || []; actionAttachmentCache.delete(roomId); return attachments; @@ -269,13 +278,20 @@ interface ActionResult { data?: { attachments?: Attachment[] }; } -export function extractAttachments(actionResults: ActionResult[]): Attachment[] { +export function extractAttachments( + actionResults: ActionResult[], +): Attachment[] { return actionResults .flatMap((result) => result.data?.attachments ?? []) .filter((att): att is Attachment => { if (!att?.url) return false; if (isBase64DataUrl(att.url)) return false; - if (att.url.startsWith("[") || att.url === "" || !att.url.startsWith("http")) return false; + if ( + att.url.startsWith("[") || + att.url === "" || + !att.url.startsWith("http") + ) + return false; return true; }); } @@ -288,7 +304,10 @@ export async function executeProviders( ): Promise { if (plannedProviders.length === 0) return currentState; - const providerState = await runtime.composeState(message, [...plannedProviders, "CHARACTER"]); + const providerState = await runtime.composeState(message, [ + ...plannedProviders, + "CHARACTER", + ]); return { ...currentState, ...providerState }; } @@ -325,7 +344,8 @@ export async function executeActions( } if (content.attachments?.length) { - const existing = actionAttachmentCache.get(message.roomId as string) || []; + const existing = + actionAttachmentCache.get(message.roomId as string) || []; for (const att of content.attachments) { const a = att as Attachment; if (a.url?.startsWith("http")) { @@ -351,7 +371,9 @@ export async function executeActions( wrappedCallback, onStreamChunk ? { onStreamChunk } : undefined, ); - const actionState = await runtime.composeState(message, ["CURRENT_RUN_CONTEXT"]); + const actionState = await runtime.composeState(message, [ + "CURRENT_RUN_CONTEXT", + ]); return { ...currentState, ...actionState }; } @@ -446,7 +468,11 @@ export async function generatePlanningWithStreaming( let streamFilter: ReturnType | null = null; if (onReasoningChunk) { - streamFilter = createPlanningStreamFilter(onReasoningChunk, "planning", messageId); + streamFilter = createPlanningStreamFilter( + onReasoningChunk, + "planning", + messageId, + ); } const response = await runtime.useModel(ModelType.TEXT_LARGE, { @@ -592,10 +618,15 @@ export async function generateResponseWithRetry( // When streaming callback is provided, enable streaming mode with XML filtering // The filter extracts content from ... AND ... tags // Text goes to main response, thought goes to reasoning display - let streamFilter: ReturnType | null = null; + let streamFilter: ReturnType | null = + null; if (onStreamChunk) { - streamFilter = createStreamingXmlFilter(onStreamChunk, messageId, onReasoningChunk); + streamFilter = createStreamingXmlFilter( + onStreamChunk, + messageId, + onReasoningChunk, + ); } const response = await runtime.useModel(ModelType.TEXT_LARGE, { @@ -614,12 +645,18 @@ export async function generateResponseWithRetry( await streamFilter.flush(); } - if (!response || (typeof response === "string" && response.trim() === "")) { - logger.warn(`[generateResponseWithRetry] Attempt ${i + 1}: Empty response from model`); + if ( + !response || + (typeof response === "string" && response.trim() === "") + ) { + logger.warn( + `[generateResponseWithRetry] Attempt ${i + 1}: Empty response from model`, + ); continue; } - lastRawResponse = typeof response === "string" ? response : JSON.stringify(response); + lastRawResponse = + typeof response === "string" ? response : JSON.stringify(response); const parsed = parseKeyValueXml(response) as ParsedResponse | null; if (parsed?.text) { @@ -631,7 +668,10 @@ export async function generateResponseWithRetry( ); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); - logger.error(`[generateResponseWithRetry] Attempt ${i + 1} failed:`, lastError.message); + logger.error( + `[generateResponseWithRetry] Attempt ${i + 1} failed:`, + lastError.message, + ); } } @@ -642,7 +682,9 @@ export async function generateResponseWithRetry( .trim(); if (cleanedResponse.length > 20) { - logger.info(`[generateResponseWithRetry] Using cleaned raw response as fallback`); + logger.info( + `[generateResponseWithRetry] Using cleaned raw response as fallback`, + ); return { text: cleanedResponse, thought: "" }; } } @@ -674,7 +716,10 @@ export async function runEvaluatorsWithTimeout( [responseMemory], ), new Promise((_, reject) => - setTimeout(() => reject(new Error("Evaluators timeout")), EVALUATOR_TIMEOUT_MS), + setTimeout( + () => reject(new Error("Evaluators timeout")), + EVALUATOR_TIMEOUT_MS, + ), ), ]); } diff --git a/packages/lib/eliza/shared/utils/parsers.ts b/packages/lib/eliza/shared/utils/parsers.ts index defea787b..103af7443 100644 --- a/packages/lib/eliza/shared/utils/parsers.ts +++ b/packages/lib/eliza/shared/utils/parsers.ts @@ -30,10 +30,14 @@ export interface ParsedResponse { * @param items - Items to parse (string, array, or undefined). * @returns Array of parsed item strings. */ -export function parsePlannedItems(items: string | string[] | undefined): string[] { +export function parsePlannedItems( + items: string | string[] | undefined, +): string[] { if (!items) return []; - const itemArray = Array.isArray(items) ? items : items.split(",").map((item) => item.trim()); + const itemArray = Array.isArray(items) + ? items + : items.split(",").map((item) => item.trim()); return itemArray.filter((item) => item && item !== ""); } @@ -45,5 +49,8 @@ export function parsePlannedItems(items: string | string[] | undefined): string[ * @returns True if plan indicates immediate response. */ export function canRespondImmediately(plan: ParsedPlan | null): boolean { - return plan?.canRespondNow?.toUpperCase() === "YES" || plan?.canRespondNow === "true"; + return ( + plan?.canRespondNow?.toUpperCase() === "YES" || + plan?.canRespondNow === "true" + ); } diff --git a/packages/lib/eliza/shared/utils/response-tracking.ts b/packages/lib/eliza/shared/utils/response-tracking.ts index 8ed5ea33d..ee968f0a5 100644 --- a/packages/lib/eliza/shared/utils/response-tracking.ts +++ b/packages/lib/eliza/shared/utils/response-tracking.ts @@ -64,7 +64,10 @@ export async function setLatestResponseId( * @param runtime - Agent runtime instance. * @param roomId - Room ID. */ -export async function clearLatestResponseId(runtime: IAgentRuntime, roomId: string): Promise { +export async function clearLatestResponseId( + runtime: IAgentRuntime, + roomId: string, +): Promise { const key = buildResponseCacheKey(runtime.agentId, roomId); logger.debug(`[clearLatestResponseId] Deleting cache key: ${key}`); await runtime.deleteCache(key); diff --git a/packages/lib/eliza/stream-validation.ts b/packages/lib/eliza/stream-validation.ts index 8e8bbe152..b6f516ec7 100644 --- a/packages/lib/eliza/stream-validation.ts +++ b/packages/lib/eliza/stream-validation.ts @@ -4,7 +4,10 @@ */ import { z } from "zod"; -import { MAX_PROMPT_LENGTH, MAX_RESPONSE_STYLE_LENGTH } from "@/lib/constants/image-generation"; +import { + MAX_PROMPT_LENGTH, + MAX_RESPONSE_STYLE_LENGTH, +} from "@/lib/constants/image-generation"; /** * Sanitize prompt string to prevent injection attacks diff --git a/packages/lib/eliza/user-context.ts b/packages/lib/eliza/user-context.ts index f92573f0e..a45e86649 100644 --- a/packages/lib/eliza/user-context.ts +++ b/packages/lib/eliza/user-context.ts @@ -97,13 +97,18 @@ export class UserContextService { // Authenticated users must have an organization if (!authResult.user.organization_id) { - throw new Error("User does not have an organization. Please contact support."); + throw new Error( + "User does not have an organization. Please contact support.", + ); } // Fetch API key and OAuth connections in parallel for efficiency const [apiKey, oauthConnections, resolvedModels] = await Promise.all([ this.getUserApiKey(authResult.user.id, authResult.user.organization_id), - this.getOAuthConnections(authResult.user.organization_id, authResult.user.id), + this.getOAuthConnections( + authResult.user.organization_id, + authResult.user.id, + ), vertexModelRegistryService.resolveModelPreferences({ organizationId: authResult.user.organization_id, userId: authResult.user.id, @@ -111,7 +116,9 @@ export class UserContextService { ]); if (!apiKey) { - logger.error(`[UserContext] No API key found for user ${authResult.user.id}`); + logger.error( + `[UserContext] No API key found for user ${authResult.user.id}`, + ); throw new Error( "No API key found for your account. Please contact support or try logging out and back in.", ); @@ -142,7 +149,10 @@ export class UserContextService { * Get user's elizaOS Cloud API key from database * Centralized API key retrieval - no more scattered getUserElizaCloudApiKey calls */ - private async getUserApiKey(userId: string, orgId: string): Promise { + private async getUserApiKey( + userId: string, + orgId: string, + ): Promise { // Validate inputs if (!userId || userId.trim() === "") { logger.error("[UserContext] Invalid userId provided"); @@ -157,7 +167,9 @@ export class UserContextService { const apiKeys = await apiKeysService.listByOrganization(orgId); // Find user's first active API key - const userKey = apiKeys.find((key) => key.user_id === userId && key.is_active); + const userKey = apiKeys.find( + (key) => key.user_id === userId && key.is_active, + ); if (!userKey) { logger.warn(`[UserContext] No API key found for user ${userId}`); @@ -165,13 +177,21 @@ export class UserContextService { } // Return the full key from the database - logger.info(`[UserContext] Retrieved key for user ${userId}: ${userKey.key_prefix}***`); + logger.info( + `[UserContext] Retrieved key for user ${userId}: ${userKey.key_prefix}***`, + ); return userKey.key; } - private async getOAuthConnections(orgId: string, userId: string): Promise { + private async getOAuthConnections( + orgId: string, + userId: string, + ): Promise { try { - const connections = await oauthService.listConnections({ organizationId: orgId, userId }); + const connections = await oauthService.listConnections({ + organizationId: orgId, + userId, + }); return connections .filter((c) => c.status === "active") .map((c) => ({ platform: c.platform })); @@ -195,13 +215,18 @@ export class UserContextService { appPromptConfig?: PromptConfig, ): Promise { const entityId = session.id || user.id; - const resolvedModels = await vertexModelRegistryService.resolveModelPreferences({ - organizationId: - typeof user.organization_id === "string" && isValidUUID(user.organization_id) - ? user.organization_id - : undefined, - userId: typeof user.id === "string" && isValidUUID(user.id) ? user.id : undefined, - }); + const resolvedModels = + await vertexModelRegistryService.resolveModelPreferences({ + organizationId: + typeof user.organization_id === "string" && + isValidUUID(user.organization_id) + ? user.organization_id + : undefined, + userId: + typeof user.id === "string" && isValidUUID(user.id) + ? user.id + : undefined, + }); logger.info( `[UserContext] Built anonymous context for session ${session.session_token} (mode: ${agentMode})`, @@ -233,7 +258,10 @@ export class UserContextService { entityId: "system", organizationId: "system", agentMode, - apiKey: process.env.SYSTEM_ELIZAOS_API_KEY || process.env.SHARED_ELIZAOS_API_KEY || "", + apiKey: + process.env.SYSTEM_ELIZAOS_API_KEY || + process.env.SHARED_ELIZAOS_API_KEY || + "", isAnonymous: false, name: "System", }; diff --git a/packages/lib/email/utils/rate-limiter.ts b/packages/lib/email/utils/rate-limiter.ts index 4427d7c90..b0b9d11a8 100644 --- a/packages/lib/email/utils/rate-limiter.ts +++ b/packages/lib/email/utils/rate-limiter.ts @@ -11,7 +11,9 @@ import { logger } from "@/lib/utils/logger"; * @param organizationId - Organization ID. * @returns True if email can be sent. */ -export async function canSendLowCreditsEmail(organizationId: string): Promise { +export async function canSendLowCreditsEmail( + organizationId: string, +): Promise { const cacheKey = `low-credits-email-sent:${organizationId}`; const lastSent = await cache.get<{ sentAt: string }>(cacheKey); @@ -32,12 +34,18 @@ export async function canSendLowCreditsEmail(organizationId: string): Promise { +export async function markLowCreditsEmailSent( + organizationId: string, +): Promise { const cacheKey = `low-credits-email-sent:${organizationId}`; const cooldownHours = 24; const cooldownSeconds = cooldownHours * 60 * 60; - await cache.set(cacheKey, { sentAt: new Date().toISOString() }, cooldownSeconds); + await cache.set( + cacheKey, + { sentAt: new Date().toISOString() }, + cooldownSeconds, + ); logger.info("[EmailRateLimiter] Marked low credits email sent", { organizationId, diff --git a/packages/lib/email/utils/template-renderer.ts b/packages/lib/email/utils/template-renderer.ts index 05a58a393..a2bff964d 100644 --- a/packages/lib/email/utils/template-renderer.ts +++ b/packages/lib/email/utils/template-renderer.ts @@ -36,7 +36,10 @@ function loadTemplate(filename: string): string { * @param data - Data object with values to interpolate. * @returns Interpolated template string. */ -function interpolate(template: string, data: Record): string { +function interpolate( + template: string, + data: Record, +): string { return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { return String(data[key] ?? match); }); @@ -134,7 +137,9 @@ export function renderInviteTemplate(data: InviteEmailData): { * @param data - Auto top-up success email data. * @returns Rendered HTML and text versions. */ -export function renderAutoTopUpSuccessTemplate(data: AutoTopUpSuccessEmailData): { +export function renderAutoTopUpSuccessTemplate( + data: AutoTopUpSuccessEmailData, +): { html: string; text: string; } { @@ -228,7 +233,9 @@ This automatic top-up ensures your services continue running without interruptio * @param data - Auto top-up disabled email data. * @returns Rendered HTML and text versions. */ -export function renderAutoTopUpDisabledTemplate(data: AutoTopUpDisabledEmailData): { +export function renderAutoTopUpDisabledTemplate( + data: AutoTopUpDisabledEmailData, +): { html: string; text: string; } { @@ -313,7 +320,9 @@ To prevent service interruptions, please address this issue as soon as possible. * @param data - Purchase confirmation email data. * @returns Rendered HTML and text versions. */ -export function renderPurchaseConfirmationTemplate(data: PurchaseConfirmationEmailData): { +export function renderPurchaseConfirmationTemplate( + data: PurchaseConfirmationEmailData, +): { html: string; text: string; } { @@ -346,7 +355,9 @@ export function renderPurchaseConfirmationTemplate(data: PurchaseConfirmationEma * @param data - Container shutdown warning email data. * @returns Rendered HTML and text versions. */ -export function renderContainerShutdownWarningTemplate(data: ContainerShutdownWarningEmailData): { +export function renderContainerShutdownWarningTemplate( + data: ContainerShutdownWarningEmailData, +): { html: string; text: string; } { diff --git a/packages/lib/events/agent-events.ts b/packages/lib/events/agent-events.ts index 3ec8b8e4e..ffc9c3bba 100644 --- a/packages/lib/events/agent-events.ts +++ b/packages/lib/events/agent-events.ts @@ -15,7 +15,12 @@ const ENV_PREFIX = process.env.VERCEL_ENV || process.env.ENVIRONMENT || "local"; * Agent event structure. */ export interface AgentEvent { - type: "message_received" | "response_started" | "response_chunk" | "response_complete" | "error"; + type: + | "message_received" + | "response_started" + | "response_chunk" + | "response_complete" + | "error"; roomId: string; timestamp: Date; data: Record; @@ -88,7 +93,11 @@ class AgentEventEmitter { void this.publishEvent(roomId, event); } - async emitResponseChunk(roomId: string, chunk: string, tokenIndex: number): Promise { + async emitResponseChunk( + roomId: string, + chunk: string, + tokenIndex: number, + ): Promise { if (!this.enabled || !this.redis) return; const event: AgentEvent = { diff --git a/packages/lib/events/credit-events-redis.ts b/packages/lib/events/credit-events-redis.ts index c51db6535..0bb2c6965 100644 --- a/packages/lib/events/credit-events-redis.ts +++ b/packages/lib/events/credit-events-redis.ts @@ -128,7 +128,9 @@ class RedisCreditEventEmitter { token: process.env.KV_REST_API_TOKEN!, }); - const processMessage = async (message: string | Record) => { + const processMessage = async ( + message: string | Record, + ) => { // Upstash Redis client auto-parses JSON, so message might already be an object let parsed: unknown; if (typeof message === "string") { @@ -224,7 +226,10 @@ class RedisCreditEventEmitter { return { enabled: this.enabled, totalOrganizations: this.activeSubscriptions.size, - totalConnections: organizations.reduce((sum, org) => sum + org.connections, 0), + totalConnections: organizations.reduce( + (sum, org) => sum + org.connections, + 0, + ), organizations, }; } diff --git a/packages/lib/events/credit-events.ts b/packages/lib/events/credit-events.ts index 23252cdb1..072f31482 100644 --- a/packages/lib/events/credit-events.ts +++ b/packages/lib/events/credit-events.ts @@ -65,7 +65,9 @@ class CreditEventEmitter { process.env.NODE_ENV === "production" || process.env.FORCE_REDIS_EVENTS === "true"; - const redisConfigured = !!(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN); + const redisConfigured = !!( + process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN + ); assertPersistentCloudStateConfigured("credit-events", redisConfigured); const useRedis = isServerless && redisConfigured; diff --git a/packages/lib/fragments/api-context.ts b/packages/lib/fragments/api-context.ts index 5c1a0b2e8..62ffcd747 100644 --- a/packages/lib/fragments/api-context.ts +++ b/packages/lib/fragments/api-context.ts @@ -4,7 +4,10 @@ * Builds context from API endpoint discovery for inclusion in generation prompts */ -import { API_ENDPOINTS, type ApiEndpoint } from "@/lib/swagger/endpoint-discovery"; +import { + API_ENDPOINTS, + type ApiEndpoint, +} from "@/lib/swagger/endpoint-discovery"; export interface ApiContextOptions { categories?: string[]; @@ -16,8 +19,15 @@ export interface ApiContextOptions { /** * Build API documentation context for LLM prompts */ -export async function buildApiContext(options: ApiContextOptions = {}): Promise { - const { categories = [], tags = [], limit = 50, includeExamples = true } = options; +export async function buildApiContext( + options: ApiContextOptions = {}, +): Promise { + const { + categories = [], + tags = [], + limit = 50, + includeExamples = true, + } = options; let filteredEndpoints = API_ENDPOINTS; @@ -77,7 +87,9 @@ export async function buildApiContext(options: ApiContextOptions = {}): Promise< for (const param of endpoint.parameters.body) { const required = param.required ? " (required)" : " (optional)"; const example = - includeExamples && param.example ? `\n Example: ${JSON.stringify(param.example)}` : ""; + includeExamples && param.example + ? `\n Example: ${JSON.stringify(param.example)}` + : ""; paramSections.push( `- \`${param.name}\` (${param.type})${required}: ${param.description}${example}`, ); @@ -89,7 +101,9 @@ export async function buildApiContext(options: ApiContextOptions = {}): Promise< for (const param of endpoint.parameters.query) { const required = param.required ? " (required)" : " (optional)"; const example = - includeExamples && param.example ? `\n Example: ${JSON.stringify(param.example)}` : ""; + includeExamples && param.example + ? `\n Example: ${JSON.stringify(param.example)}` + : ""; paramSections.push( `- \`${param.name}\` (${param.type})${required}: ${param.description}${example}`, ); diff --git a/packages/lib/fragments/schema.ts b/packages/lib/fragments/schema.ts index d9462082b..c2c093ccb 100644 --- a/packages/lib/fragments/schema.ts +++ b/packages/lib/fragments/schema.ts @@ -10,9 +10,13 @@ export const fragmentSchema = z.object({ .describe( `Describe what you're about to do and the steps you want to take for generating the fragment in great detail.`, ), - template: z.string().describe("Name of the template used to generate the fragment."), + template: z + .string() + .describe("Name of the template used to generate the fragment."), title: z.string().describe("Short title of the fragment. Max 3 words."), - description: z.string().describe("Short description of the fragment. Max 1 sentence."), + description: z + .string() + .describe("Short description of the fragment. Max 1 sentence."), additional_dependencies: z .array(z.string()) .describe( @@ -25,13 +29,21 @@ export const fragmentSchema = z.object({ ), install_dependencies_command: z .string() - .describe("Command to install additional dependencies required by the fragment."), + .describe( + "Command to install additional dependencies required by the fragment.", + ), port: z .number() .nullable() - .describe("Port number used by the resulted fragment. Null when no ports are exposed."), - file_path: z.string().describe("Relative path to the file, including the file name."), - code: z.string().describe("Code generated by the fragment. Only runnable code is allowed."), + .describe( + "Port number used by the resulted fragment. Null when no ports are exposed.", + ), + file_path: z + .string() + .describe("Relative path to the file, including the file name."), + code: z + .string() + .describe("Code generated by the fragment. Only runnable code is allowed."), }); export type FragmentSchema = z.infer; @@ -40,8 +52,12 @@ export type FragmentSchema = z.infer; * Schema for morph edit instructions (code updates) */ export const morphEditSchema = z.object({ - commentary: z.string().describe("Explain what changes you are making and why"), - instruction: z.string().describe("One line instruction on what the change is"), + commentary: z + .string() + .describe("Explain what changes you are making and why"), + instruction: z + .string() + .describe("One line instruction on what the change is"), edit: z .string() .describe( diff --git a/packages/lib/fragments/templates.ts b/packages/lib/fragments/templates.ts index 2c64e69d2..05f2da166 100644 --- a/packages/lib/fragments/templates.ts +++ b/packages/lib/fragments/templates.ts @@ -15,7 +15,15 @@ export function getTemplateId(id: string) { const templates = { "code-interpreter-v1": { name: "Python data analyst", - lib: ["python", "jupyter", "numpy", "pandas", "matplotlib", "seaborn", "plotly"], + lib: [ + "python", + "jupyter", + "numpy", + "pandas", + "matplotlib", + "seaborn", + "plotly", + ], file: "script.py", instructions: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.", @@ -34,7 +42,8 @@ const templates = { "shadcn", ], file: "pages/index.tsx", - instructions: "A Next.js 13+ app that reloads automatically. Using the pages router.", + instructions: + "A Next.js 13+ app that reloads automatically. Using the pages router.", port: 3000, }, [getTemplateIdSuffix("vue-developer")]: { @@ -47,16 +56,33 @@ const templates = { }, [getTemplateIdSuffix("streamlit-developer")]: { name: "Streamlit developer", - lib: ["streamlit", "pandas", "numpy", "matplotlib", "requests", "seaborn", "plotly"], + lib: [ + "streamlit", + "pandas", + "numpy", + "matplotlib", + "requests", + "seaborn", + "plotly", + ], file: "app.py", instructions: "A streamlit app that reloads automatically.", port: 8501, }, [getTemplateIdSuffix("gradio-developer")]: { name: "Gradio developer", - lib: ["gradio", "pandas", "numpy", "matplotlib", "requests", "seaborn", "plotly"], + lib: [ + "gradio", + "pandas", + "numpy", + "matplotlib", + "requests", + "seaborn", + "plotly", + ], file: "app.py", - instructions: "A gradio app. Gradio Blocks/Interface should be called demo.", + instructions: + "A gradio app. Gradio Blocks/Interface should be called demo.", port: 7860, }, }; diff --git a/packages/lib/hooks/open-web-ui.ts b/packages/lib/hooks/open-web-ui.ts index 874b48a27..7d68a26ba 100644 --- a/packages/lib/hooks/open-web-ui.ts +++ b/packages/lib/hooks/open-web-ui.ts @@ -26,12 +26,16 @@ export async function openWebUIWithPairing(agentId: string): Promise { } try { - const res = await fetch(`/api/v1/milady/agents/${agentId}/pairing-token`, { method: "POST" }); + const res = await fetch(`/api/v1/milady/agents/${agentId}/pairing-token`, { + method: "POST", + }); if (!res.ok) { const data = await res.json().catch(() => ({ error: "Unknown error" })); popup.close(); - toast.error(data.error || `Failed to generate pairing token (HTTP ${res.status})`); + toast.error( + data.error || `Failed to generate pairing token (HTTP ${res.status})`, + ); return; } @@ -48,6 +52,8 @@ export async function openWebUIWithPairing(agentId: string): Promise { } } catch (err) { popup.close(); - toast.error(`Failed to connect: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Failed to connect: ${err instanceof Error ? err.message : String(err)}`, + ); } } diff --git a/packages/lib/hooks/use-admin.ts b/packages/lib/hooks/use-admin.ts index 6b4a3a73e..d01e6fa8c 100644 --- a/packages/lib/hooks/use-admin.ts +++ b/packages/lib/hooks/use-admin.ts @@ -30,12 +30,18 @@ let adminCache: { timestamp: number; walletAddress: string; } | null = null; -let inFlightRequest: Promise<{ isAdmin: boolean; role: AdminRole | null }> | null = null; +let inFlightRequest: Promise<{ + isAdmin: boolean; + role: AdminRole | null; +}> | null = null; const CACHE_TTL = 30000; // 30 seconds function isDevnet(): boolean { - return process.env.NODE_ENV === "development" || process.env.NEXT_PUBLIC_DEVNET === "true"; + return ( + process.env.NODE_ENV === "development" || + process.env.NEXT_PUBLIC_DEVNET === "true" + ); } interface UseAdminResult { @@ -58,7 +64,10 @@ async function fetchAdminStatus( signal: AbortSignal, ): Promise<{ isAdmin: boolean; role: AdminRole | null }> { // In devnet, anvil wallet is always admin - if (isDevnet() && walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase()) { + if ( + isDevnet() && + walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase() + ) { return { isAdmin: true, role: "super_admin" }; } @@ -99,7 +108,8 @@ async function fetchAdminStatus( const isAdmin = res?.headers.get("X-Is-Admin") === "true"; const roleHeader = res?.headers.get("X-Admin-Role"); const role = - roleHeader && ["super_admin", "moderator", "viewer"].includes(roleHeader) + roleHeader && + ["super_admin", "moderator", "viewer"].includes(roleHeader) ? (roleHeader as AdminRole) : null; @@ -163,7 +173,10 @@ export function useAdmin(): UseAdminResult { try { setIsLoading(true); - const status = await fetchAdminStatus(walletAddress, abortController.signal); + const status = await fetchAdminStatus( + walletAddress, + abortController.signal, + ); if (mountedRef.current && currentFetch === fetchCountRef.current) { setIsAdmin(status.isAdmin); diff --git a/packages/lib/hooks/use-deduped-fetch.ts b/packages/lib/hooks/use-deduped-fetch.ts index 92817ccad..458d368b3 100644 --- a/packages/lib/hooks/use-deduped-fetch.ts +++ b/packages/lib/hooks/use-deduped-fetch.ts @@ -19,7 +19,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; // Global request cache and in-flight tracking -const requestCache = new Map(); +const requestCache = new Map< + string, + { data: unknown; timestamp: number; expiresAt: number } +>(); const inFlightRequests = new Map>(); // Default configuration @@ -156,7 +159,9 @@ export function useDedupedFetch( try { const response = await inFlight; const responseData = await response.clone().json(); - const transformedData = transform ? transform(responseData) : responseData; + const transformedData = transform + ? transform(responseData) + : responseData; if (mountedRef.current) { setData(transformedData as T); @@ -210,7 +215,9 @@ export function useDedupedFetch( } const responseData = await response.json(); - const transformedData = transform ? transform(responseData) : responseData; + const transformedData = transform + ? transform(responseData) + : responseData; // Update cache requestCache.set(cacheKey, { @@ -236,7 +243,17 @@ export function useDedupedFetch( throw err; } }, - [url, cacheKey, skip, dedupingInterval, cacheTTL, staleTTL, fetchOptions, transform, data], + [ + url, + cacheKey, + skip, + dedupingInterval, + cacheTTL, + staleTTL, + fetchOptions, + transform, + data, + ], ); // Refetch function (forces revalidation) @@ -248,7 +265,9 @@ export function useDedupedFetch( const mutate = useCallback( (newData: T | ((prev: T | null) => T)) => { const resolvedData = - typeof newData === "function" ? (newData as (prev: T | null) => T)(data) : newData; + typeof newData === "function" + ? (newData as (prev: T | null) => T)(data) + : newData; setData(resolvedData); diff --git a/packages/lib/hooks/use-job-poller.ts b/packages/lib/hooks/use-job-poller.ts index dbf4ea100..477cb062b 100644 --- a/packages/lib/hooks/use-job-poller.ts +++ b/packages/lib/hooks/use-job-poller.ts @@ -51,7 +51,8 @@ export function useJobPoller(options: UseJobPollerOptions = {}) { }, [onComplete, onFailed]); const activeJobs = useMemo( - () => Array.from(jobMap.values()).filter((job) => isActiveStatus(job.status)), + () => + Array.from(jobMap.values()).filter((job) => isActiveStatus(job.status)), [jobMap], ); const hasActiveJobs = activeJobs.length > 0; @@ -94,8 +95,8 @@ export function useJobPoller(options: UseJobPollerOptions = {}) { pollInFlightRef.current = true; try { - const currentActive = Array.from(jobMapRef.current.values()).filter((job) => - isActiveStatus(job.status), + const currentActive = Array.from(jobMapRef.current.values()).filter( + (job) => isActiveStatus(job.status), ); if (currentActive.length === 0 || cancelled) { @@ -140,7 +141,10 @@ export function useJobPoller(options: UseJobPollerOptions = {}) { const updatedJob: TrackedJob = { ...job, status: nextStatus, - error: typeof nextError === "string" ? nextError : (nextError?.message ?? null), + error: + typeof nextError === "string" + ? nextError + : (nextError?.message ?? null), }; setJobMap((prev) => { diff --git a/packages/lib/hooks/use-sandbox-status-poll.ts b/packages/lib/hooks/use-sandbox-status-poll.ts index 65c7fdac8..39731c4c9 100644 --- a/packages/lib/hooks/use-sandbox-status-poll.ts +++ b/packages/lib/hooks/use-sandbox-status-poll.ts @@ -194,7 +194,9 @@ export function useSandboxListPoll( previousStatusesRef.current = statusMap; }, [sandboxes]); - const hasActiveAgents = sandboxes.some((sb) => ACTIVE_STATES.has(sb.status as SandboxStatus)); + const hasActiveAgents = sandboxes.some((sb) => + ACTIVE_STATES.has(sb.status as SandboxStatus), + ); useEffect(() => { if (!hasActiveAgents) { @@ -231,7 +233,10 @@ export function useSandboxListPoll( ACTIVE_STATES.has(prevStatus as SandboxStatus) && newStatus === "running" ) { - callbackRef.current?.(agent.id, agent.agentName ?? agent.agent_name); + callbackRef.current?.( + agent.id, + agent.agentName ?? agent.agent_name, + ); } previousStatusesRef.current.set(agent.id, newStatus); diff --git a/packages/lib/hooks/use-streaming-message.ts b/packages/lib/hooks/use-streaming-message.ts index 7e2a524df..61a602097 100644 --- a/packages/lib/hooks/use-streaming-message.ts +++ b/packages/lib/hooks/use-streaming-message.ts @@ -197,10 +197,17 @@ export async function sendStreamingMessage({ // Process any remaining data in buffer if (buffer.trim()) { try { - processSSEMessage(buffer.trim(), onMessage, onChunk, onReasoning, onError, () => { - completeCalled = true; - onComplete?.(); - }); + processSSEMessage( + buffer.trim(), + onMessage, + onChunk, + onReasoning, + onError, + () => { + completeCalled = true; + onComplete?.(); + }, + ); } catch (err) { console.error("[Stream] Error processing final buffer:", err); onError?.("Stream ended unexpectedly"); @@ -217,7 +224,9 @@ export async function sendStreamingMessage({ // Prevent unbounded buffer growth (potential DoS vector) if (buffer.length > MAX_BUFFER_SIZE) { - throw new Error("Stream buffer exceeded maximum size - possible malformed SSE data"); + throw new Error( + "Stream buffer exceeded maximum size - possible malformed SSE data", + ); } // Process complete SSE messages (separated by double newline) @@ -228,7 +237,14 @@ export async function sendStreamingMessage({ if (!message.trim()) continue; try { - processSSEMessage(message.trim(), onMessage, onChunk, onReasoning, onError, onComplete); + processSSEMessage( + message.trim(), + onMessage, + onChunk, + onReasoning, + onError, + onComplete, + ); } catch (err) { console.error("[Stream] Error parsing SSE message:", err, message); // Continue processing other messages even if one fails @@ -308,13 +324,17 @@ function processSSEMessage( break; case "reasoning": // Chain-of-thought reasoning chunk - shows LLM's planning process - if (onReasoning && isValidReasoningChunkData(data as ReasoningChunkData)) { + if ( + onReasoning && + isValidReasoningChunkData(data as ReasoningChunkData) + ) { onReasoning(data as ReasoningChunkData); } break; case "error": { const errorData = data as SSEErrorData; - const errorMessage = errorData?.message || errorData?.error || "Unknown error"; + const errorMessage = + errorData?.message || errorData?.error || "Unknown error"; onError?.(errorMessage); break; } @@ -340,7 +360,9 @@ function processSSEMessage( /** * Type guard to validate StreamChunkData structure */ -function isValidStreamChunkData(data: StreamChunkData): data is StreamChunkData { +function isValidStreamChunkData( + data: StreamChunkData, +): data is StreamChunkData { if (!data || typeof data !== "object") return false; return ( typeof data.messageId === "string" && @@ -352,7 +374,9 @@ function isValidStreamChunkData(data: StreamChunkData): data is StreamChunkData /** * Type guard to validate ReasoningChunkData structure */ -function isValidReasoningChunkData(data: ReasoningChunkData): data is ReasoningChunkData { +function isValidReasoningChunkData( + data: ReasoningChunkData, +): data is ReasoningChunkData { if (!data || typeof data !== "object") return false; return ( typeof data.messageId === "string" && diff --git a/packages/lib/hooks/use-throttled-streaming.ts b/packages/lib/hooks/use-throttled-streaming.ts index e0ab1b00a..71712f692 100644 --- a/packages/lib/hooks/use-throttled-streaming.ts +++ b/packages/lib/hooks/use-throttled-streaming.ts @@ -125,54 +125,57 @@ export function useThrottledStreamingUpdate() { * The callback receives the current accumulated text. * Updates are throttled to ~30fps for smooth visual appearance. */ - const scheduleUpdate = useCallback((messageId: string, onUpdate: (text: string) => void) => { - // Skip if update already pending for this message - if (pendingUpdatesRef.current.has(messageId)) { - return; - } - - const now = performance.now(); - const lastUpdate = lastUpdateTimeRef.current.get(messageId) || 0; - const timeSinceLastUpdate = now - lastUpdate; - - // If we updated recently, delay this update - if (timeSinceLastUpdate < MIN_UPDATE_INTERVAL_MS) { - const delay = MIN_UPDATE_INTERVAL_MS - timeSinceLastUpdate; - - // Create the pending entry to track both timeout and future rAF - const pendingEntry: PendingUpdate = { - type: "timeout", - timeoutId: setTimeout(() => { - // Use rAF for paint-synced update - const rafId = requestAnimationFrame(() => { - // Only delete AFTER the rAF fires - this prevents race conditions - pendingUpdatesRef.current.delete(messageId); - lastUpdateTimeRef.current.set(messageId, performance.now()); - const text = textMapRef.current.get(messageId) || ""; - onUpdate(text); - }); - // Track the rAF ID so it can be cancelled if clearAll is called - const current = pendingUpdatesRef.current.get(messageId); - if (current && current.type === "timeout") { - current.rafId = rafId; - } - }, delay), - }; + const scheduleUpdate = useCallback( + (messageId: string, onUpdate: (text: string) => void) => { + // Skip if update already pending for this message + if (pendingUpdatesRef.current.has(messageId)) { + return; + } - pendingUpdatesRef.current.set(messageId, pendingEntry); - return; - } + const now = performance.now(); + const lastUpdate = lastUpdateTimeRef.current.get(messageId) || 0; + const timeSinceLastUpdate = now - lastUpdate; + + // If we updated recently, delay this update + if (timeSinceLastUpdate < MIN_UPDATE_INTERVAL_MS) { + const delay = MIN_UPDATE_INTERVAL_MS - timeSinceLastUpdate; + + // Create the pending entry to track both timeout and future rAF + const pendingEntry: PendingUpdate = { + type: "timeout", + timeoutId: setTimeout(() => { + // Use rAF for paint-synced update + const rafId = requestAnimationFrame(() => { + // Only delete AFTER the rAF fires - this prevents race conditions + pendingUpdatesRef.current.delete(messageId); + lastUpdateTimeRef.current.set(messageId, performance.now()); + const text = textMapRef.current.get(messageId) || ""; + onUpdate(text); + }); + // Track the rAF ID so it can be cancelled if clearAll is called + const current = pendingUpdatesRef.current.get(messageId); + if (current && current.type === "timeout") { + current.rafId = rafId; + } + }, delay), + }; + + pendingUpdatesRef.current.set(messageId, pendingEntry); + return; + } - // Schedule immediate rAF update - const frameId = requestAnimationFrame(() => { - pendingUpdatesRef.current.delete(messageId); - lastUpdateTimeRef.current.set(messageId, performance.now()); - const text = textMapRef.current.get(messageId) || ""; - onUpdate(text); - }); + // Schedule immediate rAF update + const frameId = requestAnimationFrame(() => { + pendingUpdatesRef.current.delete(messageId); + lastUpdateTimeRef.current.set(messageId, performance.now()); + const text = textMapRef.current.get(messageId) || ""; + onUpdate(text); + }); - pendingUpdatesRef.current.set(messageId, { type: "raf", frameId }); - }, []); + pendingUpdatesRef.current.set(messageId, { type: "raf", frameId }); + }, + [], + ); return { accumulateChunk, diff --git a/packages/lib/jobs/sandbox-cleanup.ts b/packages/lib/jobs/sandbox-cleanup.ts index d4b922ac1..05f5f0389 100644 --- a/packages/lib/jobs/sandbox-cleanup.ts +++ b/packages/lib/jobs/sandbox-cleanup.ts @@ -66,7 +66,10 @@ export async function cleanupExpiredSandboxes(): Promise<{ }); for (const session of activeSessions) { - if (session.sandbox_id && !activeSandboxIds.includes(session.sandbox_id)) { + if ( + session.sandbox_id && + !activeSandboxIds.includes(session.sandbox_id) + ) { try { await dbWrite .update(appSandboxSessions) diff --git a/packages/lib/mcp/helpers.ts b/packages/lib/mcp/helpers.ts index ab028c15b..010fd3335 100644 --- a/packages/lib/mcp/helpers.ts +++ b/packages/lib/mcp/helpers.ts @@ -53,7 +53,11 @@ export interface MCPToolContext { getToolCache: (toolName: string, params: unknown) => Promise; /** Set tool result in cache */ - setToolCache: (toolName: string, params: unknown, result: unknown) => Promise; + setToolCache: ( + toolName: string, + params: unknown, + result: unknown, + ) => Promise; /** Invalidate tool cache */ invalidateToolCache: (toolName: string, params?: unknown) => Promise; @@ -84,7 +88,9 @@ export interface MCPToolContext { * @param user - Authenticated user from requireAuth/requireAuthOrApiKey * @returns Tool execution context with caching and utilities */ -export async function createMCPContext(user: UserWithOrganization): Promise { +export async function createMCPContext( + user: UserWithOrganization, +): Promise { // Ensure user has an organization (MCP tools not available for anonymous users) if (!user.organization_id) { throw new Error("User must belong to an organization to use MCP tools"); @@ -113,7 +119,11 @@ export async function createMCPContext(user: UserWithOrganization): Promise) { + async deductCredits( + amount: number, + description: string, + metadata?: Record, + ) { const result = await creditsService.deductCredits({ organizationId: org.id, amount, @@ -128,7 +138,11 @@ export async function createMCPContext(user: UserWithOrganization): Promise) { + async refundCredits( + amount: number, + description: string, + metadata?: Record, + ) { await creditsService.refundCredits({ organizationId: org.id, amount, @@ -181,10 +195,14 @@ export function withCredits( } // Deduct credits - const deduction = await context.deductCredits(toolCost, `MCP Tool: ${toolName}`, { - tool: toolName, - params, - }); + const deduction = await context.deductCredits( + toolCost, + `MCP Tool: ${toolName}`, + { + tool: toolName, + params, + }, + ); if (!deduction.success) { throw new Error( @@ -209,12 +227,19 @@ export function withCredits( return result; } catch (error) { // Refund credits on error - logger.error(`[MCP:${toolName}] Tool failed, refunding ${toolCost} credits:`, error); + logger.error( + `[MCP:${toolName}] Tool failed, refunding ${toolCost} credits:`, + error, + ); - await context.refundCredits(toolCost, `MCP Tool Refund: ${toolName} (error)`, { - tool: toolName, - error: error instanceof Error ? error.message : String(error), - }); + await context.refundCredits( + toolCost, + `MCP Tool Refund: ${toolName} (error)`, + { + tool: toolName, + error: error instanceof Error ? error.message : String(error), + }, + ); throw error; // Re-throw to preserve error handling } diff --git a/packages/lib/middleware/app-auth.ts b/packages/lib/middleware/app-auth.ts index 52a56ab66..5dc506d06 100644 --- a/packages/lib/middleware/app-auth.ts +++ b/packages/lib/middleware/app-auth.ts @@ -28,7 +28,10 @@ export interface AppAuthContext { /** * Validate origin against app's allowed origins */ -export function validateOrigin(allowedOrigins: string[], requestOrigin: string): boolean { +export function validateOrigin( + allowedOrigins: string[], + requestOrigin: string, +): boolean { // Allow requests with no origin (e.g., server-to-server) if (!requestOrigin) { return true; @@ -109,7 +112,8 @@ export async function validateAppAuth( } // Get and validate origin - const origin = request.headers.get("origin") || request.headers.get("referer") || ""; + const origin = + request.headers.get("origin") || request.headers.get("referer") || ""; const allowedOrigins = app.allowed_origins as string[]; if (!validateOrigin(allowedOrigins, origin)) { diff --git a/packages/lib/middleware/cors-apps.ts b/packages/lib/middleware/cors-apps.ts index 9df84e828..14b1e4e38 100644 --- a/packages/lib/middleware/cors-apps.ts +++ b/packages/lib/middleware/cors-apps.ts @@ -24,7 +24,9 @@ export interface CorsValidationResult { * Validate if an origin is allowed - ALWAYS returns allowed=true. * Security is enforced via auth tokens, not CORS origin validation. */ -export async function validateOrigin(request: NextRequest): Promise { +export async function validateOrigin( + request: NextRequest, +): Promise { const origin = request.headers.get("origin"); // Always allow all origins - security is via auth tokens @@ -64,7 +66,10 @@ export function createPreflightResponse( /** * Wrapper for API handlers that adds CORS headers */ -export function withCors(origin: string | null, response: T): T { +export function withCors( + origin: string | null, + response: T, +): T { return addCorsHeaders(response, origin) as T; } diff --git a/packages/lib/middleware/rate-limit-redis.ts b/packages/lib/middleware/rate-limit-redis.ts index 6b6c42227..bd555fd6c 100644 --- a/packages/lib/middleware/rate-limit-redis.ts +++ b/packages/lib/middleware/rate-limit-redis.ts @@ -73,7 +73,9 @@ export async function checkRateLimitRedis( const client = getRedisClient(); if (!client) { - logger.warn("[Rate Limit Redis] Redis unavailable, failing open (allowing request)"); + logger.warn( + "[Rate Limit Redis] Redis unavailable, failing open (allowing request)", + ); return { allowed: true, remaining: maxRequests, @@ -113,7 +115,9 @@ export async function checkRateLimitRedis( `[Rate Limit Redis] Limit exceeded for key=${key}, count=${count + 1}, max=${maxRequests}`, ); } else { - logger.debug(`[Rate Limit Redis] Request allowed for key=${key}, remaining=${remaining}`); + logger.debug( + `[Rate Limit Redis] Request allowed for key=${key}, remaining=${remaining}`, + ); } return { diff --git a/packages/lib/middleware/rate-limit.ts b/packages/lib/middleware/rate-limit.ts index 58ee447ea..d2873013d 100644 --- a/packages/lib/middleware/rate-limit.ts +++ b/packages/lib/middleware/rate-limit.ts @@ -46,7 +46,10 @@ function validateRateLimitConfig() { // Note: RATE_LIMIT_DISABLED=true skips this startup validation warning only; // actual rate limiting is still enforced. Use RATE_LIMIT_MULTIPLIER=1000 to // effectively bypass limits in development (replaces the old dev-mode behavior). - if (process.env.RATE_LIMIT_DISABLED === "true" && process.env.NODE_ENV !== "production") { + if ( + process.env.RATE_LIMIT_DISABLED === "true" && + process.env.NODE_ENV !== "production" + ) { return; } @@ -65,7 +68,9 @@ function validateRateLimitConfig() { "Single-server deployments are unaffected.", ); } else { - logger.info("[Rate Limit] ✓ Using Redis-backed rate limiting (production mode)"); + logger.info( + "[Rate Limit] ✓ Using Redis-backed rate limiting (production mode)", + ); } } else { logger.info( @@ -185,7 +190,9 @@ function checkRateLimit( const allowed = entry.count <= config.maxRequests; const remaining = Math.max(0, config.maxRequests - entry.count); - const retryAfter = allowed ? undefined : Math.ceil((entry.resetAt - now) / 1000); + const retryAfter = allowed + ? undefined + : Math.ceil((entry.resetAt - now) / 1000); if (!allowed) { logger.warn("Rate limit exceeded", { @@ -222,7 +229,11 @@ export async function checkRateLimitAsync( const key = keyGenerator(request); if (useRedis) { - const result = await checkRateLimitRedis(key, config.windowMs, config.maxRequests); + const result = await checkRateLimitRedis( + key, + config.windowMs, + config.maxRequests, + ); logger.debug( `[Rate Limit] Redis check for key=${maskKeyForLogging(key)}, allowed=${result.allowed}, remaining=${result.remaining}`, ); @@ -283,7 +294,12 @@ export function rateLimitExceededNextResponse( windowMs: number, policy: "redis" | "in-memory", ): NextResponse { - const { body, headers } = rateLimitExceededPayload(result, maxRequests, windowMs, policy); + const { body, headers } = rateLimitExceededPayload( + result, + maxRequests, + windowMs, + policy, + ); return NextResponse.json(body, { status: 429, headers }); } @@ -294,13 +310,21 @@ export function rateLimitExceededResponse( windowMs: number, policy: "redis" | "in-memory", ): Response { - const { body, headers } = rateLimitExceededPayload(result, maxRequests, windowMs, policy); + const { body, headers } = rateLimitExceededPayload( + result, + maxRequests, + windowMs, + policy, + ); const h = new Headers(headers); h.set("Content-Type", "application/json"); return new Response(JSON.stringify(body), { status: 429, headers: h }); } -export function mcpOrgRateLimitRedisKey(organizationId: string, integrationSlug?: string): string { +export function mcpOrgRateLimitRedisKey( + organizationId: string, + integrationSlug?: string, +): string { return integrationSlug ? `mcp:ratelimit:${integrationSlug}:${organizationId}` : `mcp:ratelimit:${organizationId}`; @@ -331,7 +355,10 @@ export async function enforceOrgRateLimit( // Mirror withRateLimit: skip when Redis is not configured (dev/staging) if (process.env.REDIS_RATE_LIMITING !== "true") return null; - const { windowMs, maxRequests } = await getOrgRpmForEndpoint(organizationId, endpointType); + const { windowMs, maxRequests } = await getOrgRpmForEndpoint( + organizationId, + endpointType, + ); const key = `org:${organizationId}:${endpointType}`; const result = await checkRateLimitRedis(key, windowMs, maxRequests); if (result.allowed) return null; @@ -348,7 +375,10 @@ export async function enforceOrgRateLimit( */ type RouteContext = { params: Promise }; type StaticRouteHandler = (request: NextRequest) => Promise; -type DynamicRouteHandler = (request: NextRequest, context: RouteContext) => Promise; +type DynamicRouteHandler = ( + request: NextRequest, + context: RouteContext, +) => Promise; export function withRateLimit( handler: StaticRouteHandler, @@ -362,14 +392,21 @@ export function withRateLimit>( handler: StaticRouteHandler | DynamicRouteHandler, config: RateLimitConfig, ) { - return async (request: NextRequest, context?: { params: Promise }): Promise => { + return async ( + request: NextRequest, + context?: { params: Promise }, + ): Promise => { const useRedis = process.env.REDIS_RATE_LIMITING === "true"; const keyGenerator = config.keyGenerator || getDefaultKey; const key = keyGenerator(request); let result; if (useRedis) { - result = await checkRateLimitRedis(key, config.windowMs, config.maxRequests); + result = await checkRateLimitRedis( + key, + config.windowMs, + config.maxRequests, + ); logger.debug( `[Rate Limit] Redis check for key=${maskKeyForLogging(key)}, allowed=${result.allowed}, remaining=${result.remaining}`, ); @@ -394,7 +431,12 @@ export function withRateLimit>( ); const policy: "redis" | "in-memory" = useRedis ? "redis" : "in-memory"; - return rateLimitExceededNextResponse(result, config.maxRequests, config.windowMs, policy); + return rateLimitExceededNextResponse( + result, + config.maxRequests, + config.windowMs, + policy, + ); } // Call the actual handler @@ -501,7 +543,10 @@ export interface CostBasedRateLimitConfig { getCost: (request: NextRequest) => number | Promise; } -const costLimitStore = new Map(); +const costLimitStore = new Map< + string, + { totalCost: number; resetAt: number } +>(); /** * Check cost-based rate limit @@ -532,7 +577,9 @@ export async function checkCostBasedRateLimit( const allowed = entry.totalCost <= config.maxCost; const remaining = Math.max(0, config.maxCost - entry.totalCost); - const retryAfter = allowed ? undefined : Math.ceil((entry.resetAt - now) / 1000); + const retryAfter = allowed + ? undefined + : Math.ceil((entry.resetAt - now) / 1000); if (!allowed) { logger.warn("Cost-based rate limit exceeded", { diff --git a/packages/lib/middleware/x402-payment.ts b/packages/lib/middleware/x402-payment.ts index 0d57f81cf..2954dc03e 100644 --- a/packages/lib/middleware/x402-payment.ts +++ b/packages/lib/middleware/x402-payment.ts @@ -23,7 +23,10 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { type VerifyResult, x402FacilitatorService } from "@/lib/services/x402-facilitator"; +import { + type VerifyResult, + x402FacilitatorService, +} from "@/lib/services/x402-facilitator"; import { logger } from "@/lib/utils/logger"; // Types @@ -66,10 +69,15 @@ const X402_CONTEXT_HEADER = "x-x402-context"; * Follows the same signature as withRateLimit(). */ export function withX402Payment>( - handler: (request: NextRequest, context?: { params: Promise }) => Promise, + handler: ( + request: NextRequest, + context?: { params: Promise }, + ) => Promise, config: X402PaymentConfig, ) { - type FacilitatorPaymentPayload = Parameters[0]; + type FacilitatorPaymentPayload = Parameters< + typeof x402FacilitatorService.verify + >[0]; const network = config.network ?? "eip155:8453"; const description = config.description ?? "Paid API endpoint"; @@ -77,7 +85,10 @@ export function withX402Payment>( const maxTimeoutSeconds = config.maxTimeoutSeconds ?? 300; const autoSettle = config.autoSettle !== false; - return async (request: NextRequest, routeContext?: { params: Promise }): Promise => { + return async ( + request: NextRequest, + routeContext?: { params: Promise }, + ): Promise => { // 1. Check for X-PAYMENT header const paymentHeader = request.headers.get("x-payment") ?? @@ -118,7 +129,8 @@ export function withX402Payment>( } // 3. Build payment requirements - const parsedPaymentPayload = paymentPayload as unknown as FacilitatorPaymentPayload; + const parsedPaymentPayload = + paymentPayload as unknown as FacilitatorPaymentPayload; const paymentRequirements = { scheme: "upto", network, @@ -131,7 +143,10 @@ export function withX402Payment>( // 4. Verify with facilitator service let verifyResult: VerifyResult; try { - verifyResult = await x402FacilitatorService.verify(parsedPaymentPayload, paymentRequirements); + verifyResult = await x402FacilitatorService.verify( + parsedPaymentPayload, + paymentRequirements, + ); } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger.error(`[x402-middleware] Verification failed: ${msg}`); @@ -146,7 +161,9 @@ export function withX402Payment>( } if (!verifyResult.isValid) { - logger.warn(`[x402-middleware] Payment invalid: ${verifyResult.invalidReason}`); + logger.warn( + `[x402-middleware] Payment invalid: ${verifyResult.invalidReason}`, + ); return NextResponse.json( { success: false, @@ -168,7 +185,9 @@ export function withX402Payment>( txHash = settleResult.transaction; } catch (err) { const msg = err instanceof Error ? err.message : String(err); - logger.warn(`[x402-middleware] Settlement error (non-blocking): ${msg}`); + logger.warn( + `[x402-middleware] Settlement error (non-blocking): ${msg}`, + ); // Settlement failure is non-blocking — the payment was verified } } @@ -211,7 +230,9 @@ export function withX402Payment>( /** * Extract x402 payment context from a request (for use inside handlers). */ -export function getX402PaymentContext(request: NextRequest): X402PaymentContext | null { +export function getX402PaymentContext( + request: NextRequest, +): X402PaymentContext | null { const header = request.headers.get(X402_CONTEXT_HEADER); if (!header) return null; @@ -259,7 +280,9 @@ function buildPaymentRequiredResponse( ], }; - const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64"); + const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString( + "base64", + ); return new NextResponse( JSON.stringify({ diff --git a/packages/lib/milady-web-ui.ts b/packages/lib/milady-web-ui.ts index 702825f5c..76f8469be 100644 --- a/packages/lib/milady-web-ui.ts +++ b/packages/lib/milady-web-ui.ts @@ -2,7 +2,10 @@ import type { MiladySandbox } from "@/db/schemas/milady-sandboxes"; const DEFAULT_AGENT_BASE_DOMAIN = "waifu.fun"; -type MiladyWebUiTarget = Pick; +type MiladyWebUiTarget = Pick< + MiladySandbox, + "id" | "headscale_ip" | "web_ui_port" | "bridge_port" +>; type MiladyClientWebUiTarget = MiladyWebUiTarget & { canonicalWebUiUrl?: string | null; @@ -16,7 +19,8 @@ export interface MiladyWebUiUrlOptions { /** Resolved base domain for the current deployment (e.g. "waifu.fun"). */ export function getAgentBaseDomain(): string { return ( - normalizeAgentBaseDomain(process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN) ?? DEFAULT_AGENT_BASE_DOMAIN + normalizeAgentBaseDomain(process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN) ?? + DEFAULT_AGENT_BASE_DOMAIN ); } @@ -68,7 +72,8 @@ export function getMiladyAgentPublicWebUiUrl( options: MiladyWebUiUrlOptions = {}, ): string | null { const rawOpt = options.baseDomain; - const baseDomainOptionSupplied = Object.hasOwn(options, "baseDomain") && rawOpt !== undefined; + const baseDomainOptionSupplied = + Object.hasOwn(options, "baseDomain") && rawOpt !== undefined; if (baseDomainOptionSupplied) { const explicit = normalizeAgentBaseDomain(rawOpt); @@ -106,7 +111,8 @@ export function getPreferredMiladyAgentWebUiUrl( options: MiladyWebUiUrlOptions = {}, ): string | null { return ( - getMiladyAgentPublicWebUiUrl(sandbox, options) ?? getMiladyAgentDirectWebUiUrl(sandbox, options) + getMiladyAgentPublicWebUiUrl(sandbox, options) ?? + getMiladyAgentDirectWebUiUrl(sandbox, options) ); } diff --git a/packages/lib/models/catalog.ts b/packages/lib/models/catalog.ts index 9292456fe..11b2beefc 100644 --- a/packages/lib/models/catalog.ts +++ b/packages/lib/models/catalog.ts @@ -81,15 +81,24 @@ const QWEN_TEXT_MODEL_IDS = [ "alibaba/qwen3-coder-next", "alibaba/qwen-3-14b", ] as const; -const DEEPSEEK_TEXT_MODEL_IDS = ["deepseek/deepseek-v3.2", "deepseek/deepseek-r1"] as const; +const DEEPSEEK_TEXT_MODEL_IDS = [ + "deepseek/deepseek-v3.2", + "deepseek/deepseek-r1", +] as const; const ZAI_TEXT_MODEL_IDS = [ "zai/glm-5.1", "zai/glm-5", "zai/glm-4.7", "zai/glm-4.7-flashx", ] as const; -const MOONSHOT_TEXT_MODEL_IDS = ["moonshotai/kimi-k2.5", "moonshotai/kimi-k2-turbo"] as const; -const META_TEXT_MODEL_IDS = ["meta/llama-4-maverick", "meta/llama-4-scout"] as const; +const MOONSHOT_TEXT_MODEL_IDS = [ + "moonshotai/kimi-k2.5", + "moonshotai/kimi-k2-turbo", +] as const; +const META_TEXT_MODEL_IDS = [ + "meta/llama-4-maverick", + "meta/llama-4-scout", +] as const; const BYTEDANCE_TEXT_MODEL_IDS = ["bytedance/seed-1.8"] as const; function formatProviderLabel(provider: string): string { @@ -172,7 +181,9 @@ function titleCase(value: string): string { } function buildSelectorName(modelId: string): string { - const [provider, rawName] = modelId.includes("/") ? modelId.split("/", 2) : ["", modelId]; + const [provider, rawName] = modelId.includes("/") + ? modelId.split("/", 2) + : ["", modelId]; if (!rawName) { return modelId; @@ -201,7 +212,8 @@ function buildSelectorDescription(modelId: string): string { if (id.includes("mini")) return "Faster, lower-cost option"; if (id.includes("nano")) return "Smallest, lowest-cost option"; if (id.includes("oss")) return "Open-weight reasoning model"; - if (/\/o[134]/.test(id) || id.endsWith("/o1")) return "Reasoning-focused model"; + if (/\/o[134]/.test(id) || id.endsWith("/o1")) + return "Reasoning-focused model"; if (id.includes("compound")) return "Groq compound system model"; if (id.includes("4o")) return "General-purpose multimodal model"; if (id.includes("4.1")) return "Reliable general-purpose model"; @@ -356,7 +368,8 @@ function getProviderSortIndex(provider: string): number { export function sortSelectorModels(models: SelectorModel[]): SelectorModel[] { return [...models].sort((a, b) => { - const providerDelta = getProviderSortIndex(a.provider) - getProviderSortIndex(b.provider); + const providerDelta = + getProviderSortIndex(a.provider) - getProviderSortIndex(b.provider); if (providerDelta !== 0) { return providerDelta; } @@ -392,12 +405,14 @@ export const OPENROUTER_FREE_MODEL_MAP: Record = { "openai/gpt-4o-mini": "meta-llama/llama-3.3-70b-instruct:free", "openai/gpt-4.1": "meta-llama/llama-3.3-70b-instruct:free", "openai/gpt-4.1-mini": "meta-llama/llama-3.3-70b-instruct:free", - "anthropic/claude-sonnet-4-20250514": "meta-llama/llama-3.3-70b-instruct:free", + "anthropic/claude-sonnet-4-20250514": + "meta-llama/llama-3.3-70b-instruct:free", "anthropic/claude-3.5-sonnet": "meta-llama/llama-3.3-70b-instruct:free", "google/gemini-2.0-flash": "google/gemini-2.0-flash-exp:free", }; -export const OPENROUTER_DEFAULT_FREE_MODEL = "meta-llama/llama-3.3-70b-instruct:free"; +export const OPENROUTER_DEFAULT_FREE_MODEL = + "meta-llama/llama-3.3-70b-instruct:free"; /** * Resolve a model ID to an OpenRouter free equivalent when falling back. diff --git a/packages/lib/models/model-tiers.ts b/packages/lib/models/model-tiers.ts index 9db864fb6..36f63ac17 100644 --- a/packages/lib/models/model-tiers.ts +++ b/packages/lib/models/model-tiers.ts @@ -86,7 +86,14 @@ export const MODEL_TIERS: Record = { outputPer1k: 0.018, currency: "USD", }, - capabilities: ["text", "code", "reasoning", "vision", "function_calling", "long_context"], + capabilities: [ + "text", + "code", + "reasoning", + "vision", + "function_calling", + "long_context", + ], contextWindow: 200000, recommended: true, }, @@ -102,7 +109,14 @@ export const MODEL_TIERS: Record = { outputPer1k: 0.09, currency: "USD", }, - capabilities: ["text", "code", "reasoning", "vision", "function_calling", "long_context"], + capabilities: [ + "text", + "code", + "reasoning", + "vision", + "function_calling", + "long_context", + ], contextWindow: 200000, }, } as const; @@ -494,17 +508,22 @@ export function estimateTierCost( /** * Check if a tier has a specific capability */ -export function tierHasCapability(tier: ModelTier, capability: ModelCapability): boolean { +export function tierHasCapability( + tier: ModelTier, + capability: ModelCapability, +): boolean { return MODEL_TIERS[tier].capabilities.includes(capability); } /** * Get all tiers that have a specific capability */ -export function getTiersWithCapability(capability: ModelCapability): ModelTier[] { - return MODEL_TIER_LIST.filter((config) => config.capabilities.includes(capability)).map( - (config) => config.id, - ); +export function getTiersWithCapability( + capability: ModelCapability, +): ModelTier[] { + return MODEL_TIER_LIST.filter((config) => + config.capabilities.includes(capability), + ).map((config) => config.id); } /** diff --git a/packages/lib/onboarding/tours.ts b/packages/lib/onboarding/tours.ts index 366d5dbcf..4e74022ff 100644 --- a/packages/lib/onboarding/tours.ts +++ b/packages/lib/onboarding/tours.ts @@ -26,7 +26,8 @@ export const BUILD_TOUR: OnboardingTour = { { target: "[data-onboarding='build-save']", title: "Save Your Agent", - description: "Once you're happy with your agent, click here to save it and start chatting!", + description: + "Once you're happy with your agent, click here to save it and start chatting!", placement: "bottom", }, ], diff --git a/packages/lib/polyfills/dom-polyfills.ts b/packages/lib/polyfills/dom-polyfills.ts index b2371c2cd..390125037 100644 --- a/packages/lib/polyfills/dom-polyfills.ts +++ b/packages/lib/polyfills/dom-polyfills.ts @@ -63,7 +63,14 @@ if (typeof window === "undefined") { closePath() {} moveTo(x: number, y: number) {} lineTo(x: number, y: number) {} - bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number) {} + bezierCurveTo( + cp1x: number, + cp1y: number, + cp2x: number, + cp2y: number, + x: number, + y: number, + ) {} quadraticCurveTo(cpx: number, cpy: number, x: number, y: number) {} arc( x: number, @@ -89,23 +96,24 @@ if (typeof window === "undefined") { // Polyfill OffscreenCanvas if needed if (typeof globalThis.OffscreenCanvas === "undefined") { - (globalThis as Record).OffscreenCanvas = class OffscreenCanvas { - width: number; - height: number; + (globalThis as Record).OffscreenCanvas = + class OffscreenCanvas { + width: number; + height: number; - constructor(width: number, height: number) { - this.width = width; - this.height = height; - } + constructor(width: number, height: number) { + this.width = width; + this.height = height; + } - getContext(contextType: string) { - return null; - } + getContext(contextType: string) { + return null; + } - convertToBlob() { - return Promise.resolve(new Blob()); - } - }; + convertToBlob() { + return Promise.resolve(new Blob()); + } + }; } } diff --git a/packages/lib/pricing-constants.ts b/packages/lib/pricing-constants.ts index 476abbadb..97947575d 100644 --- a/packages/lib/pricing-constants.ts +++ b/packages/lib/pricing-constants.ts @@ -28,11 +28,15 @@ const BASE_VIDEO_GENERATION_FALLBACK_COST = 0.0208; // ~$0.0208 per fallback vid // Final costs with 20% platform markup export const IMAGE_GENERATION_COST = - Math.round(BASE_IMAGE_GENERATION_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100; // $0.01 per image + Math.round(BASE_IMAGE_GENERATION_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / + 100; // $0.01 per image export const VIDEO_GENERATION_COST = - Math.round(BASE_VIDEO_GENERATION_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100; // $0.05 per video + Math.round(BASE_VIDEO_GENERATION_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / + 100; // $0.05 per video export const VIDEO_GENERATION_FALLBACK_COST = - Math.round(BASE_VIDEO_GENERATION_FALLBACK_COST * PLATFORM_MARKUP_MULTIPLIER * 1000) / 1000; // $0.025 per fallback video + Math.round( + BASE_VIDEO_GENERATION_FALLBACK_COST * PLATFORM_MARKUP_MULTIPLIER * 1000, + ) / 1000; // $0.025 per fallback video /** * Monthly credit cap in USD. @@ -48,11 +52,15 @@ const BASE_VOICE_SAMPLE_UPLOAD_COST = 0.042; // Base cost ~$0.042 const BASE_VOICE_UPDATE_COST = 0.083; // Base cost ~$0.083 export const VOICE_CLONE_INSTANT_COST = - Math.round(BASE_VOICE_CLONE_INSTANT_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100; // ~$0.50 - 1-3 min audio, ~30s processing + Math.round(BASE_VOICE_CLONE_INSTANT_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / + 100; // ~$0.50 - 1-3 min audio, ~30s processing export const VOICE_CLONE_PROFESSIONAL_COST = - Math.round(BASE_VOICE_CLONE_PROFESSIONAL_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100; // ~$2.00 - 30+ min audio, 30-60min processing + Math.round( + BASE_VOICE_CLONE_PROFESSIONAL_COST * PLATFORM_MARKUP_MULTIPLIER * 100, + ) / 100; // ~$2.00 - 30+ min audio, 30-60min processing export const VOICE_SAMPLE_UPLOAD_COST = - Math.round(BASE_VOICE_SAMPLE_UPLOAD_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100; // ~$0.05 - Additional samples to existing voice + Math.round(BASE_VOICE_SAMPLE_UPLOAD_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / + 100; // ~$0.05 - Additional samples to existing voice export const VOICE_UPDATE_COST = Math.round(BASE_VOICE_UPDATE_COST * PLATFORM_MARKUP_MULTIPLIER * 100) / 100; // ~$0.10 - Update voice metadata/settings export const CUSTOM_VOICE_TTS_MARKUP = 1.1; // 10% additional markup for using custom cloned voices (on top of platform markup) @@ -65,8 +73,10 @@ const BASE_TTS_COST_PER_1K_CHARS = 0.024; // ElevenLabs ~$0.024 per 1K character const BASE_STT_COST_PER_MINUTE = 0.0033; // ElevenLabs ~$0.0033 per minute export const TTS_COST_PER_1K_CHARS = - Math.round(BASE_TTS_COST_PER_1K_CHARS * PLATFORM_MARKUP_MULTIPLIER * 10000) / 10000; // ~$0.029 per 1K chars with markup + Math.round(BASE_TTS_COST_PER_1K_CHARS * PLATFORM_MARKUP_MULTIPLIER * 10000) / + 10000; // ~$0.029 per 1K chars with markup export const STT_COST_PER_MINUTE = - Math.round(BASE_STT_COST_PER_MINUTE * PLATFORM_MARKUP_MULTIPLIER * 10000) / 10000; // ~$0.004 per minute with markup + Math.round(BASE_STT_COST_PER_MINUTE * PLATFORM_MARKUP_MULTIPLIER * 10000) / + 10000; // ~$0.004 per minute with markup export const TTS_MINIMUM_COST = 0.001; // Minimum charge for any TTS request export const STT_MINIMUM_COST = 0.001; // Minimum charge for any STT request diff --git a/packages/lib/pricing.ts b/packages/lib/pricing.ts index 7d98ff449..43da7fbc3 100644 --- a/packages/lib/pricing.ts +++ b/packages/lib/pricing.ts @@ -206,7 +206,9 @@ export async function estimateRequestCost( const estimatedInputTokens = estimateTokens(messageText); const estimatedOutputTokens = - typeof maxOutputTokens === "number" && maxOutputTokens > 0 ? maxOutputTokens : 500; + typeof maxOutputTokens === "number" && maxOutputTokens > 0 + ? maxOutputTokens + : 500; const { totalCost } = await calculateCost( normalizedModel, diff --git a/packages/lib/privy-sync.ts b/packages/lib/privy-sync.ts index f53b2c817..da9cfbf4f 100644 --- a/packages/lib/privy-sync.ts +++ b/packages/lib/privy-sync.ts @@ -9,7 +9,10 @@ import { organizationInvitesRepository } from "@/db/repositories/organization-invites"; import { usersRepository } from "@/db/repositories/users"; -import { abuseDetectionService, type SignupContext } from "@/lib/services/abuse-detection"; +import { + abuseDetectionService, + type SignupContext, +} from "@/lib/services/abuse-detection"; import { apiKeysService } from "@/lib/services/api-keys"; import { charactersService } from "@/lib/services/characters/characters"; import { creditsService } from "@/lib/services/credits"; @@ -59,12 +62,20 @@ function extractErrorMetadata(candidate: unknown): { }; return { - code: typeof typedCandidate.code === "string" ? typedCandidate.code : undefined, + code: + typeof typedCandidate.code === "string" ? typedCandidate.code : undefined, constraint: - typeof typedCandidate.constraint === "string" ? typedCandidate.constraint : undefined, - detail: typeof typedCandidate.detail === "string" ? typedCandidate.detail : undefined, + typeof typedCandidate.constraint === "string" + ? typedCandidate.constraint + : undefined, + detail: + typeof typedCandidate.detail === "string" + ? typedCandidate.detail + : undefined, message: - typeof typedCandidate.message === "string" ? typedCandidate.message : String(candidate), + typeof typedCandidate.message === "string" + ? typedCandidate.message + : String(candidate), }; } @@ -74,8 +85,10 @@ function isRecoverablePrivyProjectionConflict(error: unknown): boolean { } const errorMetadata = extractErrorMetadata(error); - const causeMetadata = "cause" in error ? extractErrorMetadata(error.cause) : { message: "" }; - const isUniqueViolation = errorMetadata.code === "23505" || causeMetadata.code === "23505"; + const causeMetadata = + "cause" in error ? extractErrorMetadata(error.cause) : { message: "" }; + const isUniqueViolation = + errorMetadata.code === "23505" || causeMetadata.code === "23505"; const hasExactPrivyConstraint = errorMetadata.constraint === PRIVY_IDENTITY_UNIQUE_CONSTRAINT || causeMetadata.constraint === PRIVY_IDENTITY_UNIQUE_CONSTRAINT; @@ -106,12 +119,15 @@ async function recoverCanonicalPrivyUser( return false; } - logger.warn("[PrivySync] Recovered from stale Privy identity projection conflict", { - context, - expectedUserId, - privyUserId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[PrivySync] Recovered from stale Privy identity projection conflict", + { + context, + expectedUserId, + privyUserId, + error: error instanceof Error ? error.message : String(error), + }, + ); return true; } @@ -129,8 +145,14 @@ async function rollbackCreatedUserSafely( logger.error("[PrivySync] Failed to roll back newly created user", { context, userId, - originalError: originalError instanceof Error ? originalError.message : String(originalError), - rollbackError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), + originalError: + originalError instanceof Error + ? originalError.message + : String(originalError), + rollbackError: + rollbackError instanceof Error + ? rollbackError.message + : String(rollbackError), }); } } @@ -149,8 +171,14 @@ async function restorePreviousPrivyUserIdSafely( logger.error("[PrivySync] Failed to restore previous Privy user ID", { userId, previousPrivyUserId, - originalError: originalError instanceof Error ? originalError.message : String(originalError), - rollbackError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), + originalError: + originalError instanceof Error + ? originalError.message + : String(originalError), + rollbackError: + rollbackError instanceof Error + ? rollbackError.message + : String(rollbackError), }); } } @@ -210,7 +238,9 @@ import { logger } from "@/lib/utils/logger"; */ type PrivyUserData = PrivyUser; -async function finalizeSyncedUser(user: UserWithOrganization): Promise { +async function finalizeSyncedUser( + user: UserWithOrganization, +): Promise { try { const stewardUserId = await ensureStewardUserMappingForUser( { @@ -262,7 +292,11 @@ export async function syncUserFromPrivy( if (!email && privyUser.linkedAccounts) { for (const account of privyUser.linkedAccounts) { // Email account type - if (account.type === "email" && "address" in account && typeof account.address === "string") { + if ( + account.type === "email" && + "address" in account && + typeof account.address === "string" + ) { email = account.address.toLowerCase().trim(); break; } @@ -311,7 +345,11 @@ export async function syncUserFromPrivy( if (privyUser.linkedAccounts) { // Try OAuth account name first for (const account of privyUser.linkedAccounts) { - if ("name" in account && typeof account.name === "string" && account.name.length > 0) { + if ( + "name" in account && + typeof account.name === "string" && + account.name.length > 0 + ) { name = account.name; break; } @@ -348,11 +386,14 @@ export async function syncUserFromPrivy( try { await usersService.upsertPrivyIdentity(user.id, privyUserId); } catch (error) { - logger.warn("[PrivySync] Failed to repair Privy identity projection for existing user", { - userId: user.id, - privyUserId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[PrivySync] Failed to repair Privy identity projection for existing user", + { + userId: user.id, + privyUserId, + error: error instanceof Error ? error.message : String(error), + }, + ); } // Update user if needed @@ -405,7 +446,13 @@ export async function syncUserFromPrivy( await usersService.upsertPrivyIdentity(newUser.id, privyUserId); } catch (error) { const recovered = - newUser && (await recoverCanonicalPrivyUser(newUser.id, privyUserId, "invite", error)); + newUser && + (await recoverCanonicalPrivyUser( + newUser.id, + privyUserId, + "invite", + error, + )); if (newUser && !recovered) { await rollbackCreatedUserSafely(newUser.id, "invite", error); @@ -418,13 +465,18 @@ export async function syncUserFromPrivy( const userWithOrg = await usersService.getByPrivyIdForWrite(privyUserId); if (!userWithOrg) { - throw new Error(`Failed to fetch newly created user ${privyUserId} after accepting invite`); + throw new Error( + `Failed to fetch newly created user ${privyUserId} after accepting invite`, + ); } // Mark the invite only after the canonical post-write lookup succeeds. // This stays correct even when the identity upsert was recovered from an // existing projection conflict, because userWithOrg.id is the verified owner. - await organizationInvitesRepository.markAsAccepted(pendingInvite.id, userWithOrg.id); + await organizationInvitesRepository.markAsAccepted( + pendingInvite.id, + userWithOrg.id, + ); logDiscordSignup({ userId: userWithOrg.id, @@ -444,7 +496,8 @@ export async function syncUserFromPrivy( // Check if email is already taken by a different Privy account (account linking) if (email) { - const existingByEmail = await usersService.getByEmailWithOrganization(email); + const existingByEmail = + await usersService.getByEmailWithOrganization(email); if (existingByEmail && existingByEmail.privy_user_id !== privyUserId) { console.info( `Linking Privy account for ${email}: ${existingByEmail.privy_user_id} → ${privyUserId}`, @@ -459,13 +512,19 @@ export async function syncUserFromPrivy( try { await usersService.upsertPrivyIdentity(existingByEmail.id, privyUserId); } catch (error) { - await restorePreviousPrivyUserIdSafely(existingByEmail.id, previousPrivyUserId, error); + await restorePreviousPrivyUserIdSafely( + existingByEmail.id, + previousPrivyUserId, + error, + ); throw error; } const linkedUser = await usersService.getByPrivyIdForWrite(privyUserId); if (!linkedUser) { - throw new Error(`Failed to fetch user after Privy account linking for ${email}`); + throw new Error( + `Failed to fetch user after Privy account linking for ${email}`, + ); } return await finalizeSyncedUser(linkedUser); } @@ -482,7 +541,9 @@ export async function syncUserFromPrivy( }); if (!abuseCheck.allowed) { - throw new Error(abuseCheck.reason || "Signup blocked due to suspicious activity"); + throw new Error( + abuseCheck.reason || "Signup blocked due to suspicious activity", + ); } } @@ -500,7 +561,9 @@ export async function syncUserFromPrivy( orgSlug = `${sanitized}-${timestamp}${random}`; } else { // Should never reach here - name always has a fallback - throw new Error(`Cannot generate organization slug for user ${privyUserId}`); + throw new Error( + `Cannot generate organization slug for user ${privyUserId}`, + ); } // Ensure slug is unique @@ -508,9 +571,13 @@ export async function syncUserFromPrivy( while (await organizationsService.getBySlug(orgSlug)) { attempts++; if (attempts > 10) { - throw new Error(`Failed to generate unique organization slug for user ${privyUserId}`); + throw new Error( + `Failed to generate unique organization slug for user ${privyUserId}`, + ); } - orgSlug = email ? generateSlugFromEmail(email) : generateSlugFromWallet(walletAddress!); + orgSlug = email + ? generateSlugFromEmail(email) + : generateSlugFromWallet(walletAddress!); } // Create organization with zero balance initially @@ -522,7 +589,10 @@ export async function syncUserFromPrivy( // Record signup metadata for future abuse detection if (signupContext) { - await abuseDetectionService.recordSignupMetadata(organization.id, signupContext); + await abuseDetectionService.recordSignupMetadata( + organization.id, + signupContext, + ); } // Add initial free credits via creditsService for proper tracking @@ -585,7 +655,9 @@ export async function syncUserFromPrivy( for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { // Wait a bit for the other transaction to commit - await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** (attempt - 1))); + await new Promise((resolve) => + setTimeout(resolve, 50 * 2 ** (attempt - 1)), + ); } // Try to find by Privy ID first (most common race condition) @@ -612,7 +684,10 @@ export async function syncUserFromPrivy( updated_at: new Date(), }); try { - await usersService.upsertPrivyIdentity(existingUser.id, privyUserId); + await usersService.upsertPrivyIdentity( + existingUser.id, + privyUserId, + ); } catch (upsertError) { await usersService.update(existingUser.id, { privy_user_id: previousPrivyUserId, @@ -621,9 +696,12 @@ export async function syncUserFromPrivy( throw upsertError; } await organizationsService.delete(organization.id); - const linkedUser = await usersService.getByPrivyIdForWrite(privyUserId); + const linkedUser = + await usersService.getByPrivyIdForWrite(privyUserId); if (!linkedUser) { - throw new Error(`Failed to fetch user after Privy account linking for ${email}`); + throw new Error( + `Failed to fetch user after Privy account linking for ${email}`, + ); } return await finalizeSyncedUser(linkedUser); } @@ -658,7 +736,12 @@ export async function syncUserFromPrivy( try { await usersService.upsertPrivyIdentity(createdUser.id, privyUserId); } catch (error) { - const recovered = await recoverCanonicalPrivyUser(createdUser.id, privyUserId, "signup", error); + const recovered = await recoverCanonicalPrivyUser( + createdUser.id, + privyUserId, + "signup", + error, + ); if (!recovered) { await rollbackCreatedUserSafely(createdUser.id, "signup", error); @@ -709,7 +792,10 @@ export async function syncUserFromPrivy( void ensureUserHasApiKey(userWithOrg.id, userWithOrg.organization?.id || ""); // Auto-create default Eliza character for new user (fire-and-forget) - void ensureDefaultCharacter(userWithOrg.id, userWithOrg.organization?.id || ""); + void ensureDefaultCharacter( + userWithOrg.id, + userWithOrg.organization?.id || "", + ); return await finalizeSyncedUser(userWithOrg); } @@ -725,7 +811,10 @@ export async function syncUserFromPrivy( * @param userId - User ID. * @param organizationId - Organization ID. */ -async function ensureUserHasApiKey(userId: string, organizationId: string): Promise { +async function ensureUserHasApiKey( + userId: string, + organizationId: string, +): Promise { // Validate inputs if (!userId || userId.trim() === "") { logger.warn("[PrivySync] Invalid userId, skipping API key creation"); @@ -733,13 +822,16 @@ async function ensureUserHasApiKey(userId: string, organizationId: string): Prom } if (!organizationId || organizationId.trim() === "") { - logger.warn(`[PrivySync] No organization for user ${userId}, skipping API key creation`); + logger.warn( + `[PrivySync] No organization for user ${userId}, skipping API key creation`, + ); return; } try { // Check if user already has an API key - const existingKeys = await apiKeysService.listByOrganization(organizationId); + const existingKeys = + await apiKeysService.listByOrganization(organizationId); const userHasKey = existingKeys.some((key) => key.user_id === userId); if (userHasKey) { @@ -754,7 +846,10 @@ async function ensureUserHasApiKey(userId: string, organizationId: string): Prom is_active: true, }); } catch (error) { - console.error(`[PrivySync] Error creating API key for user ${userId}:`, error); + console.error( + `[PrivySync] Error creating API key for user ${userId}:`, + error, + ); throw error; } } @@ -766,9 +861,14 @@ async function ensureUserHasApiKey(userId: string, organizationId: string): Prom * @param userId - User ID. * @param organizationId - Organization ID. */ -async function ensureDefaultCharacter(userId: string, organizationId: string): Promise { +async function ensureDefaultCharacter( + userId: string, + organizationId: string, +): Promise { if (!userId || userId.trim() === "") { - logger.warn("[PrivySync] Invalid userId, skipping default character creation"); + logger.warn( + "[PrivySync] Invalid userId, skipping default character creation", + ); return; } @@ -794,9 +894,14 @@ async function ensureDefaultCharacter(userId: string, organizationId: string): P organization_id: organizationId, }); - logger.info(`[PrivySync] Created default Eliza character for user ${userId}`); + logger.info( + `[PrivySync] Created default Eliza character for user ${userId}`, + ); } catch (error) { - console.error(`[PrivySync] Error creating default character for user ${userId}:`, error); + console.error( + `[PrivySync] Error creating default character for user ${userId}:`, + error, + ); } } diff --git a/packages/lib/promotion-pricing.ts b/packages/lib/promotion-pricing.ts index 6e640f7d6..da3745389 100644 --- a/packages/lib/promotion-pricing.ts +++ b/packages/lib/promotion-pricing.ts @@ -74,7 +74,9 @@ export function formatCost(cost: number): string { } /** Get post cost by platform (internal billing, not shown to users) */ -export function getPostCost(platform: "discord" | "telegram" | "twitter"): number { +export function getPostCost( + platform: "discord" | "telegram" | "twitter", +): number { switch (platform) { case "discord": return DISCORD_POST_COST; diff --git a/packages/lib/prompts/index.ts b/packages/lib/prompts/index.ts index 0c54608d1..bee8ee8b1 100644 --- a/packages/lib/prompts/index.ts +++ b/packages/lib/prompts/index.ts @@ -9,7 +9,11 @@ import { BASE_SYSTEM_PROMPT } from "./base"; import { DATABASE_SECURITY_RULES, DATABASE_SETUP_PROMPT } from "./database"; import { BUILD_RULES, WORKFLOW_RULES } from "./rules"; import { SDK_REFERENCE, SDK_RESTRICTIONS } from "./sdk"; -import { TEMPLATE_EXAMPLES, TEMPLATE_PROMPTS, type TemplateType } from "./templates"; +import { + TEMPLATE_EXAMPLES, + TEMPLATE_PROMPTS, + type TemplateType, +} from "./templates"; export type { TemplateType }; @@ -72,7 +76,9 @@ export function buildSystemPrompt(config: PromptConfig = {}): string { /** * Get example prompts for a template type. */ -export function getExamplePrompts(templateType: TemplateType = "blank"): string[] { +export function getExamplePrompts( + templateType: TemplateType = "blank", +): string[] { return TEMPLATE_EXAMPLES[templateType] || TEMPLATE_EXAMPLES.blank; } diff --git a/packages/lib/prompts/templates.ts b/packages/lib/prompts/templates.ts index bd9541f77..7db2f8b77 100644 --- a/packages/lib/prompts/templates.ts +++ b/packages/lib/prompts/templates.ts @@ -182,7 +182,12 @@ export const TEMPLATE_EXAMPLES: Record = { "Create pricing table", "Add contact form", ], - analytics: ["Add KPI cards", "Create trend chart", "Add date picker", "Add data export"], + analytics: [ + "Add KPI cards", + "Create trend chart", + "Add date picker", + "Add data export", + ], "saas-starter": [ "Create dashboard layout", "Add billing page", @@ -201,6 +206,16 @@ export const TEMPLATE_EXAMPLES: Record = { "Implement search", "Add prompts", ], - "a2a-agent": ["Create agent card", "Add task handler", "Add discovery", "Create router"], - blank: ["Create dashboard", "Add navigation", "Create data table", "Add theme toggle"], + "a2a-agent": [ + "Create agent card", + "Add task handler", + "Add discovery", + "Create router", + ], + blank: [ + "Create dashboard", + "Add navigation", + "Create data table", + "Add theme toggle", + ], }; diff --git a/packages/lib/providers/CreditsProvider.tsx b/packages/lib/providers/CreditsProvider.tsx index dad2427a6..78fdd669d 100644 --- a/packages/lib/providers/CreditsProvider.tsx +++ b/packages/lib/providers/CreditsProvider.tsx @@ -163,7 +163,9 @@ export function CreditsProvider({ children }: { children: ReactNode }) { authErrorCountRef.current++; if (authErrorCountRef.current >= MAX_AUTH_ERRORS) { - logger.warn("[CreditsProvider] Too many auth errors after refresh, logging out"); + logger.warn( + "[CreditsProvider] Too many auth errors after refresh, logging out", + ); // Set logout flag BEFORE stopping polling to prevent race conditions isLoggingOutRef.current = true; stopPolling(); @@ -256,7 +258,12 @@ export function CreditsProvider({ children }: { children: ReactNode }) { useEffect(() => { isMountedRef.current = true; - if (ready && authenticated && !isPollingPausedRef.current && !shouldDeferAuthenticatedFetches) { + if ( + ready && + authenticated && + !isPollingPausedRef.current && + !shouldDeferAuthenticatedFetches + ) { // Defer initial fetch to avoid cascading renders queueMicrotask(() => { fetchBalance(); @@ -296,7 +303,9 @@ export function CreditsProvider({ children }: { children: ReactNode }) { [creditBalance, isConnected, isLoading, error, lastUpdate, fetchBalance], ); - return {children}; + return ( + {children} + ); } /** diff --git a/packages/lib/providers/PostHogProvider.tsx b/packages/lib/providers/PostHogProvider.tsx index 394d24f7c..0fc34cd90 100644 --- a/packages/lib/providers/PostHogProvider.tsx +++ b/packages/lib/providers/PostHogProvider.tsx @@ -75,11 +75,21 @@ function UserIdentifier(): null { username: user.discord.username ?? undefined, } : null, - github: user.github ? { username: user.github.username ?? undefined } : null, - wallet: user.wallet ? { address: user.wallet.address ?? undefined } : null, + github: user.github + ? { username: user.github.username ?? undefined } + : null, + wallet: user.wallet + ? { address: user.wallet.address ?? undefined } + : null, }; - const email = authInfo.email?.address ?? authInfo.google?.email ?? authInfo.discord?.email; - const name = authInfo.google?.name ?? authInfo.discord?.username ?? authInfo.github?.username; + const email = + authInfo.email?.address ?? + authInfo.google?.email ?? + authInfo.discord?.email; + const name = + authInfo.google?.name ?? + authInfo.discord?.username ?? + authInfo.github?.username; const method = getSignupMethod(authInfo); const isFirstLogin = previousAuthState.current === false; @@ -131,7 +141,9 @@ interface PostHogProviderProps { children: React.ReactNode; } -export function PostHogProvider({ children }: PostHogProviderProps): React.ReactElement { +export function PostHogProvider({ + children, +}: PostHogProviderProps): React.ReactElement { useEffect(() => { initPostHog(); }, []); diff --git a/packages/lib/providers/PrivyProvider.tsx b/packages/lib/providers/PrivyProvider.tsx index 7266431c4..d9182308c 100644 --- a/packages/lib/providers/PrivyProvider.tsx +++ b/packages/lib/providers/PrivyProvider.tsx @@ -27,7 +27,10 @@ type SolanaConnectors = ReturnType; // Create Solana wallet connectors once globally to prevent // WalletConnect double-initialization in React Strict Mode and during HMR const getSolanaConnectors = (): SolanaConnectors => { - const globalCache = globalThis as unknown as Record; + const globalCache = globalThis as unknown as Record< + string, + SolanaConnectors | undefined + >; if (globalCache[SOLANA_CONNECTORS_KEY]) { return globalCache[SOLANA_CONNECTORS_KEY]; @@ -139,7 +142,11 @@ function PrivyAuthWrapper({ children }: { children: React.ReactNode }) { return children; } -export default function PrivyProvider({ children }: { children: React.ReactNode }) { +export default function PrivyProvider({ + children, +}: { + children: React.ReactNode; +}) { const hasLoggedPrivyConfigError = useRef(false); // Memoize the config to prevent unnecessary re-renders (must be before early return) @@ -160,7 +167,13 @@ export default function PrivyProvider({ children }: { children: React.ReactNode walletChainType: "ethereum-and-solana", theme: "dark", accentColor: "#6366F1", - walletList: ["metamask", "phantom", "coinbase_wallet", "rabby_wallet", "okx_wallet"], + walletList: [ + "metamask", + "phantom", + "coinbase_wallet", + "rabby_wallet", + "okx_wallet", + ], }, externalWallets: { solana: { @@ -175,12 +188,19 @@ export default function PrivyProvider({ children }: { children: React.ReactNode // Check if Privy App ID is configured const appId = process.env.NEXT_PUBLIC_PRIVY_APP_ID; const clientId = process.env.NEXT_PUBLIC_PRIVY_CLIENT_ID; - const playwrightTestAuthEnabled = process.env.NEXT_PUBLIC_PLAYWRIGHT_TEST_AUTH === "true"; - const stewardAuthEnabled = process.env.NEXT_PUBLIC_STEWARD_AUTH_ENABLED === "true"; - const resolvedClientId = isPlaceholderPrivyValue(clientId) ? undefined : clientId?.trim(); + const playwrightTestAuthEnabled = + process.env.NEXT_PUBLIC_PLAYWRIGHT_TEST_AUTH === "true"; + const stewardAuthEnabled = + process.env.NEXT_PUBLIC_STEWARD_AUTH_ENABLED === "true"; + const resolvedClientId = isPlaceholderPrivyValue(clientId) + ? undefined + : clientId?.trim(); const hasValidAppId = - typeof appId === "string" && appId.trim().length === 25 && !isPlaceholderPrivyValue(appId); - const shouldUseFallbackPrivyContext = stewardAuthEnabled || playwrightTestAuthEnabled; + typeof appId === "string" && + appId.trim().length === 25 && + !isPlaceholderPrivyValue(appId); + const shouldUseFallbackPrivyContext = + stewardAuthEnabled || playwrightTestAuthEnabled; useEffect(() => { if ( @@ -204,7 +224,10 @@ export default function PrivyProvider({ children }: { children: React.ReactNode // simply report unauthenticated. if (shouldUseFallbackPrivyContext) { return ( - + {children} ); @@ -212,7 +235,9 @@ export default function PrivyProvider({ children }: { children: React.ReactNode return (
    -

    Configuration Error

    +

    + Configuration Error +

    Privy configuration is missing.

    Please set NEXT_PUBLIC_PRIVY_APP_ID in your environment variables. @@ -223,7 +248,11 @@ export default function PrivyProvider({ children }: { children: React.ReactNode } return ( - + {children} ); diff --git a/packages/lib/providers/StewardProvider.tsx b/packages/lib/providers/StewardProvider.tsx index ef11e71b5..9ee9b1b4f 100644 --- a/packages/lib/providers/StewardProvider.tsx +++ b/packages/lib/providers/StewardProvider.tsx @@ -52,7 +52,9 @@ function AuthTokenSync({ children }: { children: React.ReactNode }) { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), - }).catch((err) => console.warn("[steward] Failed to set session cookie", err)); + }).catch((err) => + console.warn("[steward] Failed to set session cookie", err), + ); // Dispatch a custom event so non-React code (fetch wrappers, etc.) // can pick up the fresh JWT without coupling to React context. @@ -67,10 +69,15 @@ function AuthTokenSync({ children }: { children: React.ReactNode }) { return children; } -export function StewardAuthProvider({ children }: { children: React.ReactNode }) { +export function StewardAuthProvider({ + children, +}: { + children: React.ReactNode; +}) { const hasLoggedConfigError = useRef(false); - const apiUrl = process.env.NEXT_PUBLIC_STEWARD_API_URL ?? "http://localhost:3200"; + const apiUrl = + process.env.NEXT_PUBLIC_STEWARD_API_URL ?? "http://localhost:3200"; const tenantId = process.env.NEXT_PUBLIC_STEWARD_TENANT_ID; const hasValidUrl = !isPlaceholderValue(apiUrl); @@ -85,7 +92,12 @@ export function StewardAuthProvider({ children }: { children: React.ReactNode }) ); useEffect(() => { - if (typeof window === "undefined" || hasValidUrl || hasLoggedConfigError.current) return; + if ( + typeof window === "undefined" || + hasValidUrl || + hasLoggedConfigError.current + ) + return; hasLoggedConfigError.current = true; console.error( "NEXT_PUBLIC_STEWARD_API_URL is missing or invalid! Steward auth will not function.", @@ -103,7 +115,9 @@ export function StewardAuthProvider({ children }: { children: React.ReactNode }) client={client} agentId="eliza-cloud" auth={{ baseUrl: apiUrl }} - tenantId={tenantId && !isPlaceholderValue(tenantId) ? tenantId : undefined} + tenantId={ + tenantId && !isPlaceholderValue(tenantId) ? tenantId : undefined + } > {children} diff --git a/packages/lib/providers/anthropic-thinking.ts b/packages/lib/providers/anthropic-thinking.ts index b1e072fb4..95be85610 100644 --- a/packages/lib/providers/anthropic-thinking.ts +++ b/packages/lib/providers/anthropic-thinking.ts @@ -19,7 +19,10 @@ import type { AnthropicProviderOptions } from "@ai-sdk/anthropic"; import { getProviderFromModel } from "@/lib/pricing"; -import type { CloudJsonObject, CloudMergedProviderOptions } from "./cloud-provider-options"; +import type { + CloudJsonObject, + CloudMergedProviderOptions, +} from "./cloud-provider-options"; /** * Models that support Anthropic extended thinking. @@ -41,14 +44,17 @@ const EXTENDED_THINKING_MODEL_PATTERNS = [ */ export function supportsExtendedThinking(modelId: string): boolean { const normalizedId = modelId.toLowerCase(); - return EXTENDED_THINKING_MODEL_PATTERNS.some((pattern) => pattern.test(normalizedId)); + return EXTENDED_THINKING_MODEL_PATTERNS.some((pattern) => + pattern.test(normalizedId), + ); } const ENV_KEY = "ANTHROPIC_COT_BUDGET"; const ENV_MAX_KEY = "ANTHROPIC_COT_BUDGET_MAX"; /** `user_characters.settings` key for per-agent thinking token budget (integer ≥ 0). */ -export const ANTHROPIC_THINKING_BUDGET_CHARACTER_SETTINGS_KEY = "anthropicThinkingBudgetTokens"; +export const ANTHROPIC_THINKING_BUDGET_CHARACTER_SETTINGS_KEY = + "anthropicThinkingBudgetTokens"; /** Subset of env used for tests and callers that only pass a few keys. */ export type AnthropicCotEnv = Record; @@ -78,7 +84,9 @@ function parsePositiveIntStrict(raw: string, keyLabel: string): number { * - "0" or negative as string not possible with strict digit regex; 0 from digits → null * - invalid non-empty → throws */ -export function parseAnthropicCotBudgetFromEnv(env: AnthropicCotEnv = process.env): number | null { +export function parseAnthropicCotBudgetFromEnv( + env: AnthropicCotEnv = process.env, +): number | null { const raw = env[ENV_KEY]; if (raw === undefined || raw === "") { return null; @@ -171,7 +179,9 @@ export function resolveAnthropicThinkingBudgetTokens( return base; } -const anthropicThinkingOptions = (budgetTokens: number): AnthropicProviderOptions => ({ +const anthropicThinkingOptions = ( + budgetTokens: number, +): AnthropicProviderOptions => ({ thinking: { type: "enabled", budgetTokens }, }); @@ -186,7 +196,11 @@ export function anthropicThinkingProviderOptions( env: AnthropicCotEnv = process.env, agentThinkingBudgetTokens?: number, ): { providerOptions: CloudMergedProviderOptions } | Record { - const budget = resolveAnthropicThinkingBudgetTokens(modelId, env, agentThinkingBudgetTokens); + const budget = resolveAnthropicThinkingBudgetTokens( + modelId, + env, + agentThinkingBudgetTokens, + ); if (budget === null) { return {}; } @@ -242,7 +256,9 @@ export function mergeAnthropicCotProviderOptions( ); } -const GOOGLE_IMAGE_MODALITIES: CloudJsonObject = { responseModalities: ["TEXT", "IMAGE"] }; +const GOOGLE_IMAGE_MODALITIES: CloudJsonObject = { + responseModalities: ["TEXT", "IMAGE"], +}; /** * Gemini (and similar) image generation: `google.responseModalities` plus optional COT merge. diff --git a/packages/lib/providers/anthropic-web-search.ts b/packages/lib/providers/anthropic-web-search.ts index fbaa35d7f..9e96370c1 100644 --- a/packages/lib/providers/anthropic-web-search.ts +++ b/packages/lib/providers/anthropic-web-search.ts @@ -1,6 +1,9 @@ import { anthropic as anthropicProvider } from "@ai-sdk/anthropic"; -const SUPPORTED_ANTHROPIC_WEB_SEARCH_MODELS = ["claude-sonnet-4-6", "claude-opus-4-6"] as const; +const SUPPORTED_ANTHROPIC_WEB_SEARCH_MODELS = [ + "claude-sonnet-4-6", + "claude-opus-4-6", +] as const; const MAX_ANTHROPIC_WEB_SEARCH_MAX_USES = 10; @@ -16,7 +19,8 @@ export function supportsAnthropicWebSearch(model: string): boolean { const normalized = normalizeModelName(model); return SUPPORTED_ANTHROPIC_WEB_SEARCH_MODELS.some( (supportedModel) => - normalized === supportedModel || normalized.startsWith(`${supportedModel}-`), + normalized === supportedModel || + normalized.startsWith(`${supportedModel}-`), ); } @@ -25,7 +29,9 @@ export function isAnthropicWebSearchEnabled( model: string, enabled: boolean, ): boolean { - return enabled && provider === "anthropic" && supportsAnthropicWebSearch(model); + return ( + enabled && provider === "anthropic" && supportsAnthropicWebSearch(model) + ); } function resolveWebSearchMaxUses(maxUses: number | undefined): number { @@ -33,7 +39,10 @@ function resolveWebSearchMaxUses(maxUses: number | undefined): number { return DEFAULT_ANTHROPIC_WEB_SEARCH_MAX_USES; } - return Math.min(Math.max(Math.trunc(maxUses), 1), MAX_ANTHROPIC_WEB_SEARCH_MAX_USES); + return Math.min( + Math.max(Math.trunc(maxUses), 1), + MAX_ANTHROPIC_WEB_SEARCH_MAX_USES, + ); } export function buildProviderNativeWebSearchTools({ @@ -47,7 +56,12 @@ export function buildProviderNativeWebSearchTools({ enabled: boolean; maxUses?: number; }): - | { tools: Record> } + | { + tools: Record< + string, + ReturnType + >; + } | Record { if (!isAnthropicWebSearchEnabled(provider, model, enabled)) { return {}; diff --git a/packages/lib/providers/cloud-provider-options.ts b/packages/lib/providers/cloud-provider-options.ts index 8523cf82c..a0edcfbbd 100644 --- a/packages/lib/providers/cloud-provider-options.ts +++ b/packages/lib/providers/cloud-provider-options.ts @@ -1,4 +1,10 @@ -export type CloudJsonValue = null | string | number | boolean | CloudJsonObject | CloudJsonValue[]; +export type CloudJsonValue = + | null + | string + | number + | boolean + | CloudJsonObject + | CloudJsonValue[]; export type CloudJsonObject = { [key: string]: CloudJsonValue | undefined; diff --git a/packages/lib/providers/failover.ts b/packages/lib/providers/failover.ts index a217c3802..9392c1d38 100644 --- a/packages/lib/providers/failover.ts +++ b/packages/lib/providers/failover.ts @@ -33,7 +33,10 @@ export async function withProviderFallback( } catch (error) { if (fallbackFn && isRetryableProviderError(error)) { const status = (error as { status: number }).status; - logger.warn("[Provider Failover] Primary provider returned %d, trying fallback", status); + logger.warn( + "[Provider Failover] Primary provider returned %d, trying fallback", + status, + ); return await fallbackFn(); } throw error; diff --git a/packages/lib/providers/index.ts b/packages/lib/providers/index.ts index 6dcaaa4cd..d242b30e6 100644 --- a/packages/lib/providers/index.ts +++ b/packages/lib/providers/index.ts @@ -33,7 +33,8 @@ let openRouterProviderInstance: AIProvider | null = null; */ export function getProvider(): AIProvider { if (!providerInstance) { - const apiKey = process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_AI_GATEWAY_API_KEY; + const apiKey = + process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_AI_GATEWAY_API_KEY; if (!apiKey) { throw new Error( "AI_GATEWAY_API_KEY or VERCEL_AI_GATEWAY_API_KEY environment variable is required", @@ -77,7 +78,9 @@ export function getOpenRouterProvider(): AIProvider { } function hasGatewayConfigured(): boolean { - return Boolean(process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_AI_GATEWAY_API_KEY); + return Boolean( + process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_AI_GATEWAY_API_KEY, + ); } export function getProviderForModel(model: string): AIProvider { diff --git a/packages/lib/providers/language-model.ts b/packages/lib/providers/language-model.ts index 11e403435..11eae4951 100644 --- a/packages/lib/providers/language-model.ts +++ b/packages/lib/providers/language-model.ts @@ -7,7 +7,11 @@ let openAIClient: ReturnType | null = null; let openRouterClient: ReturnType | null = null; function getGatewayApiKey(): string | null { - return process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_AI_GATEWAY_API_KEY || null; + return ( + process.env.AI_GATEWAY_API_KEY || + process.env.VERCEL_AI_GATEWAY_API_KEY || + null + ); } function getGroqClient() { @@ -79,11 +83,15 @@ export function hasLanguageModelProviderConfigured(model: string): boolean { return Boolean(process.env.GROQ_API_KEY); } - return Boolean(getGatewayApiKey() || getOpenRouterApiKey() || process.env.OPENAI_API_KEY); + return Boolean( + getGatewayApiKey() || getOpenRouterApiKey() || process.env.OPENAI_API_KEY, + ); } export function hasTextEmbeddingProviderConfigured(): boolean { - return Boolean(getGatewayApiKey() || getOpenRouterApiKey() || process.env.OPENAI_API_KEY); + return Boolean( + getGatewayApiKey() || getOpenRouterApiKey() || process.env.OPENAI_API_KEY, + ); } export function getLanguageModel(model: string) { @@ -168,7 +176,11 @@ export function resolveAiProviderSource( return null; } -export function resolveEmbeddingProviderSource(): "gateway" | "openrouter" | "openai" | null { +export function resolveEmbeddingProviderSource(): + | "gateway" + | "openrouter" + | "openai" + | null { if (getGatewayApiKey()) { return "gateway"; } diff --git a/packages/lib/providers/types.ts b/packages/lib/providers/types.ts index 52d12c575..618a9763b 100644 --- a/packages/lib/providers/types.ts +++ b/packages/lib/providers/types.ts @@ -168,7 +168,10 @@ export interface OpenAIModelsResponse { */ export interface AIProvider { name: string; - chatCompletions(request: OpenAIChatRequest, options?: ProviderRequestOptions): Promise; + chatCompletions( + request: OpenAIChatRequest, + options?: ProviderRequestOptions, + ): Promise; /** * Native OpenAI Responses API passthrough. * @@ -183,7 +186,10 @@ export interface AIProvider { * an `unsupported_provider` error when absent — the route does this * explicitly rather than relying on a throwing implementation. */ - responses?(body: unknown, options?: ProviderRequestOptions): Promise; + responses?( + body: unknown, + options?: ProviderRequestOptions, + ): Promise; embeddings(request: OpenAIEmbeddingsRequest): Promise; listModels(): Promise; getModel(model: string): Promise; diff --git a/packages/lib/providers/vercel-gateway.ts b/packages/lib/providers/vercel-gateway.ts index 79a2ae974..095d93baf 100644 --- a/packages/lib/providers/vercel-gateway.ts +++ b/packages/lib/providers/vercel-gateway.ts @@ -154,14 +154,18 @@ export class VercelGatewayProvider implements AIProvider { ); } - async responses(body: unknown, options?: ProviderRequestOptions): Promise { + async responses( + body: unknown, + options?: ProviderRequestOptions, + ): Promise { // Forward the raw Responses API body to the Vercel AI Gateway // `/responses` passthrough. We do not inspect or transform the body — // that is the whole point of this path: gpt-5.x clients (Codex CLI, // AI SDK Responses transport) send shapes the Chat Completions API // does not accept (flat tools, `type: "custom"` tools, `web_search`, // `image_generation`, etc.) and must reach the upstream intact. - const bodyRecord = body && typeof body === "object" ? (body as Record) : {}; + const bodyRecord = + body && typeof body === "object" ? (body as Record) : {}; logger.debug("[Vercel Gateway] Forwarding responses request", { model: bodyRecord.model, streaming: bodyRecord.stream, diff --git a/packages/lib/security/origin-validation.ts b/packages/lib/security/origin-validation.ts index f55459b2a..f1a59a540 100644 --- a/packages/lib/security/origin-validation.ts +++ b/packages/lib/security/origin-validation.ts @@ -41,7 +41,10 @@ export function normalizeOrigin(value: string): string | null { * allowed origins. Allowlist entries may be full URLs, origins, or wildcard * origins like https://*.example.com. */ -export function isAllowedOrigin(allowedOrigins: string[], candidate: string): boolean { +export function isAllowedOrigin( + allowedOrigins: string[], + candidate: string, +): boolean { const candidateOrigin = normalizeOrigin(candidate); if (!candidateOrigin) { return false; @@ -57,7 +60,10 @@ export function isAllowedOrigin(allowedOrigins: string[], candidate: string): bo if (trimmed.includes("*")) { const pattern = stripPath(trimmed); - const regex = new RegExp(`^${escapeRegex(pattern).replace(/\\\*/g, ".*")}$`, "i"); + const regex = new RegExp( + `^${escapeRegex(pattern).replace(/\\\*/g, ".*")}$`, + "i", + ); if (regex.test(candidateOrigin)) { return true; } diff --git a/packages/lib/security/outbound-url.ts b/packages/lib/security/outbound-url.ts index f68a721bc..259e3af65 100644 --- a/packages/lib/security/outbound-url.ts +++ b/packages/lib/security/outbound-url.ts @@ -104,7 +104,11 @@ function validateUrlSyntax(rawUrl: string): URL { throw new Error("URL is missing a hostname"); } - if (hostname === "localhost" || hostname === "0.0.0.0" || hostname.endsWith(".localhost")) { + if ( + hostname === "localhost" || + hostname === "0.0.0.0" || + hostname.endsWith(".localhost") + ) { throw new Error("Localhost destinations are not allowed"); } @@ -138,7 +142,9 @@ export async function assertSafeOutboundUrl(rawUrl: string): Promise { for (const record of records) { if (isForbiddenIpAddress(record.address)) { - throw new Error("Endpoint resolves to a private or reserved IP address"); + throw new Error( + "Endpoint resolves to a private or reserved IP address", + ); } } } diff --git a/packages/lib/security/redirect-validation.ts b/packages/lib/security/redirect-validation.ts index 5086621f7..450da92af 100644 --- a/packages/lib/security/redirect-validation.ts +++ b/packages/lib/security/redirect-validation.ts @@ -35,9 +35,10 @@ function hasEmbeddedCredentials(url: URL): boolean { } export function getDefaultPlatformRedirectOrigins(): string[] { - return [process.env.NEXT_PUBLIC_APP_URL?.trim(), ...DEFAULT_PLATFORM_REDIRECT_ORIGINS].filter( - (value): value is string => !!value, - ); + return [ + process.env.NEXT_PUBLIC_APP_URL?.trim(), + ...DEFAULT_PLATFORM_REDIRECT_ORIGINS, + ].filter((value): value is string => !!value); } export function isSafeRelativeRedirectPath(value: string): boolean { @@ -55,7 +56,10 @@ export function sanitizeRelativeRedirectPath( return isSafeRelativeRedirectPath(value) ? value : fallbackPath; } -export function isAllowedAbsoluteRedirectUrl(value: string, allowedOrigins: string[]): boolean { +export function isAllowedAbsoluteRedirectUrl( + value: string, + allowedOrigins: string[], +): boolean { try { const parsed = new URL(value); if (!isHttpUrl(parsed) || hasEmbeddedCredentials(parsed)) { @@ -99,7 +103,11 @@ export function resolveSafeRedirectTarget( const parsed = new URL(value); const base = new URL(baseUrl); - if (isHttpUrl(parsed) && !hasEmbeddedCredentials(parsed) && parsed.origin === base.origin) { + if ( + isHttpUrl(parsed) && + !hasEmbeddedCredentials(parsed) && + parsed.origin === base.origin + ) { return parsed; } } catch { diff --git a/packages/lib/seo/constants.ts b/packages/lib/seo/constants.ts index 304d97bbf..434d68e6a 100644 --- a/packages/lib/seo/constants.ts +++ b/packages/lib/seo/constants.ts @@ -35,7 +35,13 @@ export const ROUTE_METADATA = { title: "Eliza Cloud - Managed Hosting for AI Agents", description: "Managed hosting, provisioning, billing, and deployment for AI agents on Eliza Cloud.", - keywords: ["AI platform", "agent development", "elizaOS", "AI hosting", "LLM deployment"], + keywords: [ + "AI platform", + "agent development", + "elizaOS", + "AI hosting", + "LLM deployment", + ], }, dashboard: { title: "Dashboard", @@ -47,7 +53,13 @@ export const ROUTE_METADATA = { title: "Containers", description: "Deploy and manage elizaOS containers on AWS ECS. Monitor health, view logs, and scale your deployments.", - keywords: ["containers", "deployment", "AWS ECS", "Docker", "elizaOS deploy"], + keywords: [ + "containers", + "deployment", + "AWS ECS", + "Docker", + "elizaOS deploy", + ], }, eliza: { title: "Chat", @@ -59,13 +71,23 @@ export const ROUTE_METADATA = { title: "Character Creator", description: "Create custom AI characters with our AI-assisted builder. Define personality, knowledge, and behaviors for your agents.", - keywords: ["character creator", "AI characters", "agent builder", "elizaOS characters"], + keywords: [ + "character creator", + "AI characters", + "agent builder", + "elizaOS characters", + ], }, myAgents: { title: "My Agents", description: "Manage and interact with your personal AI agents. View, deploy, and chat with your characters.", - keywords: ["my agents", "personal agents", "AI characters", "agent management"], + keywords: [ + "my agents", + "personal agents", + "AI characters", + "agent management", + ], }, textGeneration: { title: "Text Generation", @@ -89,7 +111,13 @@ export const ROUTE_METADATA = { title: "Voice Cloning", description: "Clone voices with ElevenLabs integration. Create custom voices for your AI agents.", - keywords: ["voice cloning", "ElevenLabs", "voice AI", "TTS", "voice synthesis"], + keywords: [ + "voice cloning", + "ElevenLabs", + "voice AI", + "TTS", + "voice synthesis", + ], }, apiExplorer: { title: "API Explorer", @@ -105,7 +133,8 @@ export const ROUTE_METADATA = { }, apiKeys: { title: "API Keys", - description: "Generate and manage API keys for programmatic access to Eliza Cloud.", + description: + "Generate and manage API keys for programmatic access to Eliza Cloud.", keywords: ["API keys", "authentication", "API access", "tokens"], }, analytics: { @@ -128,7 +157,8 @@ export const ROUTE_METADATA = { }, account: { title: "Account Settings", - description: "Manage your account settings, profile, and preferences on Eliza Cloud.", + description: + "Manage your account settings, profile, and preferences on Eliza Cloud.", keywords: ["account", "settings", "profile", "preferences"], }, } as const; diff --git a/packages/lib/seo/environment.ts b/packages/lib/seo/environment.ts index c54561fd4..5187dd2f3 100644 --- a/packages/lib/seo/environment.ts +++ b/packages/lib/seo/environment.ts @@ -7,12 +7,16 @@ function normalizeHost(host: string): string { return host.trim().toLowerCase(); } -export function getIndexableHosts(env: NodeJS.ProcessEnv = process.env): string[] { +export function getIndexableHosts( + env: NodeJS.ProcessEnv = process.env, +): string[] { const configuredHosts = env.NEXT_PUBLIC_INDEXABLE_HOSTS?.split(",") .map((host) => normalizeHost(host)) .filter(Boolean); - return configuredHosts?.length ? configuredHosts : [...DEFAULT_INDEXABLE_HOSTS]; + return configuredHosts?.length + ? configuredHosts + : [...DEFAULT_INDEXABLE_HOSTS]; } export function shouldIndexSite(env: NodeJS.ProcessEnv = process.env): boolean { @@ -39,7 +43,9 @@ export function getRobotsMetadata( }; } -export function generateRobotsFile(env: NodeJS.ProcessEnv = process.env): MetadataRoute.Robots { +export function generateRobotsFile( + env: NodeJS.ProcessEnv = process.env, +): MetadataRoute.Robots { const index = shouldIndexSite(env); const appUrl = getAppUrl(env); diff --git a/packages/lib/seo/metadata.ts b/packages/lib/seo/metadata.ts index 157d00923..44ce13591 100644 --- a/packages/lib/seo/metadata.ts +++ b/packages/lib/seo/metadata.ts @@ -6,7 +6,11 @@ import type { Metadata } from "next"; import { getAppUrl } from "@/lib/utils/app-url"; import { SEO_CONSTANTS } from "./constants"; import { getRobotsMetadata } from "./environment"; -import type { DynamicMetadataOptions, OGImageParams, PageMetadataOptions } from "./types"; +import type { + DynamicMetadataOptions, + OGImageParams, + PageMetadataOptions, +} from "./types"; /** * Generates an Open Graph image URL. @@ -45,7 +49,9 @@ export function generatePageMetadata(options: PageMetadataOptions): Metadata { const metadata: Metadata = { title: options.title, description: options.description, - keywords: options.keywords ? [...options.keywords] : [...SEO_CONSTANTS.defaultKeywords], + keywords: options.keywords + ? [...options.keywords] + : [...SEO_CONSTANTS.defaultKeywords], alternates: { canonical: canonicalUrl, }, @@ -90,7 +96,9 @@ export function generatePageMetadata(options: PageMetadataOptions): Metadata { * @param options - Dynamic metadata options with entity information. * @returns Next.js Metadata object. */ -export function generateDynamicMetadata(options: DynamicMetadataOptions): Metadata { +export function generateDynamicMetadata( + options: DynamicMetadataOptions, +): Metadata { const baseMetadata = generatePageMetadata(options); if (options.type === "article" && options.updatedAt) { @@ -134,7 +142,12 @@ export function generateContainerMetadata( return generateDynamicMetadata({ title, description: desc, - keywords: ["container", "deployment", name, ...(characterName ? [characterName] : [])], + keywords: [ + "container", + "deployment", + name, + ...(characterName ? [characterName] : []), + ], path: `/dashboard/containers/${id}`, ogImage: "/og-image.png", entityId: id, diff --git a/packages/lib/seo/schema.ts b/packages/lib/seo/schema.ts index aecdee4b5..7fad0dfdb 100644 --- a/packages/lib/seo/schema.ts +++ b/packages/lib/seo/schema.ts @@ -155,7 +155,9 @@ export function generateArticleSchema( * @param items - Array of breadcrumb items with name and URL. * @returns BreadcrumbList structured data object. */ -export function generateBreadcrumbSchema(items: Array<{ name: string; url: string }>) { +export function generateBreadcrumbSchema( + items: Array<{ name: string; url: string }>, +) { const baseUrl = getAppUrl(); return { @@ -176,7 +178,9 @@ export function generateBreadcrumbSchema(items: Array<{ name: string; url: strin * @param options - Structured data options. * @returns Structured data object matching the specified type. */ -export function generateStructuredData(options: StructuredDataOptions): Record { +export function generateStructuredData( + options: StructuredDataOptions, +): Record { const baseUrl = getAppUrl(); switch (options.type) { diff --git a/packages/lib/seo/types.ts b/packages/lib/seo/types.ts index 1f4c23dbe..52ff55084 100644 --- a/packages/lib/seo/types.ts +++ b/packages/lib/seo/types.ts @@ -44,7 +44,12 @@ export interface DynamicMetadataOptions extends PageMetadataOptions { * Options for generating structured data (JSON-LD). */ export interface StructuredDataOptions { - type: "Organization" | "WebApplication" | "Product" | "Article" | "SoftwareApplication"; + type: + | "Organization" + | "WebApplication" + | "Product" + | "Article" + | "SoftwareApplication"; name: string; description?: string; url?: string; @@ -55,4 +60,6 @@ export interface StructuredDataOptions { /** * Function type for generating metadata. */ -export type MetadataGenerator = (options: PageMetadataOptions | DynamicMetadataOptions) => Metadata; +export type MetadataGenerator = ( + options: PageMetadataOptions | DynamicMetadataOptions, +) => Metadata; diff --git a/packages/lib/services/a2a-task-store.ts b/packages/lib/services/a2a-task-store.ts index c46372aba..a4eb3d161 100644 --- a/packages/lib/services/a2a-task-store.ts +++ b/packages/lib/services/a2a-task-store.ts @@ -52,7 +52,9 @@ function getRedisClient(): Redis { if (redisUrl) { redis = Redis.fromEnv(); - logger.info("[A2A TaskStore] ✓ Redis task store initialized (native protocol)"); + logger.info( + "[A2A TaskStore] ✓ Redis task store initialized (native protocol)", + ); } else if (restUrl && restToken) { redis = new Redis({ url: restUrl, token: restToken }); logger.info("[A2A TaskStore] ✓ Redis task store initialized (REST API)"); @@ -75,13 +77,17 @@ class A2ATaskStoreService { /** * Get a task by ID */ - async get(taskId: string, organizationId: string): Promise { + async get( + taskId: string, + organizationId: string, + ): Promise { const client = getRedisClient(); const key = `${TASK_KEY_PREFIX}${taskId}`; const value = await client.get(key); if (!value) return null; - const entry: TaskStoreEntry = typeof value === "string" ? JSON.parse(value) : value; + const entry: TaskStoreEntry = + typeof value === "string" ? JSON.parse(value) : value; // Verify organization access if (entry.organizationId !== organizationId) { @@ -165,7 +171,10 @@ class A2ATaskStoreService { /** * List tasks for an organization */ - async listByOrganization(organizationId: string, limit = 50): Promise { + async listByOrganization( + organizationId: string, + limit = 50, + ): Promise { const client = getRedisClient(); const orgIndexKey = `${TASK_ORG_INDEX_PREFIX}${organizationId}`; @@ -181,7 +190,8 @@ class A2ATaskStoreService { const entries: TaskStoreEntry[] = []; for (const value of values) { if (value) { - const entry: TaskStoreEntry = typeof value === "string" ? JSON.parse(value) : value; + const entry: TaskStoreEntry = + typeof value === "string" ? JSON.parse(value) : value; entries.push(entry); } } diff --git a/packages/lib/services/abuse-detection.ts b/packages/lib/services/abuse-detection.ts index 37ac8e555..8e1d1f686 100644 --- a/packages/lib/services/abuse-detection.ts +++ b/packages/lib/services/abuse-detection.ts @@ -74,7 +74,9 @@ class AbuseDetectionService { if (!allowed) { logger.warn("[AbuseDetection] Signup blocked", { context: { - email: context.email ? `${context.email.slice(0, 3)}***` : undefined, + email: context.email + ? `${context.email.slice(0, 3)}***` + : undefined, ipAddress: context.ipAddress, hasFingerprint: !!context.fingerprint, }, @@ -85,7 +87,9 @@ class AbuseDetectionService { return { allowed, - reason: allowed ? undefined : `Suspicious activity detected: ${flags.join(", ")}`, + reason: allowed + ? undefined + : `Suspicious activity detected: ${flags.join(", ")}`, riskScore, flags, }; @@ -104,7 +108,9 @@ class AbuseDetectionService { } } - private async checkEmailAbuse(email: string): Promise<{ flags: string[]; riskScore: number }> { + private async checkEmailAbuse( + email: string, + ): Promise<{ flags: string[]; riskScore: number }> { const flags: string[] = []; let riskScore = 0; @@ -133,7 +139,9 @@ class AbuseDetectionService { return { flags, riskScore }; } - private async checkIpAbuse(ipAddress: string): Promise<{ flags: string[]; riskScore: number }> { + private async checkIpAbuse( + ipAddress: string, + ): Promise<{ flags: string[]; riskScore: number }> { const flags: string[] = []; let riskScore = 0; @@ -199,7 +207,10 @@ class AbuseDetectionService { return { flags, riskScore }; } - async recordSignupMetadata(organizationId: string, context: SignupContext): Promise { + async recordSignupMetadata( + organizationId: string, + context: SignupContext, + ): Promise { try { const [org] = await dbRead .select({ settings: organizations.settings }) diff --git a/packages/lib/services/admin-infrastructure.ts b/packages/lib/services/admin-infrastructure.ts index 503559b7d..0c8e492d3 100644 --- a/packages/lib/services/admin-infrastructure.ts +++ b/packages/lib/services/admin-infrastructure.ts @@ -2,7 +2,10 @@ import { asc } from "drizzle-orm"; import { dbRead } from "@/db/helpers"; import { dockerNodesRepository } from "@/db/repositories/docker-nodes"; import type { DockerNodeStatus } from "@/db/schemas/docker-nodes"; -import { type MiladySandboxStatus, miladySandboxes } from "@/db/schemas/milady-sandboxes"; +import { + type MiladySandboxStatus, + miladySandboxes, +} from "@/db/schemas/milady-sandboxes"; import { DockerSSHClient } from "@/lib/services/docker-ssh"; import { logger } from "@/lib/utils/logger"; @@ -19,7 +22,10 @@ const MAX_CONCURRENT_SSH_SESSIONS = 5; const SNAPSHOT_CACHE_TTL_MS = 30_000; /** Simple concurrency limiter — runs at most `limit` tasks in parallel. */ -async function pLimit(tasks: Array<() => Promise>, limit: number): Promise { +async function pLimit( + tasks: Array<() => Promise>, + limit: number, +): Promise { const results: T[] = new Array(tasks.length); let nextIndex = 0; @@ -30,15 +36,24 @@ async function pLimit(tasks: Array<() => Promise>, limit: number): Promise } } - const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker()); + const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => + worker(), + ); await Promise.all(workers); return results; } /** Wraps a promise with a timeout — rejects with an error if the deadline expires. */ -function withTimeout(promise: Promise, ms: number, label: string): Promise { +function withTimeout( + promise: Promise, + ms: number, + label: string, +): Promise { return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + const timer = setTimeout( + () => reject(new Error(`${label} timed out after ${ms}ms`)), + ms, + ); promise.then( (value) => { clearTimeout(timer); @@ -56,7 +71,10 @@ function withTimeout(promise: Promise, ms: number, label: string): Promise * Best-effort in-process cache for repeated admin polling on the same warm instance. * Serverless cold starts will begin with an empty cache. */ -let snapshotCache: { data: AdminInfrastructureSnapshot; expiresAt: number } | null = null; +let snapshotCache: { + data: AdminInfrastructureSnapshot; + expiresAt: number; +} | null = null; type IncidentSeverity = "critical" | "warning" | "info"; type IncidentScope = "cluster" | "node" | "container"; @@ -203,7 +221,9 @@ export interface AdminInfrastructureSnapshot { function toIso(value: Date | string | null | undefined): string | null { if (!value) return null; - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + return value instanceof Date + ? value.toISOString() + : new Date(value).toISOString(); } function parsePercent(value: string): number | null { @@ -229,8 +249,14 @@ function parseRuntimeContainers(output: string): RuntimeContainerRecord[] { .map((line) => line.trim()) .filter(Boolean) .map((line) => { - const [name = "", id = "", image = "", state = "", status = "", runningFor = ""] = - line.split("|"); + const [ + name = "", + id = "", + image = "", + state = "", + status = "", + runningFor = "", + ] = line.split("|"); return { name, id, @@ -249,9 +275,12 @@ function parseDockerHealth(status: string): RuntimeContainerRecord["health"] { .toLowerCase() .replace(/^.*health:\s*/, "") .trim(); - if (normalized === "unhealthy" || normalized.startsWith("unhealthy")) return "unhealthy"; - if (normalized === "healthy" || normalized.startsWith("healthy")) return "healthy"; - if (normalized === "starting" || normalized.startsWith("starting")) return "starting"; + if (normalized === "unhealthy" || normalized.startsWith("unhealthy")) + return "unhealthy"; + if (normalized === "healthy" || normalized.startsWith("healthy")) + return "healthy"; + if (normalized === "starting" || normalized.startsWith("starting")) + return "starting"; // Fallback: check the full string for keywords (handles non-standard formats) const full = status.toLowerCase(); if (full.includes("unhealthy")) return "unhealthy"; @@ -267,21 +296,32 @@ function getHeartbeatAgeMinutes(lastHeartbeatAt: string | null): number | null { return Math.max(0, Math.round((Date.now() - parsed) / 60_000)); } -function buildResourceAlert(label: string, percent: number | null): string | null { +function buildResourceAlert( + label: string, + percent: number | null, +): string | null { if (percent === null) return null; - if (percent >= NODE_RESOURCE_CRITICAL_PCT) return `${label} critical: ${percent}% used`; - if (percent >= NODE_RESOURCE_WARNING_PCT) return `${label} warning: ${percent}% used`; + if (percent >= NODE_RESOURCE_CRITICAL_PCT) + return `${label} critical: ${percent}% used`; + if (percent >= NODE_RESOURCE_WARNING_PCT) + return `${label} warning: ${percent}% used`; return null; } -function sortIncidents(a: AdminInfrastructureIncident, b: AdminInfrastructureIncident): number { +function sortIncidents( + a: AdminInfrastructureIncident, + b: AdminInfrastructureIncident, +): number { const severityWeight: Record = { critical: 0, warning: 1, info: 2, }; - return severityWeight[a.severity] - severityWeight[b.severity] || a.title.localeCompare(b.title); + return ( + severityWeight[a.severity] - severityWeight[b.severity] || + a.title.localeCompare(b.title) + ); } export function classifyContainerHealth(params: { @@ -297,7 +337,9 @@ export function classifyContainerHealth(params: { return { status: "failed", severity: "critical", - reason: params.errorMessage || "Provisioning or runtime error recorded in control plane", + reason: + params.errorMessage || + "Provisioning or runtime error recorded in control plane", }; } @@ -329,7 +371,8 @@ export function classifyContainerHealth(params: { return { status: "degraded", severity: "warning", - reason: "Control plane says stopped but container still exists on the node", + reason: + "Control plane says stopped but container still exists on the node", }; } @@ -401,7 +444,8 @@ export function classifyContainerHealth(params: { // If the container is running and Docker health check says healthy, // downgrade severity — the heartbeat mechanism may be broken but the // container itself is functional. - const runtimeHealthy = runtime?.state === "running" && runtime?.health === "healthy"; + const runtimeHealthy = + runtime?.state === "running" && runtime?.health === "healthy"; return { status: "stale", severity: runtimeHealthy ? "warning" : "critical", @@ -444,18 +488,28 @@ async function inspectNodeRuntime(node: { await ssh.exec("echo ok", SSH_CONNECT_TIMEOUT_MS); const sshLatencyMs = Date.now() - sshStart; - const [dockerVersionRaw, diskRaw, memoryRaw, loadAverageRaw, containersRaw] = await Promise.all( - [ - ssh.exec("docker version --format '{{.Server.Version}}'", SSH_COMMAND_TIMEOUT_MS), - ssh.exec("df -P / | tail -1 | awk '{print $5}'", SSH_COMMAND_TIMEOUT_MS), - ssh.exec("free -b | awk '/Mem:/ {print $3\"|\"$2}'", SSH_COMMAND_TIMEOUT_MS), - ssh.exec("cut -d' ' -f1-3 /proc/loadavg", SSH_COMMAND_TIMEOUT_MS), - ssh.exec( - "docker ps -a --filter name=milady- --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.State}}|{{.Status}}|{{.RunningFor}}' 2>/dev/null || true", - SSH_COMMAND_TIMEOUT_MS, - ), - ], - ); + const [ + dockerVersionRaw, + diskRaw, + memoryRaw, + loadAverageRaw, + containersRaw, + ] = await Promise.all([ + ssh.exec( + "docker version --format '{{.Server.Version}}'", + SSH_COMMAND_TIMEOUT_MS, + ), + ssh.exec("df -P / | tail -1 | awk '{print $5}'", SSH_COMMAND_TIMEOUT_MS), + ssh.exec( + "free -b | awk '/Mem:/ {print $3\"|\"$2}'", + SSH_COMMAND_TIMEOUT_MS, + ), + ssh.exec("cut -d' ' -f1-3 /proc/loadavg", SSH_COMMAND_TIMEOUT_MS), + ssh.exec( + "docker ps -a --filter name=milady- --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.State}}|{{.Status}}|{{.RunningFor}}' 2>/dev/null || true", + SSH_COMMAND_TIMEOUT_MS, + ), + ]); const containers = parseRuntimeContainers(containersRaw); @@ -468,7 +522,9 @@ async function inspectNodeRuntime(node: { memoryUsedPercent: parseMemoryPercent(memoryRaw.trim()), loadAverage: loadAverageRaw.trim() || null, actualContainerCount: containers.length, - runningContainerCount: containers.filter((container) => container.state === "running").length, + runningContainerCount: containers.filter( + (container) => container.state === "running", + ).length, containers, error: null, }; @@ -520,7 +576,9 @@ function buildNodeAlerts(params: { } const saturation = - node.capacity > 0 ? Math.round((node.allocated_count / node.capacity) * 100) : 0; + node.capacity > 0 + ? Math.round((node.allocated_count / node.capacity) * 100) + : 0; if (saturation >= NODE_SATURATION_CRITICAL_PCT) { alerts.push(`Capacity exhausted (${saturation}% allocated)`); } else if (saturation >= NODE_SATURATION_WARNING_PCT) { @@ -534,7 +592,8 @@ function buildNodeAlerts(params: { if (memoryAlert) alerts.push(memoryAlert); if (allocationDrift !== 0) { - const driftDirection = allocationDrift > 0 ? `+${allocationDrift}` : `${allocationDrift}`; + const driftDirection = + allocationDrift > 0 ? `+${allocationDrift}` : `${allocationDrift}`; alerts.push(`Allocation drift ${driftDirection} vs control plane`); } @@ -627,46 +686,50 @@ export async function getAdminInfrastructureSnapshot(): Promise [container.name, container]), ); - const containers: AdminInfrastructureContainer[] = dbContainers.map((container) => { - const runtimeMatch = container.containerName - ? (runtimeByName.get(container.containerName) ?? null) - : null; - const health = classifyContainerHealth({ - dbStatus: container.status, - runtime: runtimeMatch, - lastHeartbeatAt: toIso(container.lastHeartbeatAt), - errorMessage: container.errorMessage, - }); - - return { - id: container.id, - sandboxId: container.sandboxId, - agentName: container.agentName, - organizationId: container.organizationId, - userId: container.userId, - nodeId: container.nodeId, - containerName: container.containerName, - dbStatus: container.status, - liveHealth: health.status, - liveHealthSeverity: health.severity, - liveHealthReason: health.reason, - runtimeState: runtimeMatch?.state ?? null, - runtimeStatus: runtimeMatch?.status ?? null, - runtimePresent: !!runtimeMatch, - dockerImage: container.dockerImage ?? runtimeMatch?.image ?? null, - bridgePort: container.bridgePort, - webUiPort: container.webUiPort, - headscaleIp: container.headscaleIp, - bridgeUrl: container.bridgeUrl, - healthUrl: container.healthUrl, - lastHeartbeatAt: toIso(container.lastHeartbeatAt), - heartbeatAgeMinutes: getHeartbeatAgeMinutes(toIso(container.lastHeartbeatAt)), - errorMessage: container.errorMessage, - errorCount: container.errorCount ?? 0, - createdAt: toIso(container.createdAt) ?? refreshedAt, - updatedAt: toIso(container.updatedAt) ?? refreshedAt, - }; - }); + const containers: AdminInfrastructureContainer[] = dbContainers.map( + (container) => { + const runtimeMatch = container.containerName + ? (runtimeByName.get(container.containerName) ?? null) + : null; + const health = classifyContainerHealth({ + dbStatus: container.status, + runtime: runtimeMatch, + lastHeartbeatAt: toIso(container.lastHeartbeatAt), + errorMessage: container.errorMessage, + }); + + return { + id: container.id, + sandboxId: container.sandboxId, + agentName: container.agentName, + organizationId: container.organizationId, + userId: container.userId, + nodeId: container.nodeId, + containerName: container.containerName, + dbStatus: container.status, + liveHealth: health.status, + liveHealthSeverity: health.severity, + liveHealthReason: health.reason, + runtimeState: runtimeMatch?.state ?? null, + runtimeStatus: runtimeMatch?.status ?? null, + runtimePresent: !!runtimeMatch, + dockerImage: container.dockerImage ?? runtimeMatch?.image ?? null, + bridgePort: container.bridgePort, + webUiPort: container.webUiPort, + headscaleIp: container.headscaleIp, + bridgeUrl: container.bridgeUrl, + healthUrl: container.healthUrl, + lastHeartbeatAt: toIso(container.lastHeartbeatAt), + heartbeatAgeMinutes: getHeartbeatAgeMinutes( + toIso(container.lastHeartbeatAt), + ), + errorMessage: container.errorMessage, + errorCount: container.errorCount ?? 0, + createdAt: toIso(container.createdAt) ?? refreshedAt, + updatedAt: toIso(container.updatedAt) ?? refreshedAt, + }; + }, + ); const trackedContainerNames = new Set( containers @@ -710,10 +773,17 @@ export async function getAdminInfrastructureSnapshot(): Promise 0 ? Math.round((node.allocated_count / node.capacity) * 100) : 0, + node.capacity > 0 + ? Math.round((node.allocated_count / node.capacity) * 100) + : 0, runtime, allocationDrift, - alerts: buildNodeAlerts({ node, runtime, allocationDrift, unhealthyContainerCount }), + alerts: buildNodeAlerts({ + node, + runtime, + allocationDrift, + unhealthyContainerCount, + }), containers, ghostContainers, metadata: node.metadata, @@ -724,8 +794,8 @@ export async function getAdminInfrastructureSnapshot(): Promise { + const unassignedContainers: AdminInfrastructureContainer[] = + unassignedSandboxRows.map((container) => { const health = classifyContainerHealth({ dbStatus: container.status, runtime: null, @@ -755,14 +825,15 @@ export async function getAdminInfrastructureSnapshot(): Promise node.containers), @@ -877,50 +948,90 @@ export async function getAdminInfrastructureSnapshot(): Promise node.enabled); - const totalCapacity = enabledNodes.reduce((sum, node) => sum + node.capacity, 0); - const allocatedSlots = enabledNodes.reduce((sum, node) => sum + node.allocatedCount, 0); - const availableSlots = enabledNodes.reduce((sum, node) => sum + node.availableSlots, 0); + const totalCapacity = enabledNodes.reduce( + (sum, node) => sum + node.capacity, + 0, + ); + const allocatedSlots = enabledNodes.reduce( + (sum, node) => sum + node.allocatedCount, + 0, + ); + const availableSlots = enabledNodes.reduce( + (sum, node) => sum + node.availableSlots, + 0, + ); const summary: AdminInfrastructureSummary = { totalNodes: inspectedNodes.length, enabledNodes: enabledNodes.length, - healthyNodes: enabledNodes.filter((node) => node.status === "healthy").length, - degradedNodes: enabledNodes.filter((node) => node.status === "degraded").length, - offlineNodes: enabledNodes.filter((node) => node.status === "offline").length, - unknownNodes: enabledNodes.filter((node) => node.status === "unknown").length, + healthyNodes: enabledNodes.filter((node) => node.status === "healthy") + .length, + degradedNodes: enabledNodes.filter((node) => node.status === "degraded") + .length, + offlineNodes: enabledNodes.filter((node) => node.status === "offline") + .length, + unknownNodes: enabledNodes.filter((node) => node.status === "unknown") + .length, totalCapacity, allocatedSlots, availableSlots, - utilizationPct: totalCapacity > 0 ? Math.round((allocatedSlots / totalCapacity) * 100) : 0, + utilizationPct: + totalCapacity > 0 + ? Math.round((allocatedSlots / totalCapacity) * 100) + : 0, saturatedNodes: enabledNodes.filter( (node) => node.utilizationPct >= NODE_SATURATION_WARNING_PCT, ).length, - nodesWithDrift: inspectedNodes.filter((node) => node.allocationDrift !== 0).length, - totalContainers: containers.length, - runningContainers: containers.filter((container) => container.dbStatus === "running").length, - pendingContainers: containers.filter((container) => container.dbStatus === "pending").length, - provisioningContainers: containers.filter((container) => container.dbStatus === "provisioning") + nodesWithDrift: inspectedNodes.filter((node) => node.allocationDrift !== 0) .length, - stoppedContainers: containers.filter((container) => container.dbStatus === "stopped").length, - errorContainers: containers.filter((container) => container.dbStatus === "error").length, - disconnectedContainers: containers.filter((container) => container.dbStatus === "disconnected") - .length, - healthyContainers: containers.filter((container) => container.liveHealth === "healthy").length, + totalContainers: containers.length, + runningContainers: containers.filter( + (container) => container.dbStatus === "running", + ).length, + pendingContainers: containers.filter( + (container) => container.dbStatus === "pending", + ).length, + provisioningContainers: containers.filter( + (container) => container.dbStatus === "provisioning", + ).length, + stoppedContainers: containers.filter( + (container) => container.dbStatus === "stopped", + ).length, + errorContainers: containers.filter( + (container) => container.dbStatus === "error", + ).length, + disconnectedContainers: containers.filter( + (container) => container.dbStatus === "disconnected", + ).length, + healthyContainers: containers.filter( + (container) => container.liveHealth === "healthy", + ).length, attentionContainers: containers.filter( (container) => container.liveHealth !== "healthy" && container.liveHealth !== "warming" && container.liveHealth !== "stopped", ).length, - staleContainers: containers.filter((container) => container.liveHealth === "stale").length, - missingContainers: containers.filter((container) => container.liveHealth === "missing").length, - failedContainers: containers.filter((container) => container.liveHealth === "failed").length, + staleContainers: containers.filter( + (container) => container.liveHealth === "stale", + ).length, + missingContainers: containers.filter( + (container) => container.liveHealth === "missing", + ).length, + failedContainers: containers.filter( + (container) => container.liveHealth === "failed", + ).length, backlogCount: containers.filter( - (container) => container.dbStatus === "pending" || container.dbStatus === "provisioning", + (container) => + container.dbStatus === "pending" || + container.dbStatus === "provisioning", ).length, }; - if (summary.backlogCount > summary.availableSlots && summary.availableSlots >= 0) { + if ( + summary.backlogCount > summary.availableSlots && + summary.availableSlots >= 0 + ) { incidents.push({ severity: "warning", scope: "cluster", @@ -942,7 +1053,8 @@ export async function getAdminInfrastructureSnapshot(): Promise { // In devnet, the default anvil wallet is always admin - if (isDevnet() && walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase()) { + if ( + isDevnet() && + walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase() + ) { return true; } @@ -82,7 +87,10 @@ class AdminService { */ async getAdminRole(walletAddress: string): Promise { // In devnet, the default anvil wallet is super_admin - if (isDevnet() && walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase()) { + if ( + isDevnet() && + walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase() + ) { return "super_admin"; } @@ -103,7 +111,10 @@ class AdminService { walletAddress: string, ): Promise<{ isAdmin: boolean; role: AdminRole | null }> { // In devnet, the default anvil wallet is super_admin - if (isDevnet() && walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase()) { + if ( + isDevnet() && + walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase() + ) { return { isAdmin: true, role: "super_admin" }; } @@ -126,7 +137,12 @@ class AdminService { grantedByWallet?: string; notes?: string; }): Promise { - const { walletAddress, role = "moderator", grantedByWallet, notes } = params; + const { + walletAddress, + role = "moderator", + grantedByWallet, + notes, + } = params; // Check if already admin const existing = await dbRead.query.adminUsers.findFirst({ @@ -194,7 +210,10 @@ class AdminService { /** * Revoke admin privileges */ - async revokeAdmin(walletAddress: string, revokedByWallet?: string): Promise { + async revokeAdmin( + walletAddress: string, + revokedByWallet?: string, + ): Promise { await dbWrite .update(adminUsers) .set({ @@ -220,7 +239,8 @@ class AdminService { // In devnet, include the anvil wallet if not already in list if (isDevnet()) { const hasAnvil = admins.some( - (a) => a.walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase(), + (a) => + a.walletAddress.toLowerCase() === ANVIL_DEFAULT_WALLET.toLowerCase(), ); if (!hasAnvil) { @@ -332,7 +352,8 @@ class AdminService { userId, status: "clean", totalViolations: 1, - warningCount: action === "warned" || action === "flagged_for_ban" ? 1 : 0, + warningCount: + action === "warned" || action === "flagged_for_ban" ? 1 : 0, riskScore: 20, lastViolationAt: now, lastWarningAt: action === "warned" ? now : null, @@ -343,7 +364,9 @@ class AdminService { /** * Get user moderation status */ - async getUserModerationStatus(userId: string): Promise { + async getUserModerationStatus( + userId: string, + ): Promise { const result = await dbRead.query.userModerationStatus.findFirst({ where: eq(userModerationStatus.userId, userId), }); @@ -410,7 +433,11 @@ class AdminService { /** * Ban a user */ - async banUser(params: { userId: string; adminUserId: string; reason: string }): Promise { + async banUser(params: { + userId: string; + adminUserId: string; + reason: string; + }): Promise { const { userId, adminUserId, reason } = params; const now = new Date(); @@ -514,14 +541,15 @@ class AdminService { violations: ModerationViolation[]; generationsCount: number; }> { - const [user, moderationStatusResult, violations, generationsResult] = await Promise.all([ - dbRead.query.users.findFirst({ where: eq(users.id, userId) }), - this.getUserModerationStatus(userId), - this.getUserViolations(userId), - dbRead.execute<{ count: string }>( - sql`SELECT COUNT(*) as count FROM generations WHERE user_id = ${userId}::uuid`, - ), - ]); + const [user, moderationStatusResult, violations, generationsResult] = + await Promise.all([ + dbRead.query.users.findFirst({ where: eq(users.id, userId) }), + this.getUserModerationStatus(userId), + this.getUserViolations(userId), + dbRead.execute<{ count: string }>( + sql`SELECT COUNT(*) as count FROM generations WHERE user_id = ${userId}::uuid`, + ), + ]); return { user: user ?? null, diff --git a/packages/lib/services/advertising/index.ts b/packages/lib/services/advertising/index.ts index b9940109b..4e45f25ab 100644 --- a/packages/lib/services/advertising/index.ts +++ b/packages/lib/services/advertising/index.ts @@ -55,13 +55,21 @@ class AdvertisingService { // Credential Management // ============================================ - private async getCredentials(account: AdAccount): Promise { + private async getCredentials( + account: AdAccount, + ): Promise { const [accessToken, refreshToken] = await Promise.all([ account.access_token_secret_id - ? secretsService.getDecryptedValue(account.access_token_secret_id, account.organization_id) + ? secretsService.getDecryptedValue( + account.access_token_secret_id, + account.organization_id, + ) : undefined, account.refresh_token_secret_id - ? secretsService.getDecryptedValue(account.refresh_token_secret_id, account.organization_id) + ? secretsService.getDecryptedValue( + account.refresh_token_secret_id, + account.organization_id, + ) : undefined, ]); @@ -84,7 +92,10 @@ class AdvertisingService { organizationId: string, options?: { platform?: AdPlatform }, ): Promise { - return await adAccountsRepository.listByOrganization(organizationId, options); + return await adAccountsRepository.listByOrganization( + organizationId, + options, + ); } async getAccount(id: string): Promise { @@ -154,7 +165,8 @@ class AdvertisingService { organization_id: input.organizationId, connected_by_user_id: input.userId, platform: input.platform, - external_account_id: input.externalAccountId || validation.accountId || "", + external_account_id: + input.externalAccountId || validation.accountId || "", account_name: input.accountName || validation.accountName || "Ad Account", access_token_secret_id: accessTokenSecret.id, refresh_token_secret_id: refreshTokenSecretId, @@ -169,7 +181,10 @@ class AdvertisingService { return account; } - async disconnectAccount(accountId: string, organizationId: string): Promise { + async disconnectAccount( + accountId: string, + organizationId: string, + ): Promise { const account = await adAccountsRepository.findById(accountId); if (!account || account.organization_id !== organizationId) { @@ -262,7 +277,10 @@ class AdvertisingService { } // Allocate budget credits - const budgetCredits = calculateSpendCredits(account.platform, input.budgetAmount); + const budgetCredits = calculateSpendCredits( + account.platform, + input.budgetAmount, + ); const budgetDeduction = await creditsService.deductCredits({ organizationId: input.organizationId, @@ -280,7 +298,8 @@ class AdvertisingService { await creditsService.refundCredits({ organizationId: input.organizationId, amount: AD_CREDIT_RATES.createCampaign, - description: "Refund: Campaign creation failed due to insufficient budget", + description: + "Refund: Campaign creation failed due to insufficient budget", metadata: {}, }); throw new Error("Insufficient credits for campaign budget"); @@ -293,7 +312,11 @@ class AdvertisingService { const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform); - const result = await provider.createCampaign(credentials, account.external_account_id, input); + const result = await provider.createCampaign( + credentials, + account.external_account_id, + input, + ); if (!result.success) { // Refund all credits @@ -367,7 +390,11 @@ class AdvertisingService { const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform); - const result = await provider.updateCampaign(credentials, campaign.external_campaign_id, input); + const result = await provider.updateCampaign( + credentials, + campaign.external_campaign_id, + input, + ); if (!result.success) { throw new Error(result.error || "Failed to update campaign"); @@ -375,7 +402,9 @@ class AdvertisingService { const updated = await adCampaignsRepository.update(campaignId, { name: input.name, - budget_amount: input.budgetAmount ? String(input.budgetAmount) : undefined, + budget_amount: input.budgetAmount + ? String(input.budgetAmount) + : undefined, start_date: input.startDate, end_date: input.endDate, targeting: input.targeting, @@ -386,7 +415,10 @@ class AdvertisingService { return updated!; } - async startCampaign(campaignId: string, organizationId: string): Promise { + async startCampaign( + campaignId: string, + organizationId: string, + ): Promise { const campaign = await adCampaignsRepository.findById(campaignId); if (!campaign || campaign.organization_id !== organizationId) { throw new Error("Campaign not found"); @@ -404,20 +436,29 @@ class AdvertisingService { const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform); - const result = await provider.activateCampaign(credentials, campaign.external_campaign_id); + const result = await provider.activateCampaign( + credentials, + campaign.external_campaign_id, + ); if (!result.success) { throw new Error(result.error || "Failed to start campaign"); } - const updated = await adCampaignsRepository.updateStatus(campaignId, "active"); + const updated = await adCampaignsRepository.updateStatus( + campaignId, + "active", + ); logger.info("[Advertising] Campaign started", { campaignId }); return updated!; } - async pauseCampaign(campaignId: string, organizationId: string): Promise { + async pauseCampaign( + campaignId: string, + organizationId: string, + ): Promise { const campaign = await adCampaignsRepository.findById(campaignId); if (!campaign || campaign.organization_id !== organizationId) { throw new Error("Campaign not found"); @@ -435,20 +476,29 @@ class AdvertisingService { const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform); - const result = await provider.pauseCampaign(credentials, campaign.external_campaign_id); + const result = await provider.pauseCampaign( + credentials, + campaign.external_campaign_id, + ); if (!result.success) { throw new Error(result.error || "Failed to pause campaign"); } - const updated = await adCampaignsRepository.updateStatus(campaignId, "paused"); + const updated = await adCampaignsRepository.updateStatus( + campaignId, + "paused", + ); logger.info("[Advertising] Campaign paused", { campaignId }); return updated!; } - async deleteCampaign(campaignId: string, organizationId: string): Promise { + async deleteCampaign( + campaignId: string, + organizationId: string, + ): Promise { const campaign = await adCampaignsRepository.findById(campaignId); if (!campaign || campaign.organization_id !== organizationId) { throw new Error("Campaign not found"); @@ -456,11 +506,16 @@ class AdvertisingService { // If synced with platform, delete there first if (campaign.external_campaign_id) { - const account = await adAccountsRepository.findById(campaign.ad_account_id); + const account = await adAccountsRepository.findById( + campaign.ad_account_id, + ); if (account) { const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform); - await provider.deleteCampaign(credentials, campaign.external_campaign_id); + await provider.deleteCampaign( + credentials, + campaign.external_campaign_id, + ); } } @@ -546,7 +601,10 @@ class AdvertisingService { // Creative Operations // ============================================ - async listCreatives(campaignId: string, organizationId: string): Promise { + async listCreatives( + campaignId: string, + organizationId: string, + ): Promise { const campaign = await adCampaignsRepository.findById(campaignId); if (!campaign || campaign.organization_id !== organizationId) { throw new Error("Campaign not found"); @@ -555,7 +613,10 @@ class AdvertisingService { return await adCreativesRepository.listByCampaign(campaignId); } - async createCreative(organizationId: string, input: CreateCreativeInput): Promise { + async createCreative( + organizationId: string, + input: CreateCreativeInput, + ): Promise { const campaign = await adCampaignsRepository.findById(input.campaignId); if (!campaign || campaign.organization_id !== organizationId) { throw new Error("Campaign not found"); @@ -589,7 +650,9 @@ class AdvertisingService { // Sync with platform if campaign is synced if (campaign.external_campaign_id) { - const account = await adAccountsRepository.findById(campaign.ad_account_id); + const account = await adAccountsRepository.findById( + campaign.ad_account_id, + ); if (account) { const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform); @@ -645,7 +708,10 @@ class AdvertisingService { return updated!; } - async deleteCreative(creativeId: string, organizationId: string): Promise { + async deleteCreative( + creativeId: string, + organizationId: string, + ): Promise { const creative = await adCreativesRepository.findById(creativeId); if (!creative) { throw new Error("Creative not found"); diff --git a/packages/lib/services/advertising/providers/google.ts b/packages/lib/services/advertising/providers/google.ts index 70ce3b1aa..9a679b74d 100644 --- a/packages/lib/services/advertising/providers/google.ts +++ b/packages/lib/services/advertising/providers/google.ts @@ -53,7 +53,9 @@ async function googleAdsRequest( if (!response.ok) { const error = json as GoogleAdsError; - throw new Error(error.error?.message || `Google Ads API error: ${response.status}`); + throw new Error( + error.error?.message || `Google Ads API error: ${response.status}`, + ); } return json as T; @@ -96,12 +98,15 @@ export const googleAdsProvider: AdProvider = { } // List accessible customers to validate token - const response = await fetch(`${GOOGLE_ADS_BASE_URL}/customers:listAccessibleCustomers`, { - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - "developer-token": process.env.GOOGLE_ADS_DEVELOPER_TOKEN, + const response = await fetch( + `${GOOGLE_ADS_BASE_URL}/customers:listAccessibleCustomers`, + { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + "developer-token": process.env.GOOGLE_ADS_DEVELOPER_TOKEN, + }, }, - }); + ); if (!response.ok) { return { @@ -156,12 +161,15 @@ export const googleAdsProvider: AdProvider = { async listAdAccounts( credentials: AdAccountCredentials, ): Promise> { - const response = await fetch(`${GOOGLE_ADS_BASE_URL}/customers:listAccessibleCustomers`, { - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - "developer-token": process.env.GOOGLE_ADS_DEVELOPER_TOKEN || "", + const response = await fetch( + `${GOOGLE_ADS_BASE_URL}/customers:listAccessibleCustomers`, + { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + "developer-token": process.env.GOOGLE_ADS_DEVELOPER_TOKEN || "", + }, }, - }); + ); if (!response.ok) { throw new Error("Failed to list Google Ads accounts"); @@ -226,11 +234,15 @@ export const googleAdsProvider: AdProvider = { create: { name: `${input.name} - Budget`, deliveryMethod: "STANDARD", - amountMicros: Math.round(input.budgetAmount * 1_000_000).toString(), + amountMicros: Math.round( + input.budgetAmount * 1_000_000, + ).toString(), ...(input.budgetType === "daily" ? {} : { - totalAmountMicros: Math.round(input.budgetAmount * 1_000_000).toString(), + totalAmountMicros: Math.round( + input.budgetAmount * 1_000_000, + ).toString(), }), }, }, @@ -254,7 +266,8 @@ export const googleAdsProvider: AdProvider = { create: { name: input.name, advertisingChannelType: channelConfig.advertisingChannelType, - advertisingChannelSubType: channelConfig.advertisingChannelSubType, + advertisingChannelSubType: + channelConfig.advertisingChannelSubType, status: "PAUSED", campaignBudget: budgetResourceName, startDate: input.startDate @@ -269,7 +282,8 @@ export const googleAdsProvider: AdProvider = { }), }); - const campaignResourceName = campaignMutateResponse.results?.[0]?.resourceName; + const campaignResourceName = + campaignMutateResponse.results?.[0]?.resourceName; if (!campaignResourceName) { return { success: false, error: "Failed to create campaign" }; } @@ -310,29 +324,40 @@ export const googleAdsProvider: AdProvider = { } if (input.startDate) { - updateFields.startDate = input.startDate.toISOString().split("T")[0].replace(/-/g, ""); + updateFields.startDate = input.startDate + .toISOString() + .split("T")[0] + .replace(/-/g, ""); updateMask.push("startDate"); } if (input.endDate) { - updateFields.endDate = input.endDate.toISOString().split("T")[0].replace(/-/g, ""); + updateFields.endDate = input.endDate + .toISOString() + .split("T")[0] + .replace(/-/g, ""); updateMask.push("endDate"); } - await googleAdsRequest("/campaigns:mutate", credentials.accessToken, accountId, { - method: "POST", - body: JSON.stringify({ - operations: [ - { - updateMask: updateMask.join(","), - update: { - resourceName: `customers/${accountId}/campaigns/${campaignId}`, - ...updateFields, + await googleAdsRequest( + "/campaigns:mutate", + credentials.accessToken, + accountId, + { + method: "POST", + body: JSON.stringify({ + operations: [ + { + updateMask: updateMask.join(","), + update: { + resourceName: `customers/${accountId}/campaigns/${campaignId}`, + ...updateFields, + }, }, - }, - ], - }), - }); + ], + }), + }, + ); return { success: true, externalCampaignId }; }, @@ -347,20 +372,25 @@ export const googleAdsProvider: AdProvider = { } const [accountId, campaignId] = parts; - await googleAdsRequest("/campaigns:mutate", credentials.accessToken, accountId, { - method: "POST", - body: JSON.stringify({ - operations: [ - { - updateMask: "status", - update: { - resourceName: `customers/${accountId}/campaigns/${campaignId}`, - status: "PAUSED", + await googleAdsRequest( + "/campaigns:mutate", + credentials.accessToken, + accountId, + { + method: "POST", + body: JSON.stringify({ + operations: [ + { + updateMask: "status", + update: { + resourceName: `customers/${accountId}/campaigns/${campaignId}`, + status: "PAUSED", + }, }, - }, - ], - }), - }); + ], + }), + }, + ); return { success: true, externalCampaignId }; }, @@ -375,20 +405,25 @@ export const googleAdsProvider: AdProvider = { } const [accountId, campaignId] = parts; - await googleAdsRequest("/campaigns:mutate", credentials.accessToken, accountId, { - method: "POST", - body: JSON.stringify({ - operations: [ - { - updateMask: "status", - update: { - resourceName: `customers/${accountId}/campaigns/${campaignId}`, - status: "ENABLED", + await googleAdsRequest( + "/campaigns:mutate", + credentials.accessToken, + accountId, + { + method: "POST", + body: JSON.stringify({ + operations: [ + { + updateMask: "status", + update: { + resourceName: `customers/${accountId}/campaigns/${campaignId}`, + status: "ENABLED", + }, }, - }, - ], - }), - }); + ], + }), + }, + ); return { success: true, externalCampaignId }; }, @@ -404,20 +439,25 @@ export const googleAdsProvider: AdProvider = { const [accountId, campaignId] = parts; // Google Ads doesn't allow deletion, only removal (status = REMOVED) - await googleAdsRequest("/campaigns:mutate", credentials.accessToken, accountId, { - method: "POST", - body: JSON.stringify({ - operations: [ - { - updateMask: "status", - update: { - resourceName: `customers/${accountId}/campaigns/${campaignId}`, - status: "REMOVED", + await googleAdsRequest( + "/campaigns:mutate", + credentials.accessToken, + accountId, + { + method: "POST", + body: JSON.stringify({ + operations: [ + { + updateMask: "status", + update: { + resourceName: `customers/${accountId}/campaigns/${campaignId}`, + status: "REMOVED", + }, }, - }, - ], - }), - }); + ], + }), + }, + ); return { success: true }; }, @@ -514,7 +554,8 @@ export const googleAdsProvider: AdProvider = { } const [accountId, campaignId] = parts; - const startDate = dateRange?.start || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const startDate = + dateRange?.start || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const endDate = dateRange?.end || new Date(); const query = ` diff --git a/packages/lib/services/advertising/providers/meta.ts b/packages/lib/services/advertising/providers/meta.ts index 437a55f94..46a9d6007 100644 --- a/packages/lib/services/advertising/providers/meta.ts +++ b/packages/lib/services/advertising/providers/meta.ts @@ -61,7 +61,9 @@ async function checkRateLimit(): Promise { const oneMinuteAgo = now - 60000; // Clean old timestamps - RATE_LIMIT.requestTimestamps = RATE_LIMIT.requestTimestamps.filter((ts) => ts > oneMinuteAgo); + RATE_LIMIT.requestTimestamps = RATE_LIMIT.requestTimestamps.filter( + (ts) => ts > oneMinuteAgo, + ); if (RATE_LIMIT.requestTimestamps.length >= RATE_LIMIT.requestsPerMinute) { const waitTime = RATE_LIMIT.requestTimestamps[0] - oneMinuteAgo + 100; @@ -83,7 +85,9 @@ async function graphApiRequest( ): Promise { await checkRateLimit(); - const url = new URL(endpoint.startsWith("http") ? endpoint : `${GRAPH_API_BASE}${endpoint}`); + const url = new URL( + endpoint.startsWith("http") ? endpoint : `${GRAPH_API_BASE}${endpoint}`, + ); if (!options.method || options.method === "GET") { url.searchParams.set("access_token", accessToken); @@ -104,7 +108,9 @@ async function graphApiRequest( if ((json as GraphApiError).error) { const error = (json as GraphApiError).error!; - lastError = new Error(`Meta API Error: ${error.message} (code: ${error.code})`); + lastError = new Error( + `Meta API Error: ${error.message} (code: ${error.code})`, + ); if (isRetryableError(error.code) && attempt < RETRY_CONFIG.maxAttempts) { const delay = Math.min( @@ -191,7 +197,9 @@ export const metaAdsProvider: AdProvider = { const appSecret = process.env.META_APP_SECRET; if (!appId || !appSecret) { - throw new Error("META_APP_ID and META_APP_SECRET required for token refresh"); + throw new Error( + "META_APP_ID and META_APP_SECRET required for token refresh", + ); } const response = await graphApiRequest<{ @@ -245,7 +253,9 @@ export const metaAdsProvider: AdProvider = { objective: input.objective, }); - const actAccountId = accountId.startsWith("act_") ? accountId : `act_${accountId}`; + const actAccountId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; // Create campaign const campaignParams = new URLSearchParams({ @@ -275,7 +285,9 @@ export const metaAdsProvider: AdProvider = { if (input.budgetType === "daily") { adSetParams.daily_budget = String(Math.round(input.budgetAmount * 100)); } else { - adSetParams.lifetime_budget = String(Math.round(input.budgetAmount * 100)); + adSetParams.lifetime_budget = String( + Math.round(input.budgetAmount * 100), + ); } if (input.startDate) { @@ -359,9 +371,13 @@ export const metaAdsProvider: AdProvider = { const updateParams = new URLSearchParams(params); - await graphApiRequest(`/${externalCampaignId}?${updateParams}`, credentials.accessToken, { - method: "POST", - }); + await graphApiRequest( + `/${externalCampaignId}?${updateParams}`, + credentials.accessToken, + { + method: "POST", + }, + ); logger.info("[MetaAds] Campaign updated", { externalCampaignId }); @@ -385,9 +401,13 @@ export const metaAdsProvider: AdProvider = { status: "PAUSED", }); - await graphApiRequest(`/${externalCampaignId}?${params}`, credentials.accessToken, { - method: "POST", - }); + await graphApiRequest( + `/${externalCampaignId}?${params}`, + credentials.accessToken, + { + method: "POST", + }, + ); logger.info("[MetaAds] Campaign paused", { externalCampaignId }); @@ -411,9 +431,13 @@ export const metaAdsProvider: AdProvider = { status: "ACTIVE", }); - await graphApiRequest(`/${externalCampaignId}?${params}`, credentials.accessToken, { - method: "POST", - }); + await graphApiRequest( + `/${externalCampaignId}?${params}`, + credentials.accessToken, + { + method: "POST", + }, + ); logger.info("[MetaAds] Campaign activated", { externalCampaignId }); @@ -436,9 +460,13 @@ export const metaAdsProvider: AdProvider = { access_token: credentials.accessToken, }); - await graphApiRequest(`/${externalCampaignId}?${params}`, credentials.accessToken, { - method: "DELETE", - }); + await graphApiRequest( + `/${externalCampaignId}?${params}`, + credentials.accessToken, + { + method: "DELETE", + }, + ); logger.info("[MetaAds] Campaign deleted", { externalCampaignId }); @@ -465,7 +493,9 @@ export const metaAdsProvider: AdProvider = { name: input.name, }); - const actAccountId = accountId.startsWith("act_") ? accountId : `act_${accountId}`; + const actAccountId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; // Get the ad set for this campaign const adSets = await graphApiRequest<{ @@ -554,7 +584,8 @@ export const metaAdsProvider: AdProvider = { logger.error("[MetaAds] Create creative failed", { error }); return { success: false, - error: error instanceof Error ? error.message : "Failed to create creative", + error: + error instanceof Error ? error.message : "Failed to create creative", }; } }, @@ -565,9 +596,15 @@ export const metaAdsProvider: AdProvider = { dateRange?: { start: Date; end: Date }, ): Promise { try { - const fields = ["spend", "impressions", "clicks", "conversions", "ctr", "cpc", "cpm"].join( - ",", - ); + const fields = [ + "spend", + "impressions", + "clicks", + "conversions", + "ctr", + "cpc", + "cpm", + ].join(","); let url = `/${externalCampaignId}/insights?fields=${fields}&access_token=${credentials.accessToken}`; diff --git a/packages/lib/services/advertising/providers/tiktok.ts b/packages/lib/services/advertising/providers/tiktok.ts index ff683d268..4ab598b15 100644 --- a/packages/lib/services/advertising/providers/tiktok.ts +++ b/packages/lib/services/advertising/providers/tiktok.ts @@ -129,15 +129,18 @@ export const tiktokAdsProvider: AdProvider = { refreshToken?: string; expiresAt?: Date; }> { - const response = await fetch(`${TIKTOK_ADS_BASE_URL}/oauth2/refresh_token/`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - app_id: process.env.TIKTOK_ADS_APP_ID, - secret: process.env.TIKTOK_ADS_APP_SECRET, - refresh_token: refreshToken, - }), - }); + const response = await fetch( + `${TIKTOK_ADS_BASE_URL}/oauth2/refresh_token/`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + app_id: process.env.TIKTOK_ADS_APP_ID, + secret: process.env.TIKTOK_ADS_APP_SECRET, + refresh_token: refreshToken, + }), + }, + ); const json = (await response.json()) as TikTokAdsResponse<{ access_token: string; @@ -152,7 +155,9 @@ export const tiktokAdsProvider: AdProvider = { return { accessToken: json.data.access_token, refreshToken: json.data.refresh_token, - expiresAt: new Date(Date.now() + json.data.refresh_token_expires_in * 1000), + expiresAt: new Date( + Date.now() + json.data.refresh_token_expires_in * 1000, + ), }; }, @@ -196,7 +201,10 @@ export const tiktokAdsProvider: AdProvider = { advertiser_id: accountId, campaign_name: input.name, objective_type: objective, - budget_mode: input.budgetType === "daily" ? "BUDGET_MODE_DAY" : "BUDGET_MODE_TOTAL", + budget_mode: + input.budgetType === "daily" + ? "BUDGET_MODE_DAY" + : "BUDGET_MODE_TOTAL", budget: budgetCents, operation_status: "DISABLE", // Start paused }), @@ -258,14 +266,18 @@ export const tiktokAdsProvider: AdProvider = { } const [advertiserId, campaignId] = parts; - await tiktokAdsRequest("/campaign/update/status/", credentials.accessToken, { - method: "POST", - body: JSON.stringify({ - advertiser_id: advertiserId, - campaign_ids: [campaignId], - operation_status: "DISABLE", - }), - }); + await tiktokAdsRequest( + "/campaign/update/status/", + credentials.accessToken, + { + method: "POST", + body: JSON.stringify({ + advertiser_id: advertiserId, + campaign_ids: [campaignId], + operation_status: "DISABLE", + }), + }, + ); return { success: true, externalCampaignId }; }, @@ -280,14 +292,18 @@ export const tiktokAdsProvider: AdProvider = { } const [advertiserId, campaignId] = parts; - await tiktokAdsRequest("/campaign/update/status/", credentials.accessToken, { - method: "POST", - body: JSON.stringify({ - advertiser_id: advertiserId, - campaign_ids: [campaignId], - operation_status: "ENABLE", - }), - }); + await tiktokAdsRequest( + "/campaign/update/status/", + credentials.accessToken, + { + method: "POST", + body: JSON.stringify({ + advertiser_id: advertiserId, + campaign_ids: [campaignId], + operation_status: "ENABLE", + }), + }, + ); return { success: true, externalCampaignId }; }, @@ -302,14 +318,18 @@ export const tiktokAdsProvider: AdProvider = { } const [advertiserId, campaignId] = parts; - await tiktokAdsRequest("/campaign/update/status/", credentials.accessToken, { - method: "POST", - body: JSON.stringify({ - advertiser_id: advertiserId, - campaign_ids: [campaignId], - operation_status: "DELETE", - }), - }); + await tiktokAdsRequest( + "/campaign/update/status/", + credentials.accessToken, + { + method: "POST", + body: JSON.stringify({ + advertiser_id: advertiserId, + campaign_ids: [campaignId], + operation_status: "DELETE", + }), + }, + ); return { success: true }; }, @@ -393,7 +413,8 @@ export const tiktokAdsProvider: AdProvider = { } const [advertiserId, campaignId] = parts; - const startDate = dateRange?.start || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const startDate = + dateRange?.start || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const endDate = dateRange?.end || new Date(); const data = await tiktokAdsRequest<{ @@ -412,7 +433,12 @@ export const tiktokAdsProvider: AdProvider = { campaign_ids: JSON.stringify([campaignId]), data_level: "AUCTION_CAMPAIGN", dimensions: JSON.stringify(["campaign_id"]), - metrics: JSON.stringify(["spend", "impressions", "clicks", "conversion"]), + metrics: JSON.stringify([ + "spend", + "impressions", + "clicks", + "conversion", + ]), start_date: startDate.toISOString().split("T")[0], end_date: endDate.toISOString().split("T")[0], report_type: "BASIC", diff --git a/packages/lib/services/advertising/types.ts b/packages/lib/services/advertising/types.ts index 323838585..5704fdfd7 100644 --- a/packages/lib/services/advertising/types.ts +++ b/packages/lib/services/advertising/types.ts @@ -5,8 +5,16 @@ */ import type { AdAccountStatus, AdPlatform } from "@/db/schemas/ad-accounts"; -import type { BudgetType, CampaignObjective, CampaignStatus } from "@/db/schemas/ad-campaigns"; -import type { CallToAction, CreativeStatus, CreativeType } from "@/db/schemas/ad-creatives"; +import type { + BudgetType, + CampaignObjective, + CampaignStatus, +} from "@/db/schemas/ad-campaigns"; +import type { + CallToAction, + CreativeStatus, + CreativeType, +} from "@/db/schemas/ad-creatives"; export type { AdAccountStatus, @@ -171,7 +179,9 @@ export interface AdProvider { /** * Validates ad account credentials. */ - validateCredentials(credentials: AdAccountCredentials): Promise; + validateCredentials( + credentials: AdAccountCredentials, + ): Promise; /** * Refreshes access token if expired. @@ -185,7 +195,9 @@ export interface AdProvider { /** * Lists available ad accounts for the authenticated user. */ - listAdAccounts(credentials: AdAccountCredentials): Promise>; + listAdAccounts( + credentials: AdAccountCredentials, + ): Promise>; /** * Creates a campaign on the ad platform. @@ -269,7 +281,10 @@ export const AD_CREDIT_RATES = { detailedAnalytics: 0.1, } as const; -export function calculateSpendCredits(platform: AdPlatform, amount: number): number { +export function calculateSpendCredits( + platform: AdPlatform, + amount: number, +): number { const markup = AD_CREDIT_RATES.spendMarkup[platform] || 1.1; return amount * markup; } diff --git a/packages/lib/services/affiliate-images.ts b/packages/lib/services/affiliate-images.ts index a48cd81b1..80e69e522 100644 --- a/packages/lib/services/affiliate-images.ts +++ b/packages/lib/services/affiliate-images.ts @@ -74,7 +74,10 @@ function isVercelBlobUrl(str: string): boolean { * @param timeoutMs - Timeout in milliseconds. * @returns Upload result. */ -async function uploadWithTimeout(promise: Promise, timeoutMs: number): Promise { +async function uploadWithTimeout( + promise: Promise, + timeoutMs: number, +): Promise { const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error("Upload timeout")), timeoutMs); }); @@ -100,7 +103,9 @@ async function uploadSingleImage( const filename = `${prefix}-${Date.now()}.png`; if (isBase64DataUrl(imageData)) { - logger.debug(`[AffiliateImages] Uploading base64 image ${index} to blob storage`); + logger.debug( + `[AffiliateImages] Uploading base64 image ${index} to blob storage`, + ); const result = await uploadWithTimeout( uploadBase64Image(imageData, { filename, @@ -114,11 +119,15 @@ async function uploadSingleImage( return result.url; } else if (isValidHttpUrl(imageData)) { if (isVercelBlobUrl(imageData)) { - logger.debug(`[AffiliateImages] URL image ${index} already on Vercel Blob`); + logger.debug( + `[AffiliateImages] URL image ${index} already on Vercel Blob`, + ); return imageData; } - logger.info(`[AffiliateImages] Re-uploading external URL ${index} to blob storage`); + logger.info( + `[AffiliateImages] Re-uploading external URL ${index} to blob storage`, + ); const result = await uploadWithTimeout( uploadFromUrl(imageData, { filename, @@ -203,7 +212,9 @@ export async function processAffiliateImages( ); const imagesToProcess = uniqueImages.slice(0, MAX_IMAGES); - logger.info(`[AffiliateImages] Processing ${imagesToProcess.length} unique images`); + logger.info( + `[AffiliateImages] Processing ${imagesToProcess.length} unique images`, + ); const chunks: Array = []; for (let i = 0; i < imagesToProcess.length; i += MAX_CONCURRENT_UPLOADS) { @@ -213,7 +224,12 @@ export async function processAffiliateImages( let processedIndex = 0; for (const chunk of chunks) { const uploadPromises = chunk.map((img, chunkIndex) => - uploadSingleImage(img.data, processedIndex + chunkIndex, characterId, img.isAvatar), + uploadSingleImage( + img.data, + processedIndex + chunkIndex, + characterId, + img.isAvatar, + ), ); const results = await Promise.all(uploadPromises); @@ -238,11 +254,15 @@ export async function processAffiliateImages( if (!result.avatarUrl && result.referenceImageUrls.length > 0) { result.avatarUrl = result.referenceImageUrls[0]; - logger.info("[AffiliateImages] Using first reference image as avatar fallback"); + logger.info( + "[AffiliateImages] Using first reference image as avatar fallback", + ); } logger.info("[AffiliateImages] Processing complete", { - avatarUrl: result.avatarUrl ? result.avatarUrl.substring(0, 60) + "..." : null, + avatarUrl: result.avatarUrl + ? result.avatarUrl.substring(0, 60) + "..." + : null, referenceCount: result.referenceImageUrls.length, failedCount: result.failedUploads, }); @@ -250,7 +270,9 @@ export async function processAffiliateImages( return result; } -export function buildAffiliateImageReferences(urls: string[]): AffiliateImageReference[] { +export function buildAffiliateImageReferences( + urls: string[], +): AffiliateImageReference[] { return urls.map((url, index) => ({ url, isProfilePic: index === 0, @@ -266,7 +288,9 @@ export function extractSafeImageUrls( const imageUrls = affiliateData.imageUrls; if (!Array.isArray(imageUrls)) return []; - return imageUrls.filter((url): url is string => typeof url === "string" && isValidHttpUrl(url)); + return imageUrls.filter( + (url): url is string => typeof url === "string" && isValidHttpUrl(url), + ); } export function hasValidReferenceImages( diff --git a/packages/lib/services/affiliates.ts b/packages/lib/services/affiliates.ts index c2cb48ca3..93d524fcd 100644 --- a/packages/lib/services/affiliates.ts +++ b/packages/lib/services/affiliates.ts @@ -20,7 +20,8 @@ function normalizeAffiliateCode(code: string): string { function isUniqueViolation(error: unknown): boolean { const code = error instanceof Error ? Reflect.get(error, "code") : undefined; return ( - code === "23505" || (error instanceof Error && error.message.includes("unique constraint")) + code === "23505" || + (error instanceof Error && error.message.includes("unique constraint")) ); } @@ -43,17 +44,27 @@ export class AffiliatesService { } const code = await affiliatesRepository.getAffiliateCodeByUserId(userId); - await cache.set(cacheKey, code || { __none: true }, CacheTTL.affiliate.data); + await cache.set( + cacheKey, + code || { __none: true }, + CacheTTL.affiliate.data, + ); return code; } /** * Generates or returns an existing affiliate code for the user. */ - async getOrCreateAffiliateCode(userId: string, markupPercent?: number): Promise { + async getOrCreateAffiliateCode( + userId: string, + markupPercent?: number, + ): Promise { let affiliateCode = await this.getAffiliateCode(userId); if (affiliateCode) { - if (markupPercent !== undefined && Number(affiliateCode.markup_percent) !== markupPercent) { + if ( + markupPercent !== undefined && + Number(affiliateCode.markup_percent) !== markupPercent + ) { return this.updateMarkup(userId, markupPercent); } return affiliateCode; @@ -69,22 +80,27 @@ export class AffiliatesService { const code = `AFF-${nanoid(8).toUpperCase()}`; try { - affiliateCode = await affiliatesRepository.createAffiliateCodeIfNotExists({ - user_id: userId, - code, - markup_percent: markup.toFixed(2) as string, - }); + affiliateCode = + await affiliatesRepository.createAffiliateCodeIfNotExists({ + user_id: userId, + code, + markup_percent: markup.toFixed(2) as string, + }); if (affiliateCode) { await cache.del(CacheKeys.affiliate.codeByUserId(userId)); } } catch (error) { if (isUniqueViolation(error)) { - affiliateCode = await affiliatesRepository.getAffiliateCodeByUserId(userId); + affiliateCode = + await affiliatesRepository.getAffiliateCodeByUserId(userId); if (affiliateCode) { - logger.info("[Affiliates] Using concurrently created affiliate code", { - userId, - }); + logger.info( + "[Affiliates] Using concurrently created affiliate code", + { + userId, + }, + ); break; } attempts++; @@ -94,11 +110,15 @@ export class AffiliatesService { } if (!affiliateCode) { - affiliateCode = await affiliatesRepository.getAffiliateCodeByUserId(userId); + affiliateCode = + await affiliatesRepository.getAffiliateCodeByUserId(userId); if (affiliateCode) { - logger.info("[Affiliates] Using concurrently created affiliate code", { - userId, - }); + logger.info( + "[Affiliates] Using concurrently created affiliate code", + { + userId, + }, + ); break; } throw new Error("Failed to create or retrieve affiliate code"); @@ -112,7 +132,10 @@ export class AffiliatesService { throw new Error("Failed to generate a unique affiliate code"); } - if (markupPercent !== undefined && Number(affiliateCode.markup_percent) !== markupPercent) { + if ( + markupPercent !== undefined && + Number(affiliateCode.markup_percent) !== markupPercent + ) { return this.updateMarkup(userId, markupPercent); } @@ -122,19 +145,26 @@ export class AffiliatesService { /** * Updates the markup percentage for an affiliate code */ - async updateMarkup(userId: string, markupPercent: number): Promise { + async updateMarkup( + userId: string, + markupPercent: number, + ): Promise { if (markupPercent < 0 || markupPercent > 1000) { throw new Error("Markup percent must be between 0 and 1000"); } - const existing = await affiliatesRepository.getAffiliateCodeByUserId(userId); + const existing = + await affiliatesRepository.getAffiliateCodeByUserId(userId); if (!existing) { throw new Error(ERRORS.CODE_NOT_FOUND); } - const updated = await affiliatesRepository.updateAffiliateCode(existing.id, { - markup_percent: markupPercent.toFixed(2) as string, - }); + const updated = await affiliatesRepository.updateAffiliateCode( + existing.id, + { + markup_percent: markupPercent.toFixed(2) as string, + }, + ); if (!updated) { throw new Error("Failed to update affiliate code"); @@ -145,22 +175,31 @@ export class AffiliatesService { await cache.del(CacheKeys.affiliate.codeByUserId(existing.user_id)); await cache.del(CacheKeys.affiliate.codeByCode(existing.code)); - logger.info("[Affiliates] Updated affiliate markup", { userId, markupPercent }); + logger.info("[Affiliates] Updated affiliate markup", { + userId, + markupPercent, + }); return updated; } /** * Links a user to an affiliate code (invoked during signup) */ - async linkUserToAffiliateCode(userId: string, code: string): Promise { + async linkUserToAffiliateCode( + userId: string, + code: string, + ): Promise { const normalizedCode = normalizeAffiliateCode(code); // Check cache for affiliate code by code string const codeCacheKey = CacheKeys.affiliate.codeByCode(normalizedCode); - let affiliateCode = await cache.get(codeCacheKey); + let affiliateCode = await cache.get( + codeCacheKey, + ); if (!affiliateCode) { - const dbCode = await affiliatesRepository.getAffiliateCodeByCode(normalizedCode); + const dbCode = + await affiliatesRepository.getAffiliateCodeByCode(normalizedCode); affiliateCode = dbCode || { __none: true }; await cache.set(codeCacheKey, affiliateCode, CacheTTL.affiliate.data); } @@ -193,7 +232,8 @@ export class AffiliatesService { }); } catch (error) { if (isUniqueViolation(error)) { - const concurrentLink = await affiliatesRepository.getUserAffiliate(userId); + const concurrentLink = + await affiliatesRepository.getUserAffiliate(userId); if (concurrentLink?.affiliate_code_id === affiliateCode.id) { return concurrentLink; } @@ -233,7 +273,9 @@ export class AffiliatesService { const linkData = link as UserAffiliate; const codeId = linkData.affiliate_code_id; const codeCacheKey = CacheKeys.affiliate.codeById(codeId); - let affiliateCode = await cache.get(codeCacheKey); + let affiliateCode = await cache.get( + codeCacheKey, + ); if (!affiliateCode) { const dbCode = await affiliatesRepository.getAffiliateCodeById(codeId); @@ -241,7 +283,10 @@ export class AffiliatesService { await cache.set(codeCacheKey, affiliateCode, CacheTTL.affiliate.data); } - if ("__none" in affiliateCode || !(affiliateCode as AffiliateCode).is_active) { + if ( + "__none" in affiliateCode || + !(affiliateCode as AffiliateCode).is_active + ) { return null; } diff --git a/packages/lib/services/agent-budgets.ts b/packages/lib/services/agent-budgets.ts index 069c01442..1e1b087d1 100644 --- a/packages/lib/services/agent-budgets.ts +++ b/packages/lib/services/agent-budgets.ts @@ -26,7 +26,11 @@ import { organizations } from "@/db/schemas/organizations"; import { userCharacters } from "@/db/schemas/user-characters"; import { users } from "@/db/schemas/users"; import { logger } from "@/lib/utils/logger"; -import { type CreditReservation, creditsService, InsufficientCreditsError } from "./credits"; +import { + type CreditReservation, + creditsService, + InsufficientCreditsError, +} from "./credits"; import { emailService } from "./email"; // ============================================================================ @@ -145,7 +149,10 @@ class AgentBudgetService { * Check if agent has sufficient budget for an operation * This is a pre-flight check - does not lock or modify anything */ - async checkBudget(agentId: string, estimatedCost: number): Promise { + async checkBudget( + agentId: string, + estimatedCost: number, + ): Promise { const budget = await this.getOrCreateBudget(agentId); if (!budget) { @@ -162,7 +169,8 @@ class AgentBudgetService { if (budget.is_paused) { return { canProceed: false, - availableBudget: Number(budget.allocated_budget) - Number(budget.spent_budget), + availableBudget: + Number(budget.allocated_budget) - Number(budget.spent_budget), dailyRemaining: budget.daily_limit ? Number(budget.daily_limit) - Number(budget.daily_spent) : null, @@ -220,7 +228,15 @@ class AgentBudgetService { * Deduct from agent's budget atomically */ async deductBudget(params: DeductBudgetParams): Promise { - const { agentId, amount, description, operationType, model, tokensUsed, metadata } = params; + const { + agentId, + amount, + description, + operationType, + model, + tokensUsed, + metadata, + } = params; if (amount <= 0) { return { @@ -251,7 +267,8 @@ class AgentBudgetService { if (budget.is_paused) { return { success: false, - newBalance: Number(budget.allocated_budget) - Number(budget.spent_budget), + newBalance: + Number(budget.allocated_budget) - Number(budget.spent_budget), dailySpent: Number(budget.daily_spent), error: budget.pause_reason || "Agent budget is paused", }; @@ -372,14 +389,20 @@ class AgentBudgetService { ) { // Trigger auto-refill asynchronously - failure is non-critical this.triggerAutoRefill(agentId).catch((err) => - logger.error("[AgentBudgets] Auto-refill failed", { agentId, error: String(err) }), + logger.error("[AgentBudgets] Auto-refill failed", { + agentId, + error: String(err), + }), ); } // Check for low budget alert (fire-and-forget, logged on failure) if (newBalance.lte(lowThreshold) && !budget.low_budget_alert_sent) { this.sendLowBudgetAlert(agentId, newBalance.toNumber()).catch((err) => - logger.error("[AgentBudgets] Low budget alert failed", { agentId, error: String(err) }), + logger.error("[AgentBudgets] Low budget alert failed", { + agentId, + error: String(err), + }), ); } @@ -426,7 +449,8 @@ class AgentBudgetService { }); } catch (error) { if (error instanceof InsufficientCreditsError) { - const currentBalance = Number(budget.allocated_budget) - Number(budget.spent_budget); + const currentBalance = + Number(budget.allocated_budget) - Number(budget.spent_budget); return { success: false, newBalance: currentBalance, @@ -458,11 +482,14 @@ class AgentBudgetService { allocated_budget: newAllocated.toFixed(4), // Unpause if was paused due to depletion is_paused: - lockedBudget.is_paused && lockedBudget.pause_reason === "Budget depleted" + lockedBudget.is_paused && + lockedBudget.pause_reason === "Budget depleted" ? false : lockedBudget.is_paused, pause_reason: - lockedBudget.pause_reason === "Budget depleted" ? null : lockedBudget.pause_reason, + lockedBudget.pause_reason === "Budget depleted" + ? null + : lockedBudget.pause_reason, low_budget_alert_sent: false, // Reset alert flag updated_at: new Date(), }) @@ -625,7 +652,9 @@ class AgentBudgetService { }; if (settings.dailyLimit !== undefined) { - updateData.daily_limit = settings.dailyLimit ? String(settings.dailyLimit) : null; + updateData.daily_limit = settings.dailyLimit + ? String(settings.dailyLimit) + : null; } if (settings.autoRefillEnabled !== undefined) { updateData.auto_refill_enabled = settings.autoRefillEnabled; @@ -649,7 +678,10 @@ class AgentBudgetService { : null; } - await dbWrite.update(agentBudgets).set(updateData).where(eq(agentBudgets.id, budget.id)); + await dbWrite + .update(agentBudgets) + .set(updateData) + .where(eq(agentBudgets.id, budget.id)); logger.info("[AgentBudgets] Settings updated", { agentId, settings }); @@ -659,7 +691,10 @@ class AgentBudgetService { /** * Get transaction history for a budget */ - async getTransactions(agentId: string, limit = 50): Promise { + async getTransactions( + agentId: string, + limit = 50, + ): Promise { const budget = await this.getBudget(agentId); if (!budget) { return []; @@ -696,13 +731,20 @@ class AgentBudgetService { const budgetsToRefill = await dbRead .select() .from(agentBudgets) - .where(and(eq(agentBudgets.auto_refill_enabled, true), eq(agentBudgets.is_paused, false))); + .where( + and( + eq(agentBudgets.auto_refill_enabled, true), + eq(agentBudgets.is_paused, false), + ), + ); let processed = 0; const failedAgents: string[] = []; for (const budget of budgetsToRefill) { - const available = new Decimal(budget.allocated_budget).minus(budget.spent_budget); + const available = new Decimal(budget.allocated_budget).minus( + budget.spent_budget, + ); const threshold = budget.auto_refill_threshold ? new Decimal(budget.auto_refill_threshold) : new Decimal(10); @@ -760,7 +802,10 @@ class AgentBudgetService { } } - private async sendLowBudgetAlert(agentId: string, balance: number): Promise { + private async sendLowBudgetAlert( + agentId: string, + balance: number, + ): Promise { // Mark alert as sent await dbWrite .update(agentBudgets) diff --git a/packages/lib/services/agent-monetization.ts b/packages/lib/services/agent-monetization.ts index faff7c0c7..04d647d88 100644 --- a/packages/lib/services/agent-monetization.ts +++ b/packages/lib/services/agent-monetization.ts @@ -20,7 +20,11 @@ import Decimal from "decimal.js"; import { eq, sql } from "drizzle-orm"; import { dbRead, dbWrite } from "@/db/client"; import { userCharacters } from "@/db/schemas/user-characters"; -import { calculateCost, estimateRequestCost, getProviderFromModel } from "@/lib/pricing"; +import { + calculateCost, + estimateRequestCost, + getProviderFromModel, +} from "@/lib/pricing"; import { logger } from "@/lib/utils/logger"; import { creditsService } from "./credits"; import { redeemableEarningsService } from "./redeemable-earnings"; @@ -79,7 +83,9 @@ class AgentMonetizationService { /** * Get agent monetization info */ - async getAgentMonetization(agentId: string): Promise { + async getAgentMonetization( + agentId: string, + ): Promise { const agent = await dbRead.query.userCharacters.findFirst({ where: eq(userCharacters.id, agentId), }); @@ -108,7 +114,9 @@ class AgentMonetizationService { monetizationEnabled: boolean, ): Promise<{ baseCost: number; creatorMarkup: number; totalCost: number }> { const baseCost = await estimateRequestCost(model, messages); - const creatorMarkup = monetizationEnabled ? baseCost * (markupPercentage / 100) : 0; + const creatorMarkup = monetizationEnabled + ? baseCost * (markupPercentage / 100) + : 0; const totalCost = baseCost + creatorMarkup; return { baseCost, creatorMarkup, totalCost }; @@ -159,8 +167,16 @@ class AgentMonetizationService { async recordCreatorEarnings( params: RecordEarningsParams, ): Promise<{ success: boolean; error?: string }> { - const { agentId, agentName, ownerId, earnings, consumerOrgId, model, tokens, protocol } = - params; + const { + agentId, + agentName, + ownerId, + earnings, + consumerOrgId, + model, + tokens, + protocol, + } = params; if (earnings <= 0) { return { success: true }; // No earnings to record @@ -254,7 +270,9 @@ class AgentMonetizationService { usage.outputTokens, ); - const actualCreatorMarkup = monetizationEnabled ? actualBaseCost * (markupPercentage / 100) : 0; + const actualCreatorMarkup = monetizationEnabled + ? actualBaseCost * (markupPercentage / 100) + : 0; const actualTotal = actualBaseCost + actualCreatorMarkup; // Record creator earnings if monetization is enabled @@ -408,7 +426,10 @@ class AgentMonetizationService { ); const topAgents = monetizedAgents - .sort((a, b) => Number(b.total_creator_earnings) - Number(a.total_creator_earnings)) + .sort( + (a, b) => + Number(b.total_creator_earnings) - Number(a.total_creator_earnings), + ) .slice(0, 5) .map((a) => ({ id: a.id, diff --git a/packages/lib/services/agents/agents.ts b/packages/lib/services/agents/agents.ts index c529f6b23..74741501f 100644 --- a/packages/lib/services/agents/agents.ts +++ b/packages/lib/services/agents/agents.ts @@ -22,7 +22,10 @@ import { ContentType, type Media } from "@elizaos/core"; import { memoriesRepository, participantsRepository } from "@/db/repositories"; import { type AgentInfo, agentsRepository } from "@/db/repositories/agents"; -import { agentStateCache, type RoomContext } from "@/lib/cache/agent-state-cache"; +import { + agentStateCache, + type RoomContext, +} from "@/lib/cache/agent-state-cache"; import { cache as cacheClient } from "@/lib/cache/client"; import { distributedLocks } from "@/lib/cache/distributed-locks"; import { CacheTTL } from "@/lib/cache/keys"; @@ -165,9 +168,13 @@ class AgentsService { }); if (created) { - logger.info(`[Agents Service] Created default Eliza agent ${DEFAULT_AGENT_ID}`); + logger.info( + `[Agents Service] Created default Eliza agent ${DEFAULT_AGENT_ID}`, + ); } else { - logger.debug(`[Agents Service] Default Eliza agent already exists (race condition)`); + logger.debug( + `[Agents Service] Default Eliza agent already exists (race condition)`, + ); } } @@ -193,13 +200,17 @@ class AgentsService { } // Extract character data - const characterData = character.character_data as Record | undefined; + const characterData = character.character_data as + | Record + | undefined; // Create agent from character const created = await agentsRepository.create({ id: characterId as `${string}-${string}-${string}-${string}-${string}`, name: character.name, - bio: characterData?.bio as string | string[] | undefined as string[] | undefined, + bio: characterData?.bio as string | string[] | undefined as + | string[] + | undefined, settings: { ...(character.avatar_url ? { avatarUrl: character.avatar_url } : {}), ...(characterData?.settings as Record | undefined), @@ -209,9 +220,13 @@ class AgentsService { if (!created) { // Agent was created by another process (race condition), that's fine - logger.debug(`[Agents Service] Agent ${characterId} already exists (race condition)`); + logger.debug( + `[Agents Service] Agent ${characterId} already exists (race condition)`, + ); } else { - logger.info(`[Agents Service] Created agent ${characterId} from character ${character.name}`); + logger.info( + `[Agents Service] Created agent ${characterId} from character ${character.name}`, + ); } return characterId; @@ -256,7 +271,8 @@ class AgentsService { */ async getOrCreateRoom(entityId: string, agentId: string): Promise { // Use repository to check for existing rooms - const existingRoomIds = await participantsRepository.findRoomsByEntityId(entityId); + const existingRoomIds = + await participantsRepository.findRoomsByEntityId(entityId); if (existingRoomIds && existingRoomIds.length > 0) { logger.debug( @@ -273,7 +289,9 @@ class AgentsService { name: "New Chat", }); - logger.info(`[Agents Service] Created new room ${room.id} for entity ${entityId}`); + logger.info( + `[Agents Service] Created new room ${room.id} for entity ${entityId}`, + ); return room.id; } @@ -286,20 +304,28 @@ class AgentsService { const { roomId, message, streaming, attachments, characterId } = input; // Acquire distributed lock with retry - const lock = await distributedLocks.acquireRoomLockWithRetry(roomId, 60000, { - maxRetries: 10, - initialDelayMs: 100, - maxDelayMs: 2000, - }); + const lock = await distributedLocks.acquireRoomLockWithRetry( + roomId, + 60000, + { + maxRetries: 10, + initialDelayMs: 100, + maxDelayMs: 2000, + }, + ); if (!lock) { - throw new Error("Room is currently processing another message. Maximum wait time exceeded."); + throw new Error( + "Room is currently processing another message. Maximum wait time exceeded.", + ); } try { // Use specific character runtime if provided (e.g., from WhatsApp/SMS routing), // otherwise fall back to the default system runtime. - const userContext = userContextService.createSystemContext(AgentMode.CHAT); + const userContext = userContextService.createSystemContext( + AgentMode.CHAT, + ); if (characterId) { userContext.characterId = characterId; } @@ -312,22 +338,28 @@ class AgentsService { id: crypto.randomUUID(), url: attachment.url, title: attachment.filename, - contentType: attachment.type === "image" ? ContentType.IMAGE : ContentType.DOCUMENT, + contentType: + attachment.type === "image" + ? ContentType.IMAGE + : ContentType.DOCUMENT, })) ?? []; const messageHandler = createMessageHandler(runtime, userContext); - const { message: agentMessage, usage: messageUsage } = await messageHandler.process({ - roomId, - text: message, - attachments: mediaAttachments, - characterId, - }); + const { message: agentMessage, usage: messageUsage } = + await messageHandler.process({ + roomId, + text: message, + attachments: mediaAttachments, + characterId, + }); await agentEventEmitter.emitResponseComplete( roomId, agentMessage, messageUsage || { inputTokens: Math.ceil(message.length / 4), - outputTokens: Math.ceil(((agentMessage.content.text as string) || "").length / 4), + outputTokens: Math.ceil( + ((agentMessage.content.text as string) || "").length / 4, + ), model: "eliza-agent", }, ); @@ -341,7 +373,9 @@ class AgentsService { timestamp: new Date(agentMessage.createdAt || Date.now()), usage: { inputTokens: Math.ceil(message.length / 4), - outputTokens: Math.ceil(((agentMessage.content.text as string) || "").length / 4), + outputTokens: Math.ceil( + ((agentMessage.content.text as string) || "").length / 4, + ), model: "eliza-agent", }, ...(streaming && { @@ -366,7 +400,9 @@ class AgentsService { return cached; } - logger.debug(`[Agents Service] Cache miss for room ${roomId}, fetching from DB`); + logger.debug( + `[Agents Service] Cache miss for room ${roomId}, fetching from DB`, + ); // PERFORMANCE: Fetch messages and participants in parallel const [messages, participantIds] = await Promise.all([ diff --git a/packages/lib/services/agents/rooms.ts b/packages/lib/services/agents/rooms.ts index 3186cbff6..9502c1dd6 100644 --- a/packages/lib/services/agents/rooms.ts +++ b/packages/lib/services/agents/rooms.ts @@ -16,7 +16,10 @@ import { roomsRepository, } from "@/db/repositories"; import { entityTable, participantTable, roomTable } from "@/db/schemas/eliza"; -import { isVisibleDialogueMessage, parseMessageContent } from "@/lib/types/message-content"; +import { + isVisibleDialogueMessage, + parseMessageContent, +} from "@/lib/types/message-content"; /** * Input for creating a room. @@ -161,7 +164,9 @@ export class RoomsService { } }); - const cleanMessages = visibleMessages.filter((_, index) => !indicesToRemove.has(index)); + const cleanMessages = visibleMessages.filter( + (_, index) => !indicesToRemove.has(index), + ); return { room, @@ -187,7 +192,8 @@ export class RoomsService { options?: { includeBuildRooms?: boolean }, ): Promise { // Single query: participants → rooms → last message → user_characters - const roomsWithPreview = await roomsRepository.findRoomsWithPreviewForEntity(entityId); + const roomsWithPreview = + await roomsRepository.findRoomsWithPreviewForEntity(entityId); const includeBuildRooms = options?.includeBuildRooms ?? false; @@ -197,7 +203,9 @@ export class RoomsService { const metadata = room.metadata as { locked?: boolean } | null; const isLocked = metadata?.locked === true; const isBuildRoom = - room.name?.startsWith("[BUILD]") || room.name?.startsWith("[CREATOR]") || false; + room.name?.startsWith("[BUILD]") || + room.name?.startsWith("[CREATOR]") || + false; return { id: room.id, @@ -205,7 +213,8 @@ export class RoomsService { characterId: room.characterId || undefined, characterName: room.characterName || undefined, characterAvatarUrl: room.characterAvatarUrl || undefined, - lastTime: room.lastMessageTime?.getTime() || room.createdAt?.getTime(), + lastTime: + room.lastMessageTime?.getTime() || room.createdAt?.getTime(), lastText: room.lastMessageText?.substring(0, 100) || undefined, isLocked, isBuildRoom, @@ -253,7 +262,10 @@ export class RoomsService { * Prevents race condition where room creation succeeds but participant addition fails, * leaving the system in an inconsistent state. */ - async createRoomWithParticipant(roomInput: CreateRoomInput, entityId: string): Promise { + async createRoomWithParticipant( + roomInput: CreateRoomInput, + entityId: string, + ): Promise { const roomId = roomInput.id || uuidv4(); const agentId = roomInput.agentId; @@ -308,7 +320,10 @@ export class RoomsService { /** * Update room metadata */ - async updateMetadata(roomId: string, metadata: Record): Promise { + async updateMetadata( + roomId: string, + metadata: Record, + ): Promise { await roomsRepository.updateMetadata(roomId, metadata); } @@ -335,12 +350,13 @@ export class RoomsService { participantCount: number; lastMessage?: { time: number; text: string }; } | null> { - const [room, messageCount, participantCount, lastMessage] = await Promise.all([ - roomsRepository.findById(roomId), - memoriesRepository.countMessages(roomId), - participantsRepository.countByRoomId(roomId), - memoriesRepository.findLastMessageForRoom(roomId), - ]); + const [room, messageCount, participantCount, lastMessage] = + await Promise.all([ + roomsRepository.findById(roomId), + memoriesRepository.countMessages(roomId), + participantsRepository.countByRoomId(roomId), + memoriesRepository.findLastMessageForRoom(roomId), + ]); if (!room) { return null; @@ -353,7 +369,10 @@ export class RoomsService { lastMessage: lastMessage ? { time: lastMessage.createdAt || Date.now(), - text: ((lastMessage.content?.text as string) || "").substring(0, 100), + text: ((lastMessage.content?.text as string) || "").substring( + 0, + 100, + ), } : undefined, }; @@ -368,7 +387,10 @@ export class RoomsService { */ async hasAccess(roomId: string, entityId: string): Promise { // First check if user is a participant - const isParticipant = await participantsRepository.isParticipant(roomId, entityId); + const isParticipant = await participantsRepository.isParticipant( + roomId, + entityId, + ); if (isParticipant) { return true; } @@ -401,7 +423,11 @@ export class RoomsService { /** * Add participant to room */ - async addParticipant(roomId: string, entityId: string, agentId: string): Promise { + async addParticipant( + roomId: string, + entityId: string, + agentId: string, + ): Promise { // Ensure entity exists await entitiesRepository.create({ id: entityId, diff --git a/packages/lib/services/ai-app-builder.ts b/packages/lib/services/ai-app-builder.ts index 4145c5874..d6fdaeac2 100644 --- a/packages/lib/services/ai-app-builder.ts +++ b/packages/lib/services/ai-app-builder.ts @@ -94,13 +94,17 @@ export interface PromptResult { } export class AIAppBuilderService { - private async verifyOwnership(sessionId: string, userId: string): Promise { + private async verifyOwnership( + sessionId: string, + userId: string, + ): Promise { const session = await dbRead.query.appSandboxSessions.findFirst({ where: eq(appSandboxSessions.id, sessionId), }); if (!session) throw new Error("Session not found"); - if (session.user_id !== userId) throw new Error("Access denied: You don't own this session"); + if (session.user_id !== userId) + throw new Error("Access denied: You don't own this session"); return session; } @@ -143,7 +147,8 @@ export class AIAppBuilderService { const result = await appFactoryService.createApp( { name: appName, - description: appDescription || `AI-built app (template: ${templateType})`, + description: + appDescription || `AI-built app (template: ${templateType})`, organization_id: organizationId, created_by_user_id: userId, app_url: "https://placeholder.local", @@ -184,11 +189,14 @@ export class AIAppBuilderService { if (existingApp && existingApp.organization_id === organizationId) { // User owns this app - reuse it instead of failing - logger.info("Reusing existing app owned by user (previous session may have failed)", { - appId: existingApp.id, - appName, - slug, - }); + logger.info( + "Reusing existing app owned by user (previous session may have failed)", + { + appId: existingApp.id, + appName, + slug, + }, + ); appId = existingApp.id; appApiKey = await appsService.regenerateApiKey(appId); @@ -250,11 +258,14 @@ export class AIAppBuilderService { githubRepo, }); } catch (error) { - logger.warn("Failed to get authenticated clone URL, falling back to template", { - appId, - githubRepo, - error: error instanceof Error ? error.message : "Unknown", - }); + logger.warn( + "Failed to get authenticated clone URL, falling back to template", + { + appId, + githubRepo, + error: error instanceof Error ? error.message : "Unknown", + }, + ); } } @@ -268,9 +279,12 @@ export class AIAppBuilderService { : undefined; if (!templateUrl) { - logger.info("Template not found in database, using prompt-based template guidance", { - templateType, - }); + logger.info( + "Template not found in database, using prompt-based template guidance", + { + templateType, + }, + ); } else { logger.info("Using template from database", { templateType, @@ -291,7 +305,8 @@ export class AIAppBuilderService { if (isLocalDev) { // Local development: Use postMessage proxy bridge // The sandbox will embed an iframe to /sandbox-proxy which forwards API calls to localhost - const localServerUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const localServerUrl = + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; sandboxEnv.NEXT_PUBLIC_ELIZA_PROXY_URL = localServerUrl; // If ELIZA_API_URL is explicitly set (e.g., ngrok), use it as a direct API URL instead @@ -306,7 +321,8 @@ export class AIAppBuilderService { }); } else { // Production: Use direct API URL - const apiUrl = process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL; + const apiUrl = + process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL; if (apiUrl) { sandboxEnv.NEXT_PUBLIC_ELIZA_API_URL = apiUrl; } @@ -326,7 +342,9 @@ export class AIAppBuilderService { const promptToAnalyze = initialPrompt || appDescription || ""; // Analyze prompt for stateful indicators (fallback if user didn't explicitly choose) - const detectionResult = promptToAnalyze ? getDetailedAnalysis(promptToAnalyze) : null; + const detectionResult = promptToAnalyze + ? getDetailedAnalysis(promptToAnalyze) + : null; // Provision database if user explicitly requested OR if prompt analysis detects need const shouldProvisionDatabase = @@ -342,7 +360,10 @@ export class AIAppBuilderService { }); // Provision database for the app - const dbResult = await userDatabaseService.provisionDatabase(appId, appName || "app"); + const dbResult = await userDatabaseService.provisionDatabase( + appId, + appName || "app", + ); if (dbResult.success && dbResult.connectionUri) { // NOTE: We intentionally do NOT inject DATABASE_URL into the sandbox globally. @@ -357,11 +378,14 @@ export class AIAppBuilderService { }); } else { // Log warning but continue without database (graceful degradation) - logger.warn("Database provisioning failed, continuing without database", { - appId, - error: dbResult.error, - errorCode: dbResult.errorCode, - }); + logger.warn( + "Database provisioning failed, continuing without database", + { + appId, + error: dbResult.error, + errorCode: dbResult.errorCode, + }, + ); } } else if (!shouldProvisionDatabase) { logger.info("No database provisioning needed", { @@ -429,7 +453,8 @@ export class AIAppBuilderService { completed_at: new Date(), } satisfies NewAppBuilderPrompt); - const examplePrompts = EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; + const examplePrompts = + EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; logger.info("AI App Builder session started", { sessionId: session.id, @@ -464,11 +489,16 @@ export class AIAppBuilderService { promptLength: initialPrompt.length, }); - initialPromptResult = await this.sendPrompt(session.id, initialPrompt, userId, { - onToolUse, - onThinking, - abortSignal, - }); + initialPromptResult = await this.sendPrompt( + session.id, + initialPrompt, + userId, + { + onToolUse, + onThinking, + abortSignal, + }, + ); logger.info("Initial prompt completed", { sessionId: session.id, @@ -527,7 +557,9 @@ export class AIAppBuilderService { if (!session.sandbox_id) throw new Error("Sandbox not available"); if (session.status !== "ready") - throw new Error(`Session is not ready. Current status: ${session.status}`); + throw new Error( + `Session is not ready. Current status: ${session.status}`, + ); await dbWrite .update(appSandboxSessions) @@ -594,7 +626,8 @@ export class AIAppBuilderService { completed_at: new Date(), } satisfies NewAppBuilderPrompt); - const messages = (session.claude_messages as BuilderSession["messages"]) || []; + const messages = + (session.claude_messages as BuilderSession["messages"]) || []; messages.push( { role: "user", content: prompt, timestamp: new Date().toISOString() }, { @@ -622,11 +655,12 @@ export class AIAppBuilderService { // Auto-commit to GitHub if files were changed and app has a repo if (result.success && result.filesAffected.length > 0 && session.app_id) { - this.autoCommitToGitHub(session, prompt, result.filesAffected).catch((err) => - logger.warn("Auto-commit failed (non-blocking)", { - sessionId, - error: err instanceof Error ? err.message : "Unknown error", - }), + this.autoCommitToGitHub(session, prompt, result.filesAffected).catch( + (err) => + logger.warn("Auto-commit failed (non-blocking)", { + sessionId, + error: err instanceof Error ? err.message : "Unknown error", + }), ); } @@ -714,11 +748,17 @@ export class AIAppBuilderService { } } - async verifySessionOwnership(sessionId: string, userId: string): Promise { + async verifySessionOwnership( + sessionId: string, + userId: string, + ): Promise { return this.verifyOwnership(sessionId, userId); } - async getSession(sessionId: string, userId: string): Promise { + async getSession( + sessionId: string, + userId: string, + ): Promise { const session = await this.verifyOwnership(sessionId, userId); let currentStatus = session.status; @@ -730,7 +770,11 @@ export class AIAppBuilderService { .set({ status: "timeout", updated_at: new Date() }) .where(eq(appSandboxSessions.id, sessionId)); logger.info("Session marked as timeout due to expiration", { sessionId }); - } else if (session.sandbox_id && currentStatus !== "stopped" && currentStatus !== "timeout") { + } else if ( + session.sandbox_id && + currentStatus !== "stopped" && + currentStatus !== "timeout" + ) { const sandboxStatus = sandboxService.getStatus(session.sandbox_id); if (sandboxStatus === "unknown") { currentStatus = "timeout"; @@ -759,8 +803,10 @@ export class AIAppBuilderService { })) .reverse(); - const templateType = (session.template_type as keyof typeof EXAMPLE_PROMPTS) || "blank"; - const examplePrompts = EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; + const templateType = + (session.template_type as keyof typeof EXAMPLE_PROMPTS) || "blank"; + const examplePrompts = + EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; return { id: session.id, @@ -792,7 +838,9 @@ export class AIAppBuilderService { }); if (!includeInactive) { - return sessions.filter((s) => s.status !== "stopped" && s.status !== "timeout"); + return sessions.filter( + (s) => s.status !== "stopped" && s.status !== "timeout", + ); } return sessions; @@ -809,9 +857,13 @@ export class AIAppBuilderService { await sandboxService.extendTimeout(session.sandbox_id, durationMs); - const currentExpiresAt = session.expires_at ? new Date(session.expires_at) : new Date(); + const currentExpiresAt = session.expires_at + ? new Date(session.expires_at) + : new Date(); const baseTime = - currentExpiresAt.getTime() > Date.now() ? currentExpiresAt.getTime() : Date.now(); + currentExpiresAt.getTime() > Date.now() + ? currentExpiresAt.getTime() + : Date.now(); const newExpiresAt = new Date(baseTime + durationMs); await dbWrite @@ -829,7 +881,11 @@ export class AIAppBuilderService { return { expiresAt: newExpiresAt }; } - async getLogs(sessionId: string, userId: string, tail = 50): Promise { + async getLogs( + sessionId: string, + userId: string, + tail = 50, + ): Promise { const session = await this.verifyOwnership(sessionId, userId); if (!session.sandbox_id) return []; return sandboxService.getLogs(session.sandbox_id, tail); @@ -881,7 +937,11 @@ export class AIAppBuilderService { userId: string, options: { onProgress?: (progress: SandboxProgress) => void; - onRestoreProgress?: (progress: { current: number; total: number; filePath: string }) => void; + onRestoreProgress?: (progress: { + current: number; + total: number; + filePath: string; + }) => void; } = {}, ): Promise { const { onProgress, onRestoreProgress } = options; @@ -911,8 +971,10 @@ export class AIAppBuilderService { })) .reverse(); - const templateType = (session.template_type as keyof typeof EXAMPLE_PROMPTS) || "blank"; - const examplePrompts = EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; + const templateType = + (session.template_type as keyof typeof EXAMPLE_PROMPTS) || "blank"; + const examplePrompts = + EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; // Get the app's github repo if available let githubRepo: string | null = null; @@ -939,7 +1001,9 @@ export class AIAppBuilderService { // Check if session can be resumed (must be timeout or stopped) if (session.status !== "timeout" && session.status !== "stopped") { - throw new Error(`Session cannot be resumed. Current status: ${session.status}`); + throw new Error( + `Session cannot be resumed. Current status: ${session.status}`, + ); } logger.info("Resuming session", { @@ -979,10 +1043,13 @@ export class AIAppBuilderService { ); if (reconnected) { - logger.info("Successfully reconnected to existing sandbox (fast path)", { - sessionId, - sandboxId: reconnected.sandboxId, - }); + logger.info( + "Successfully reconnected to existing sandbox (fast path)", + { + sessionId, + sandboxId: reconnected.sandboxId, + }, + ); sandboxData = reconnected; // Report instant restore @@ -994,14 +1061,25 @@ export class AIAppBuilderService { }); } } else { - logger.info("Reconnection failed, falling back to new sandbox creation", { - sessionId, - }); - sandboxData = await this.createNewSandboxForResume(session, githubRepo, options); + logger.info( + "Reconnection failed, falling back to new sandbox creation", + { + sessionId, + }, + ); + sandboxData = await this.createNewSandboxForResume( + session, + githubRepo, + options, + ); } } else { // No existing sandbox info - create new - sandboxData = await this.createNewSandboxForResume(session, githubRepo, options); + sandboxData = await this.createNewSandboxForResume( + session, + githubRepo, + options, + ); } const expiresAt = new Date(Date.now() + 30 * 60 * 1000); @@ -1053,8 +1131,10 @@ export class AIAppBuilderService { })) .reverse(); - const templateType = (session.template_type as keyof typeof EXAMPLE_PROMPTS) || "blank"; - const examplePrompts = EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; + const templateType = + (session.template_type as keyof typeof EXAMPLE_PROMPTS) || "blank"; + const examplePrompts = + EXAMPLE_PROMPTS[templateType] || EXAMPLE_PROMPTS.blank; logger.info("Session resumed successfully", { sessionId, @@ -1085,7 +1165,11 @@ export class AIAppBuilderService { githubRepo: string | null, options: { onProgress?: (progress: SandboxProgress) => void; - onRestoreProgress?: (progress: { current: number; total: number; filePath: string }) => void; + onRestoreProgress?: (progress: { + current: number; + total: number; + filePath: string; + }) => void; }, ): Promise<{ sandboxId: string; @@ -1131,7 +1215,8 @@ export class AIAppBuilderService { const sandboxEnv: Record = {}; if (isLocalDev) { - const localServerUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const localServerUrl = + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; sandboxEnv.NEXT_PUBLIC_ELIZA_PROXY_URL = localServerUrl; if (process.env.ELIZA_API_URL) { @@ -1139,7 +1224,8 @@ export class AIAppBuilderService { delete sandboxEnv.NEXT_PUBLIC_ELIZA_PROXY_URL; } } else { - const apiUrl = process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL; + const apiUrl = + process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL; if (apiUrl) { sandboxEnv.NEXT_PUBLIC_ELIZA_API_URL = apiUrl; } diff --git a/packages/lib/services/ai-billing.ts b/packages/lib/services/ai-billing.ts index 241b9bd94..eeac0cc73 100644 --- a/packages/lib/services/ai-billing.ts +++ b/packages/lib/services/ai-billing.ts @@ -143,7 +143,8 @@ export function estimateInputTokens( const messageText = messages .map((m) => { if (typeof m.content === "string") return m.content; - if (m.content && typeof m.content === "object") return JSON.stringify(m.content); + if (m.content && typeof m.content === "object") + return JSON.stringify(m.content); return ""; }) .join(" "); @@ -190,7 +191,9 @@ export async function billUsage( // Apply affiliate markup if present let _appliedAffiliateMarkup = false; if (context.affiliateCode) { - const affiliate = await affiliatesRepository.getAffiliateCodeByCode(context.affiliateCode); + const affiliate = await affiliatesRepository.getAffiliateCodeByCode( + context.affiliateCode, + ); if (affiliate && affiliate.is_active) { const markupPercent = Number(affiliate.markup_percent) / 100; @@ -275,7 +278,9 @@ export async function billFlatUsage( const provider = context.provider ?? getProviderFromModel(context.model); if (context.affiliateCode) { - const affiliate = await affiliatesRepository.getAffiliateCodeByCode(context.affiliateCode); + const affiliate = await affiliatesRepository.getAffiliateCodeByCode( + context.affiliateCode, + ); if (affiliate && affiliate.is_active) { const markupPercent = Number(affiliate.markup_percent) / 100; const affiliateEarnings = totalCost * markupPercent; @@ -301,11 +306,14 @@ export async function billFlatUsage( dedupeBySourceId: true, }) .catch((err) => { - logger.error("[AI Billing] Failed to add flat-operation affiliate earnings", { - error: err instanceof Error ? err.message : String(err), - affiliateId: affiliate.id, - amount: affiliateEarnings, - }); + logger.error( + "[AI Billing] Failed to add flat-operation affiliate earnings", + { + error: err instanceof Error ? err.message : String(err), + affiliateId: affiliate.id, + amount: affiliateEarnings, + }, + ); }); } } @@ -356,7 +364,13 @@ export async function recordUsageAnalytics( latencyMs?: number; } = {}, ): Promise { - const { type = "chat", isSuccessful = true, errorMessage, content, prompt } = options; + const { + type = "chat", + isSuccessful = true, + errorMessage, + content, + prompt, + } = options; const provider = context.provider ?? getProviderFromModel(context.model); try { @@ -435,7 +449,8 @@ export async function recordUsageAnalytics( } catch (trajError) { // Trajectory logging is non-critical — never block the request logger.warn("[AI Billing] Failed to log trajectory", { - error: trajError instanceof Error ? trajError.message : String(trajError), + error: + trajError instanceof Error ? trajError.message : String(trajError), }); } } catch (error) { diff --git a/packages/lib/services/ai-pricing-definitions.ts b/packages/lib/services/ai-pricing-definitions.ts index 139532d7c..e18fa2aeb 100644 --- a/packages/lib/services/ai-pricing-definitions.ts +++ b/packages/lib/services/ai-pricing-definitions.ts @@ -62,7 +62,10 @@ export interface ElevenLabsSnapshotEntry { modelId: string; provider: "elevenlabs"; billingSource: "elevenlabs"; - productFamily: Exclude; + productFamily: Exclude< + PricingProductFamily, + "language" | "embedding" | "image" | "video" + >; chargeType: string; unit: PricingChargeUnit; unitPrice: number; @@ -174,7 +177,8 @@ export const SUPPORTED_VIDEO_MODELS: SupportedVideoModelDefinition[] = [ provider: "fal", billingSource: "fal", label: "Kling 3 Standard", - pageUrl: "https://fal.ai/models/fal-ai/kling-video/v3/standard/text-to-video", + pageUrl: + "https://fal.ai/models/fal-ai/kling-video/v3/standard/text-to-video", pricingParser: "kling", defaultParameters: { durationSeconds: 5, @@ -213,7 +217,8 @@ export const SUPPORTED_VIDEO_MODELS: SupportedVideoModelDefinition[] = [ provider: "fal", billingSource: "fal", label: "Hailuo 2.3 Standard", - pageUrl: "https://fal.ai/models/fal-ai/minimax/hailuo-2.3/standard/text-to-video", + pageUrl: + "https://fal.ai/models/fal-ai/minimax/hailuo-2.3/standard/text-to-video", pricingParser: "hailuo_standard", defaultParameters: { durationSeconds: 6, @@ -224,7 +229,8 @@ export const SUPPORTED_VIDEO_MODELS: SupportedVideoModelDefinition[] = [ provider: "fal", billingSource: "fal", label: "Hailuo 2.3 Pro", - pageUrl: "https://fal.ai/models/fal-ai/minimax/hailuo-2.3/pro/text-to-video", + pageUrl: + "https://fal.ai/models/fal-ai/minimax/hailuo-2.3/pro/text-to-video", pricingParser: "hailuo_pro", defaultParameters: { durationSeconds: 6, @@ -398,8 +404,12 @@ export const ELEVENLABS_SNAPSHOT_PRICING: ElevenLabsSnapshotEntry[] = [ }, ] as const; -export const SUPPORTED_VIDEO_MODEL_IDS = SUPPORTED_VIDEO_MODELS.map((model) => model.modelId); -export const SUPPORTED_IMAGE_MODEL_IDS = SUPPORTED_IMAGE_MODELS.map((model) => model.modelId); +export const SUPPORTED_VIDEO_MODEL_IDS = SUPPORTED_VIDEO_MODELS.map( + (model) => model.modelId, +); +export const SUPPORTED_IMAGE_MODEL_IDS = SUPPORTED_IMAGE_MODELS.map( + (model) => model.modelId, +); export function getSupportedVideoModelDefinition(modelId: string) { return SUPPORTED_VIDEO_MODELS.find((model) => model.modelId === modelId); diff --git a/packages/lib/services/ai-pricing.ts b/packages/lib/services/ai-pricing.ts index 39b48323c..d9c4fafb3 100644 --- a/packages/lib/services/ai-pricing.ts +++ b/packages/lib/services/ai-pricing.ts @@ -126,7 +126,9 @@ function applyPlatformMarkup(baseCost: Decimal): { }; } -function normalizeDimensionValue(value: unknown): string | number | boolean | null { +function normalizeDimensionValue( + value: unknown, +): string | number | boolean | null { if ( value === null || typeof value === "string" || @@ -154,13 +156,22 @@ export function normalizePricingDimensions( ); } -export function buildDimensionKey(dimensions?: Record): string { +export function buildDimensionKey( + dimensions?: Record, +): string { const normalized = normalizePricingDimensions(dimensions); - return Object.keys(normalized).length === 0 ? "*" : JSON.stringify(normalized); + return Object.keys(normalized).length === 0 + ? "*" + : JSON.stringify(normalized); } -function dimensionsAreSubset(candidate: PricingDimensions, requested: PricingDimensions): boolean { - return Object.entries(candidate).every(([key, value]) => requested[key] === value); +function dimensionsAreSubset( + candidate: PricingDimensions, + requested: PricingDimensions, +): boolean { + return Object.entries(candidate).every( + ([key, value]) => requested[key] === value, + ); } function sourcePriorityForKind(sourceKind: string): number { @@ -237,7 +248,10 @@ function hashPreparedEntry(entry: PreparedPricingEntry): string { .digest("hex"); } -function toDbEntry(entry: PreparedPricingEntry, timestamp: Date): NewAiPricingEntry { +function toDbEntry( + entry: PreparedPricingEntry, + timestamp: Date, +): NewAiPricingEntry { const dimensions = normalizePricingDimensions(entry.dimensions); return { @@ -255,7 +269,8 @@ function toDbEntry(entry: PreparedPricingEntry, timestamp: Date): NewAiPricingEn source_url: entry.sourceUrl, source_hash: hashPreparedEntry(entry), fetched_at: entry.fetchedAt ?? timestamp, - stale_after: entry.staleAfter ?? new Date(timestamp.getTime() + EXTERNAL_CACHE_TTL_MS), + stale_after: + entry.staleAfter ?? new Date(timestamp.getTime() + EXTERNAL_CACHE_TTL_MS), effective_from: timestamp, priority: entry.priority ?? sourcePriorityForKind(entry.sourceKind), is_active: true, @@ -298,7 +313,9 @@ function parseNumericPrice(value: unknown): number | null { return null; } -function inferGatewayProductFamily(model: GatewayCatalogModel): PricingProductFamily { +function inferGatewayProductFamily( + model: GatewayCatalogModel, +): PricingProductFamily { const tags = new Set(model.tags ?? []); if (model.type === "embedding" || model.id.includes("embedding")) { @@ -317,7 +334,9 @@ function inferGatewayProductFamily(model: GatewayCatalogModel): PricingProductFa return "language"; } -function buildGatewayPreparedEntries(model: GatewayCatalogModel): PreparedPricingEntry[] { +function buildGatewayPreparedEntries( + model: GatewayCatalogModel, +): PreparedPricingEntry[] { const pricing = model.pricing ?? {}; const productFamily = inferGatewayProductFamily(model); const provider = inferProviderFromCanonicalModel(model.id); @@ -325,7 +344,12 @@ function buildGatewayPreparedEntries(model: GatewayCatalogModel): PreparedPricin const staleAfter = new Date(fetchedAt.getTime() + EXTERNAL_CACHE_TTL_MS); const entries: PreparedPricingEntry[] = []; - for (const chargeType of ["input", "output", "input_cache_read", "input_cache_write"]) { + for (const chargeType of [ + "input", + "output", + "input_cache_read", + "input_cache_write", + ]) { const unitPrice = parseNumericPrice(pricing[chargeType]); if (unitPrice == null) continue; @@ -427,7 +451,9 @@ function buildGatewayPreparedEntries(model: GatewayCatalogModel): PreparedPricin return entries; } -function inferOpenRouterProductFamily(model: OpenRouterCatalogModel): PricingProductFamily { +function inferOpenRouterProductFamily( + model: OpenRouterCatalogModel, +): PricingProductFamily { const modality = model.architecture?.modality ?? ""; if (model.id.includes("embedding")) { return "embedding"; @@ -438,7 +464,9 @@ function inferOpenRouterProductFamily(model: OpenRouterCatalogModel): PricingPro return "language"; } -function buildOpenRouterPreparedEntries(model: OpenRouterCatalogModel): PreparedPricingEntry[] { +function buildOpenRouterPreparedEntries( + model: OpenRouterCatalogModel, +): PreparedPricingEntry[] { const pricing = model.pricing ?? {}; const provider = inferProviderFromCanonicalModel(model.id); const productFamily = inferOpenRouterProductFamily(model); @@ -536,13 +564,19 @@ function parseFalPricingEntries( switch (model.pricingParser) { case "veo": { - const match = paragraph.match(/\$([\d.]+)\s+\(audio off\)\s+or\s+\$([\d.]+)\s+\(audio on\)/i); + const match = paragraph.match( + /\$([\d.]+)\s+\(audio off\)\s+or\s+\$([\d.]+)\s+\(audio on\)/i, + ); if (!match) { throw new Error(`Unable to parse Veo pricing paragraph: ${paragraph}`); } - entries.push(buildFalEntry(model, "second", Number(match[1]), { audio: false })); - entries.push(buildFalEntry(model, "second", Number(match[2]), { audio: true })); + entries.push( + buildFalEntry(model, "second", Number(match[1]), { audio: false }), + ); + entries.push( + buildFalEntry(model, "second", Number(match[2]), { audio: true }), + ); break; } case "veo31": { @@ -550,20 +584,36 @@ function parseFalPricingEntries( /\$([\d.]+)\s+without audio\s+or\s+\$([\d.]+)\s+with audio\s+for 720p or 1080p.*?\$([\d.]+)\s+per second without audio,\s+or\s+\$([\d.]+)\s+with/i, ); if (!match) { - throw new Error(`Unable to parse Veo 3.1 pricing paragraph: ${paragraph}`); + throw new Error( + `Unable to parse Veo 3.1 pricing paragraph: ${paragraph}`, + ); } for (const resolution of ["720p", "1080p"]) { entries.push( - buildFalEntry(model, "second", Number(match[1]), { resolution, audio: false }), + buildFalEntry(model, "second", Number(match[1]), { + resolution, + audio: false, + }), + ); + entries.push( + buildFalEntry(model, "second", Number(match[2]), { + resolution, + audio: true, + }), ); - entries.push(buildFalEntry(model, "second", Number(match[2]), { resolution, audio: true })); } entries.push( - buildFalEntry(model, "second", Number(match[3]), { resolution: "4k", audio: false }), + buildFalEntry(model, "second", Number(match[3]), { + resolution: "4k", + audio: false, + }), ); entries.push( - buildFalEntry(model, "second", Number(match[4]), { resolution: "4k", audio: true }), + buildFalEntry(model, "second", Number(match[4]), { + resolution: "4k", + audio: true, + }), ); break; } @@ -572,20 +622,34 @@ function parseFalPricingEntries( /\$([\d.]+)\s+for 720p with audio,\s+\$([\d.]+)\s+for 720p without audio,\s+\$([\d.]+)\s+for 1080p with audio\s+or\s+\$([\d.]+)\s+for 1080p without audio/i, ); if (!match) { - throw new Error(`Unable to parse Veo 3.1 Lite pricing paragraph: ${paragraph}`); + throw new Error( + `Unable to parse Veo 3.1 Lite pricing paragraph: ${paragraph}`, + ); } entries.push( - buildFalEntry(model, "second", Number(match[1]), { resolution: "720p", audio: true }), + buildFalEntry(model, "second", Number(match[1]), { + resolution: "720p", + audio: true, + }), ); entries.push( - buildFalEntry(model, "second", Number(match[2]), { resolution: "720p", audio: false }), + buildFalEntry(model, "second", Number(match[2]), { + resolution: "720p", + audio: false, + }), ); entries.push( - buildFalEntry(model, "second", Number(match[3]), { resolution: "1080p", audio: true }), + buildFalEntry(model, "second", Number(match[3]), { + resolution: "1080p", + audio: true, + }), ); entries.push( - buildFalEntry(model, "second", Number(match[4]), { resolution: "1080p", audio: false }), + buildFalEntry(model, "second", Number(match[4]), { + resolution: "1080p", + audio: false, + }), ); break; } @@ -594,36 +658,61 @@ function parseFalPricingEntries( /\$([\d.]+)\s+\(audio off\)\s+or\s+\$([\d.]+)\s+\(audio on\)(?:,\s+if voice control is used while generating audio you will be charged\s+\$([\d.]+))?/i, ); if (!match) { - throw new Error(`Unable to parse Kling pricing paragraph: ${paragraph}`); + throw new Error( + `Unable to parse Kling pricing paragraph: ${paragraph}`, + ); } entries.push( - buildFalEntry(model, "second", Number(match[1]), { audio: false, voiceControl: false }), + buildFalEntry(model, "second", Number(match[1]), { + audio: false, + voiceControl: false, + }), ); entries.push( - buildFalEntry(model, "second", Number(match[2]), { audio: true, voiceControl: false }), + buildFalEntry(model, "second", Number(match[2]), { + audio: true, + voiceControl: false, + }), ); if (match[3]) { entries.push( - buildFalEntry(model, "second", Number(match[3]), { audio: true, voiceControl: true }), + buildFalEntry(model, "second", Number(match[3]), { + audio: true, + voiceControl: true, + }), ); } break; } case "hailuo_standard": { - const match = paragraph.match(/\$([\d.]+)\s+per\s+6 second.*?\$([\d.]+)\s+per\s+10 second/i); + const match = paragraph.match( + /\$([\d.]+)\s+per\s+6 second.*?\$([\d.]+)\s+per\s+10 second/i, + ); if (!match) { - throw new Error(`Unable to parse Hailuo standard pricing paragraph: ${paragraph}`); + throw new Error( + `Unable to parse Hailuo standard pricing paragraph: ${paragraph}`, + ); } - entries.push(buildFalEntry(model, "request", Number(match[1]), { durationSeconds: 6 })); - entries.push(buildFalEntry(model, "request", Number(match[2]), { durationSeconds: 10 })); + entries.push( + buildFalEntry(model, "request", Number(match[1]), { + durationSeconds: 6, + }), + ); + entries.push( + buildFalEntry(model, "request", Number(match[2]), { + durationSeconds: 10, + }), + ); break; } case "hailuo_pro": { const match = paragraph.match(/\$([\d.]+)\s+per video generation/i); if (!match) { - throw new Error(`Unable to parse Hailuo pro pricing paragraph: ${paragraph}`); + throw new Error( + `Unable to parse Hailuo pro pricing paragraph: ${paragraph}`, + ); } entries.push(buildFalEntry(model, "request", Number(match[1]), {})); @@ -637,8 +726,16 @@ function parseFalPricingEntries( throw new Error(`Unable to parse Wan pricing paragraph: ${paragraph}`); } - entries.push(buildFalEntry(model, "second", Number(match[1]), { resolution: "720p" })); - entries.push(buildFalEntry(model, "second", Number(match[2]), { resolution: "1080p" })); + entries.push( + buildFalEntry(model, "second", Number(match[1]), { + resolution: "720p", + }), + ); + entries.push( + buildFalEntry(model, "second", Number(match[2]), { + resolution: "1080p", + }), + ); break; } case "pixverse": { @@ -646,7 +743,9 @@ function parseFalPricingEntries( /\$([\d.]+)\s+for 360p and 540p,\s+\$([\d.]+)\s+for 720p,\s+and\s+\$([\d.]+)\s+for 1080p\.\s+Enabling audio adds\s+\$([\d.]+)\s+for 360p\/540p\/720p,\s+and\s+\$([\d.]+)\s+for 1080p\.\s+For 8-second videos, costs are 2x the 5-second base;\s+for 10-second videos, costs are 2.2x the 5-second base/i, ); if (!match) { - throw new Error(`Unable to parse PixVerse pricing paragraph: ${paragraph}`); + throw new Error( + `Unable to parse PixVerse pricing paragraph: ${paragraph}`, + ); } const baseByResolution: Record = { @@ -665,7 +764,9 @@ function parseFalPricingEntries( for (const [duration, multiplier] of Object.entries(multipliers)) { const numericDuration = Number(duration); - for (const [resolution, basePrice] of Object.entries(baseByResolution)) { + for (const [resolution, basePrice] of Object.entries( + baseByResolution, + )) { if (numericDuration === 10 && resolution === "1080p") { continue; } @@ -679,7 +780,8 @@ function parseFalPricingEntries( }), ); - const audioPrice = (basePrice + audioAddByResolution[resolution]) * multiplier; + const audioPrice = + (basePrice + audioAddByResolution[resolution]) * multiplier; entries.push( buildFalEntry(model, "request", audioPrice, { durationSeconds: numericDuration, @@ -749,15 +851,21 @@ async function getCachedExternalEntries( async function fetchGatewayCatalogEntries(): Promise { return await getCachedExternalEntries("gateway", async () => { - const payload = await fetchJson<{ data?: GatewayCatalogModel[] }>(GATEWAY_MODELS_URL); + const payload = await fetchJson<{ data?: GatewayCatalogModel[] }>( + GATEWAY_MODELS_URL, + ); const models = Array.isArray(payload.data) ? payload.data : []; return models.flatMap((model) => buildGatewayPreparedEntries(model)); }); } -async function fetchOpenRouterCatalogEntries(): Promise { +async function fetchOpenRouterCatalogEntries(): Promise< + PreparedPricingEntry[] +> { return await getCachedExternalEntries("openrouter", async () => { - const payload = await fetchJson<{ data?: OpenRouterCatalogModel[] }>(OPENROUTER_MODELS_URL); + const payload = await fetchJson<{ data?: OpenRouterCatalogModel[] }>( + OPENROUTER_MODELS_URL, + ); const models = Array.isArray(payload.data) ? payload.data : []; return models.flatMap((model) => buildOpenRouterPreparedEntries(model)); }); @@ -800,7 +908,9 @@ async function fetchElevenLabsEntries(): Promise { }); } -async function fetchEntriesForSource(source: PriceLookupSource): Promise { +async function fetchEntriesForSource( + source: PriceLookupSource, +): Promise { switch (source) { case "gateway": case "openai": @@ -822,7 +932,10 @@ function chooseBestMatchingEntry( requestedDimensions: PricingDimensions, ): PreparedPricingEntry | null { const matching = entries.filter((entry) => - dimensionsAreSubset(normalizePricingDimensions(entry.dimensions), requestedDimensions), + dimensionsAreSubset( + normalizePricingDimensions(entry.dimensions), + requestedDimensions, + ), ); if (matching.length === 0) { @@ -854,7 +967,10 @@ async function resolvePreparedPricingEntry(params: { }): Promise { const canonicalModel = canonicalModelId(params.model, params.provider); const requestedDimensions = normalizePricingDimensions(params.dimensions); - const sources = normalizeBillingSourceCandidates(params.billingSource, params.provider); + const sources = normalizeBillingSourceCandidates( + params.billingSource, + params.provider, + ); for (const source of sources) { const persistedEntries = await aiPricingRepository.listActiveEntries({ @@ -892,7 +1008,10 @@ async function resolvePreparedPricingEntry(params: { ); } -function computeCostFromEntry(entry: PreparedPricingEntry, quantity: number): FlatOperationCost { +function computeCostFromEntry( + entry: PreparedPricingEntry, + quantity: number, +): FlatOperationCost { const baseCost = asDecimal(entry.unitPrice).mul(quantity); const markedUp = applyPlatformMarkup(baseCost); @@ -959,14 +1078,18 @@ export async function calculateTextCostFromCatalog(params: { billingSource: params.billingSource, provider: params.provider, model: canonicalModel, - productFamily: params.model.includes("embedding") ? "embedding" : "language", + productFamily: params.model.includes("embedding") + ? "embedding" + : "language", chargeType: "input", }); const outputEntry = await resolvePreparedPricingEntry({ billingSource: params.billingSource, provider: params.provider, model: canonicalModel, - productFamily: params.model.includes("embedding") ? "embedding" : "language", + productFamily: params.model.includes("embedding") + ? "embedding" + : "language", chargeType: "output", }).catch(() => null); @@ -981,7 +1104,9 @@ export async function calculateTextCostFromCatalog(params: { return { inputCost: inputTotals.totalCost, outputCost: outputTotals.totalCost, - totalCost: decimalToMoney(asDecimal(inputTotals.totalCost).plus(outputTotals.totalCost)), + totalCost: decimalToMoney( + asDecimal(inputTotals.totalCost).plus(outputTotals.totalCost), + ), baseInputCost: inputTotals.baseTotalCost, baseOutputCost: outputTotals.baseTotalCost, baseTotalCost: decimalToMoney(baseInputCost.plus(baseOutputCost)), @@ -1220,7 +1345,12 @@ async function refreshSourceEntries( } export async function refreshPricingCatalog( - sources: PricingRefreshSource[] = ["gateway", "openrouter", "fal", "elevenlabs"], + sources: PricingRefreshSource[] = [ + "gateway", + "openrouter", + "fal", + "elevenlabs", + ], ) { const results = []; @@ -1234,9 +1364,13 @@ export async function refreshPricingCatalog( if (sources.includes("openrouter")) { results.push( - await refreshSourceEntries("openrouter", OPENROUTER_MODELS_URL, async () => { - return await fetchOpenRouterCatalogEntries(); - }), + await refreshSourceEntries( + "openrouter", + OPENROUTER_MODELS_URL, + async () => { + return await fetchOpenRouterCatalogEntries(); + }, + ), ); } @@ -1250,9 +1384,13 @@ export async function refreshPricingCatalog( if (sources.includes("elevenlabs")) { results.push( - await refreshSourceEntries("elevenlabs", "https://elevenlabs.io/pricing/api", async () => { - return await fetchElevenLabsEntries(); - }), + await refreshSourceEntries( + "elevenlabs", + "https://elevenlabs.io/pricing/api", + async () => { + return await fetchElevenLabsEntries(); + }, + ), ); } @@ -1273,7 +1411,9 @@ export async function listPersistedPricingEntries(filters?: { const entries = await aiPricingRepository.listActiveEntries({ billingSource: filters?.billingSource, provider: filters?.provider, - model: filters?.model ? canonicalModelId(filters.model, filters.provider) : undefined, + model: filters?.model + ? canonicalModelId(filters.model, filters.provider) + : undefined, productFamily: filters?.productFamily, chargeType: filters?.chargeType, }); diff --git a/packages/lib/services/alb-priority-manager.ts b/packages/lib/services/alb-priority-manager.ts index 880ccdb4f..6f12b669b 100644 --- a/packages/lib/services/alb-priority-manager.ts +++ b/packages/lib/services/alb-priority-manager.ts @@ -31,19 +31,27 @@ export class DatabasePriorityManager { * @param userId - The user ID * @param projectName - The project name (each user can have multiple projects) */ - async allocatePriority(userId: string, projectName: string = "default"): Promise { + async allocatePriority( + userId: string, + projectName: string = "default", + ): Promise { logger.info( `[ALB allocatePriority] Starting allocation for user ${userId}, project ${projectName}`, ); let lastError: Error | null = null; - for (let attempt = 1; attempt <= DatabasePriorityManager.MAX_RETRIES; attempt++) { + for ( + let attempt = 1; + attempt <= DatabasePriorityManager.MAX_RETRIES; + attempt++ + ) { try { return await this.tryAllocatePriority(userId, projectName, attempt); } catch (error) { lastError = error as Error; - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); // Check if it's a unique constraint violation (can retry) const isConstraintViolation = @@ -51,12 +59,17 @@ export class DatabasePriorityManager { errorMessage.includes("duplicate") || errorMessage.includes("23505"); // PostgreSQL unique violation code - if (isConstraintViolation && attempt < DatabasePriorityManager.MAX_RETRIES) { + if ( + isConstraintViolation && + attempt < DatabasePriorityManager.MAX_RETRIES + ) { logger.warn( `[ALB allocatePriority] Attempt ${attempt} failed with constraint violation, retrying...`, ); // Small random delay to reduce collision probability - await new Promise((resolve) => setTimeout(resolve, Math.random() * 100 * attempt)); + await new Promise((resolve) => + setTimeout(resolve, Math.random() * 100 * attempt), + ); continue; } @@ -65,7 +78,10 @@ export class DatabasePriorityManager { } } - throw lastError || new Error("Failed to allocate ALB priority after max retries"); + throw ( + lastError || + new Error("Failed to allocate ALB priority after max retries") + ); } /** @@ -83,7 +99,10 @@ export class DatabasePriorityManager { // Check if user+project already has a priority record (active or expired) const existing = await tx.query.albPriorities.findFirst({ - where: and(eq(albPriorities.userId, userId), eq(albPriorities.projectName, projectName)), + where: and( + eq(albPriorities.userId, userId), + eq(albPriorities.projectName, projectName), + ), }); if (existing && !existing.expiresAt) { @@ -102,7 +121,12 @@ export class DatabasePriorityManager { const [reactivated] = await tx .update(albPriorities) .set({ expiresAt: null, createdAt: new Date() }) - .where(and(eq(albPriorities.userId, userId), eq(albPriorities.projectName, projectName))) + .where( + and( + eq(albPriorities.userId, userId), + eq(albPriorities.projectName, projectName), + ), + ) .returning(); logger.info( @@ -122,7 +146,9 @@ export class DatabasePriorityManager { // Validate we haven't exceeded ALB limit if (nextPriority > 50000) { - throw new Error("ALB priority limit exceeded - too many containers created (max 50,000)"); + throw new Error( + "ALB priority limit exceeded - too many containers created (max 50,000)", + ); } logger.info( @@ -155,14 +181,22 @@ export class DatabasePriorityManager { * @param userId - The user ID * @param projectName - The project name (each user can have multiple projects) */ - async releasePriority(userId: string, projectName: string = "default"): Promise { + async releasePriority( + userId: string, + projectName: string = "default", + ): Promise { // Set expiry date (1 hour from now for audit trail) const expiryDate = new Date(Date.now() + 60 * 60 * 1000); const result = await dbWrite .update(albPriorities) .set({ expiresAt: expiryDate }) - .where(and(eq(albPriorities.userId, userId), eq(albPriorities.projectName, projectName))) + .where( + and( + eq(albPriorities.userId, userId), + eq(albPriorities.projectName, projectName), + ), + ) .returning(); if (result.length > 0) { @@ -170,7 +204,9 @@ export class DatabasePriorityManager { `✅ Released ALB priority ${result[0].priority} for user ${userId} project ${projectName} (expires: ${expiryDate.toISOString()})`, ); } else { - logger.warn(`⚠️ No ALB priority found for user ${userId} project ${projectName}`); + logger.warn( + `⚠️ No ALB priority found for user ${userId} project ${projectName}`, + ); } } @@ -180,9 +216,15 @@ export class DatabasePriorityManager { * @param userId - The user ID * @param projectName - The project name (each user can have multiple projects) */ - async getPriority(userId: string, projectName: string = "default"): Promise { + async getPriority( + userId: string, + projectName: string = "default", + ): Promise { const result = await dbRead.query.albPriorities.findFirst({ - where: and(eq(albPriorities.userId, userId), eq(albPriorities.projectName, projectName)), + where: and( + eq(albPriorities.userId, userId), + eq(albPriorities.projectName, projectName), + ), }); // Only return if not expired @@ -201,7 +243,12 @@ export class DatabasePriorityManager { const now = new Date(); const deleted = await dbWrite .delete(albPriorities) - .where(and(isNotNull(albPriorities.expiresAt), lt(albPriorities.expiresAt, now))) + .where( + and( + isNotNull(albPriorities.expiresAt), + lt(albPriorities.expiresAt, now), + ), + ) .returning(); if (deleted.length > 0) { diff --git a/packages/lib/services/analytics.ts b/packages/lib/services/analytics.ts index f93449854..a535c586c 100644 --- a/packages/lib/services/analytics.ts +++ b/packages/lib/services/analytics.ts @@ -4,7 +4,10 @@ * Uses the usage-records repository for all data access */ -import type { CostBreakdownItem, UsageStats } from "@/db/repositories/usage-records"; +import type { + CostBreakdownItem, + UsageStats, +} from "@/db/repositories/usage-records"; import { usageRecordsRepository } from "@/db/repositories/usage-records"; import { cache as cacheClient } from "@/lib/cache/client"; import { CacheKeys, CacheStaleTTL } from "@/lib/cache/keys"; @@ -30,12 +33,15 @@ export class AnalyticsService { const dateRange = `${options?.startDate?.toISOString() || "null"}-${options?.endDate?.toISOString() || "null"}`; const cacheKey = CacheKeys.analytics.stats(organizationId, dateRange); - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.analytics.stats, () => - usageRecordsRepository.getStatsByOrganization( - organizationId, - options?.startDate, - options?.endDate, - ), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.analytics.stats, + () => + usageRecordsRepository.getStatsByOrganization( + organizationId, + options?.startDate, + options?.endDate, + ), ); if (data === null) { @@ -65,12 +71,18 @@ export class AnalyticsService { options.endDate.toISOString(), ); - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.analytics.overview, () => - usageRecordsRepository.getUsageTimeSeries(organizationId, options), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.analytics.overview, + () => usageRecordsRepository.getUsageTimeSeries(organizationId, options), ); const result = - data || (await usageRecordsRepository.getUsageTimeSeries(organizationId, options)); + data || + (await usageRecordsRepository.getUsageTimeSeries( + organizationId, + options, + )); if (options.maxRows && result.length > options.maxRows) { return result.slice(0, options.maxRows); @@ -91,11 +103,15 @@ export class AnalyticsService { const params = `${options?.startDate?.toISOString() || "null"}-${options?.endDate?.toISOString() || "null"}-${options?.limit || 0}`; const cacheKey = CacheKeys.analytics.userBreakdown(organizationId, params); - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.analytics.breakdown, () => - usageRecordsRepository.getUsageByUser(organizationId, options), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.analytics.breakdown, + () => usageRecordsRepository.getUsageByUser(organizationId, options), ); - const result = data || (await usageRecordsRepository.getUsageByUser(organizationId, options)); + const result = + data || + (await usageRecordsRepository.getUsageByUser(organizationId, options)); if (options?.maxRows && result.length > options.maxRows) { return result.slice(0, options.maxRows); @@ -118,12 +134,19 @@ export class AnalyticsService { options?.endDate?.toISOString() || "null", ); - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.analytics.breakdown, () => - usageRecordsRepository.getProviderBreakdown(organizationId, options), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.analytics.breakdown, + () => + usageRecordsRepository.getProviderBreakdown(organizationId, options), ); const result = - data || (await usageRecordsRepository.getProviderBreakdown(organizationId, options)); + data || + (await usageRecordsRepository.getProviderBreakdown( + organizationId, + options, + )); if (options?.maxRows && result.length > options.maxRows) { return result.slice(0, options.maxRows); @@ -147,12 +170,15 @@ export class AnalyticsService { options?.endDate?.toISOString() || "null", ); - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.analytics.breakdown, () => - usageRecordsRepository.getModelBreakdown(organizationId, options), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.analytics.breakdown, + () => usageRecordsRepository.getModelBreakdown(organizationId, options), ); const result = - data || (await usageRecordsRepository.getModelBreakdown(organizationId, options)); + data || + (await usageRecordsRepository.getModelBreakdown(organizationId, options)); if (options?.maxRows && result.length > options.maxRows) { return result.slice(0, options.maxRows); @@ -166,7 +192,11 @@ export class AnalyticsService { currentPeriod: { startDate: Date; endDate: Date }, previousPeriod: { startDate: Date; endDate: Date }, ) { - return await usageRecordsRepository.getTrendData(organizationId, currentPeriod, previousPeriod); + return await usageRecordsRepository.getTrendData( + organizationId, + currentPeriod, + previousPeriod, + ); } async getCostBreakdown( @@ -187,12 +217,23 @@ export class AnalyticsService { this.serializeBreakdownOptions(options), ); - const data = await cacheClient.getWithSWR(cacheKey, CacheStaleTTL.analytics.breakdown, () => - usageRecordsRepository.getCostBreakdown(organizationId, dimension, options), + const data = await cacheClient.getWithSWR( + cacheKey, + CacheStaleTTL.analytics.breakdown, + () => + usageRecordsRepository.getCostBreakdown( + organizationId, + dimension, + options, + ), ); if (data === null) { - return await usageRecordsRepository.getCostBreakdown(organizationId, dimension, options); + return await usageRecordsRepository.getCostBreakdown( + organizationId, + dimension, + options, + ); } return data; @@ -202,38 +243,51 @@ export class AnalyticsService { * Get complete analytics overview with caching * Handles date calculations, parallel data fetching, and response formatting */ - async getOverview(organizationId: string, timeRange: "daily" | "weekly" | "monthly") { + async getOverview( + organizationId: string, + timeRange: "daily" | "weekly" | "monthly", + ) { const cacheKey = CacheKeys.analytics.overview(organizationId, timeRange); const data = await cacheClient.getWithSWR( cacheKey, CacheStaleTTL.analytics.overview, async () => { - const { startDate, endDate, granularity, previousStartDate, previousEndDate } = - this.calculateDateRanges(timeRange); - - const [summary, timeSeries, providerBreakdown, modelBreakdown, trends] = await Promise.all([ - usageRecordsRepository.getStatsByOrganization(organizationId, startDate, endDate), - usageRecordsRepository.getUsageTimeSeries(organizationId, { - startDate, - endDate, - granularity, - }), - usageRecordsRepository.getProviderBreakdown(organizationId, { - startDate, - endDate, - }), - usageRecordsRepository.getModelBreakdown(organizationId, { - startDate, - endDate, - limit: 20, - }), - usageRecordsRepository.getTrendData( - organizationId, - { startDate, endDate }, - { startDate: previousStartDate, endDate: previousEndDate }, - ), - ]); + const { + startDate, + endDate, + granularity, + previousStartDate, + previousEndDate, + } = this.calculateDateRanges(timeRange); + + const [summary, timeSeries, providerBreakdown, modelBreakdown, trends] = + await Promise.all([ + usageRecordsRepository.getStatsByOrganization( + organizationId, + startDate, + endDate, + ), + usageRecordsRepository.getUsageTimeSeries(organizationId, { + startDate, + endDate, + granularity, + }), + usageRecordsRepository.getProviderBreakdown(organizationId, { + startDate, + endDate, + }), + usageRecordsRepository.getModelBreakdown(organizationId, { + startDate, + endDate, + limit: 20, + }), + usageRecordsRepository.getTrendData( + organizationId, + { startDate, endDate }, + { startDate: previousStartDate, endDate: previousEndDate }, + ), + ]); return { timeSeries: timeSeries.map((point) => ({ @@ -261,7 +315,9 @@ export class AnalyticsService { totalTokens: summary.totalInputTokens + summary.totalOutputTokens, successRate: summary.successRate, avgCostPerRequest: - summary.totalRequests > 0 ? summary.totalCost / summary.totalRequests : 0, + summary.totalRequests > 0 + ? summary.totalCost / summary.totalRequests + : 0, avgLatency: 0, activeApiKeys: 0, }, @@ -277,31 +333,41 @@ export class AnalyticsService { ); if (data === null) { - const { startDate, endDate, granularity, previousStartDate, previousEndDate } = - this.calculateDateRanges(timeRange); - - const [summary, timeSeries, providerBreakdown, modelBreakdown, trends] = await Promise.all([ - usageRecordsRepository.getStatsByOrganization(organizationId, startDate, endDate), - usageRecordsRepository.getUsageTimeSeries(organizationId, { - startDate, - endDate, - granularity, - }), - usageRecordsRepository.getProviderBreakdown(organizationId, { - startDate, - endDate, - }), - usageRecordsRepository.getModelBreakdown(organizationId, { - startDate, - endDate, - limit: 20, - }), - usageRecordsRepository.getTrendData( - organizationId, - { startDate, endDate }, - { startDate: previousStartDate, endDate: previousEndDate }, - ), - ]); + const { + startDate, + endDate, + granularity, + previousStartDate, + previousEndDate, + } = this.calculateDateRanges(timeRange); + + const [summary, timeSeries, providerBreakdown, modelBreakdown, trends] = + await Promise.all([ + usageRecordsRepository.getStatsByOrganization( + organizationId, + startDate, + endDate, + ), + usageRecordsRepository.getUsageTimeSeries(organizationId, { + startDate, + endDate, + granularity, + }), + usageRecordsRepository.getProviderBreakdown(organizationId, { + startDate, + endDate, + }), + usageRecordsRepository.getModelBreakdown(organizationId, { + startDate, + endDate, + limit: 20, + }), + usageRecordsRepository.getTrendData( + organizationId, + { startDate, endDate }, + { startDate: previousStartDate, endDate: previousEndDate }, + ), + ]); return { timeSeries: timeSeries.map((point) => ({ @@ -329,7 +395,9 @@ export class AnalyticsService { totalTokens: summary.totalInputTokens + summary.totalOutputTokens, successRate: summary.successRate, avgCostPerRequest: - summary.totalRequests > 0 ? summary.totalCost / summary.totalRequests : 0, + summary.totalRequests > 0 + ? summary.totalCost / summary.totalRequests + : 0, avgLatency: 0, activeApiKeys: 0, }, @@ -454,7 +522,8 @@ export const getTrendData = ( organizationId: string, currentPeriod: { startDate: Date; endDate: Date }, previousPeriod: { startDate: Date; endDate: Date }, -) => analyticsService.getTrendData(organizationId, currentPeriod, previousPeriod); +) => + analyticsService.getTrendData(organizationId, currentPeriod, previousPeriod); export const getCostBreakdown = ( organizationId: string, @@ -470,6 +539,8 @@ export const getCostBreakdown = ( ) => analyticsService.getCostBreakdown(organizationId, dimension, options); // Validation helper for granularity -export function validateGranularity(value: string): value is "hour" | "day" | "week" | "month" { +export function validateGranularity( + value: string, +): value is "hour" | "day" | "week" | "month" { return ["hour", "day", "week", "month"].includes(value); } diff --git a/packages/lib/services/anonymous-session-creator.ts b/packages/lib/services/anonymous-session-creator.ts index 94b9b478a..68cf5fe60 100644 --- a/packages/lib/services/anonymous-session-creator.ts +++ b/packages/lib/services/anonymous-session-creator.ts @@ -32,7 +32,8 @@ export interface CreateAnonymousUserAndSessionResult { export async function createAnonymousUserAndSession( params: CreateAnonymousUserAndSessionParams, ): Promise { - const { sessionToken, expiresAt, ipAddress, userAgent, messagesLimit } = params; + const { sessionToken, expiresAt, ipAddress, userAgent, messagesLimit } = + params; const [newUser] = await dbWrite .insert(users) @@ -55,11 +56,14 @@ export async function createAnonymousUserAndSession( messages_limit: messagesLimit, }); - logger.info("[anonymous-session-creator] Created new anonymous user and session", { - userId: newUser.id, - sessionId: newSession.id, - expiresAt, - }); + logger.info( + "[anonymous-session-creator] Created new anonymous user and session", + { + userId: newUser.id, + sessionId: newSession.id, + expiresAt, + }, + ); return { newUser, newSession }; } diff --git a/packages/lib/services/api-keys.ts b/packages/lib/services/api-keys.ts index 38b0ac9b6..2a0fc453a 100644 --- a/packages/lib/services/api-keys.ts +++ b/packages/lib/services/api-keys.ts @@ -5,7 +5,11 @@ */ import crypto from "crypto"; -import { type ApiKey, apiKeysRepository, type NewApiKey } from "@/db/repositories"; +import { + type ApiKey, + apiKeysRepository, + type NewApiKey, +} from "@/db/repositories"; import { cache } from "@/lib/cache/client"; import { CacheKeys, CacheTTL } from "@/lib/cache/keys"; import { API_KEY_PREFIX_LENGTH } from "@/lib/pricing"; @@ -79,7 +83,9 @@ export class ApiKeysService { return await apiKeysRepository.listByOrganization(organizationId); } - async create(data: Omit): Promise<{ + async create( + data: Omit, + ): Promise<{ apiKey: ApiKey; plainKey: string; }> { @@ -98,7 +104,10 @@ export class ApiKeysService { }; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { // Get the key first to invalidate cache const existing = await apiKeysRepository.findById(id); if (existing) { @@ -123,7 +132,10 @@ export class ApiKeysService { } async deactivateUserKeysByName(userId: string, name: string): Promise { - const existingKeys = await apiKeysRepository.findByUserAndName(userId, name); + const existingKeys = await apiKeysRepository.findByUserAndName( + userId, + name, + ); for (const key of existingKeys) { await this.invalidateCache(key.key_hash); diff --git a/packages/lib/services/app-analytics.ts b/packages/lib/services/app-analytics.ts index 6410e7786..6addcdb99 100644 --- a/packages/lib/services/app-analytics.ts +++ b/packages/lib/services/app-analytics.ts @@ -25,14 +25,26 @@ export class AppAnalyticsService { responseTimeMs?: number; metadata?: Record; }): Promise { - const { appId, userId, requestType, success, creditsUsed = "0.00", metadata } = params; + const { + appId, + userId, + requestType, + success, + creditsUsed = "0.00", + metadata, + } = params; // Track app usage await appsRepository.incrementUsage(appId, creditsUsed); // Track app user activity if userId is provided if (userId) { - await appsRepository.trackAppUserActivity(appId, userId, creditsUsed, metadata); + await appsRepository.trackAppUserActivity( + appId, + userId, + creditsUsed, + metadata, + ); } logger.info("Tracked app request", { diff --git a/packages/lib/services/app-builder-ai-sdk.ts b/packages/lib/services/app-builder-ai-sdk.ts index f7e367124..8fb7c2894 100644 --- a/packages/lib/services/app-builder-ai-sdk.ts +++ b/packages/lib/services/app-builder-ai-sdk.ts @@ -21,7 +21,10 @@ import { gateway } from "@ai-sdk/gateway"; import type { ModelMessage, UserModelMessage } from "ai"; import { streamText, tool } from "ai"; -import { buildFullAppPrompt, type FullAppTemplateType } from "@/lib/fragments/prompt"; +import { + buildFullAppPrompt, + type FullAppTemplateType, +} from "@/lib/fragments/prompt"; import { mergeAnthropicCotProviderOptions } from "@/lib/providers/anthropic-thinking"; import { logger } from "@/lib/utils/logger"; @@ -48,7 +51,11 @@ export type { SandboxInstance } from "./sandbox"; export interface AppBuilderStreamCallbacks { onToolCall?: (toolName: string, args: unknown) => void | Promise; - onToolResult?: (toolName: string, args: unknown, result: string) => void | Promise; + onToolResult?: ( + toolName: string, + args: unknown, + result: string, + ) => void | Promise; onThinking?: (text: string) => void | Promise; onReasoning?: (text: string) => void | Promise; // Chain-of-thought tokens } @@ -241,7 +248,8 @@ export class AppBuilderAISDK { const tailwindWarning = globalsCss && - (globalsCss.includes("@tailwind") || globalsCss.includes("tailwindcss/tailwind.css")) + (globalsCss.includes("@tailwind") || + globalsCss.includes("tailwindcss/tailwind.css")) ? `\n⚠️ CRITICAL: globals.css uses Tailwind v3 syntax. Replace with: @import "tailwindcss";\n` : ""; @@ -285,14 +293,18 @@ CRITICAL RULES: }); // Build multimodal user content if images are attached - const buildUserMessage = (text: string, includeImages: boolean = false): UserModelMessage => { + const buildUserMessage = ( + text: string, + includeImages: boolean = false, + ): UserModelMessage => { if (!includeImages || images.length === 0) { return { role: "user", content: text }; } // Multimodal content with images const contentParts: Array< - { type: "text"; text: string } | { type: "image"; image: string; mediaType?: string } + | { type: "text"; text: string } + | { type: "image"; image: string; mediaType?: string } > = [{ type: "text", text: text }]; // Add images @@ -343,7 +355,8 @@ CRITICAL RULES: inputSchema: toolSchemas.read_file, }), check_build: tool({ - description: "Check build status. Call ONCE at the end, not after each file.", + description: + "Check build status. Call ONCE at the end, not after each file.", inputSchema: toolSchemas.check_build, }), list_files: tool({ @@ -383,14 +396,18 @@ CRITICAL RULES: }); } else if (part.type === "tool-call") { // Tool calls are handled separately below via result.toolCalls - } else if (part.type === "reasoning-start" || part.type === "reasoning-end") { + } else if ( + part.type === "reasoning-start" || + part.type === "reasoning-end" + ) { // Reasoning lifecycle events - check for text content // Different providers may include reasoning text in different ways const partAny = part as Record; if (partAny.text && typeof partAny.text === "string") { reasoningText += partAny.text; yield { type: "reasoning", text: partAny.text }; - if (callbacks?.onReasoning) await callbacks.onReasoning(partAny.text); + if (callbacks?.onReasoning) + await callbacks.onReasoning(partAny.text); } } else { // Handle any other reasoning-related types dynamically @@ -398,7 +415,10 @@ CRITICAL RULES: const partAny = part as Record; const partType = String(partAny.type || ""); - if (partType.includes("reasoning") || partType.includes("thinking")) { + if ( + partType.includes("reasoning") || + partType.includes("thinking") + ) { // Extract text from reasoning-related parts const text = (partAny.text as string) || @@ -421,13 +441,15 @@ CRITICAL RULES: reasoning?: string | Promise; reasoningText?: string | Promise; }; - const finalReasoning = (await resultAny.reasoning) || (await resultAny.reasoningText); + const finalReasoning = + (await resultAny.reasoning) || (await resultAny.reasoningText); if (finalReasoning && typeof finalReasoning === "string") { // If we got reasoning from the result that wasn't captured during streaming if (!reasoningText.includes(finalReasoning)) { reasoningText += finalReasoning; yield { type: "reasoning", text: finalReasoning }; - if (callbacks?.onReasoning) await callbacks.onReasoning(finalReasoning); + if (callbacks?.onReasoning) + await callbacks.onReasoning(finalReasoning); } } } catch { @@ -465,7 +487,10 @@ CRITICAL RULES: input?: unknown; toolName: string; }; - const toolArgs = (tcAny.args ?? tcAny.input ?? {}) as Record; + const toolArgs = (tcAny.args ?? tcAny.input ?? {}) as Record< + string, + unknown + >; yield { type: "tool_call", @@ -473,21 +498,18 @@ CRITICAL RULES: args: toolArgs, reasoningContext: reasoningText || assistantText || undefined, // Include reasoning that led to this tool }; - if (callbacks?.onToolCall) await callbacks.onToolCall(tc.toolName, toolArgs); + if (callbacks?.onToolCall) + await callbacks.onToolCall(tc.toolName, toolArgs); toolCallCount++; // Use shared tool executor - const { result: toolResult, filesAffected: affected } = await sharedExecuteToolCall( - sandbox, - tc.toolName, - toolArgs, - { + const { result: toolResult, filesAffected: affected } = + await sharedExecuteToolCall(sandbox, tc.toolName, toolArgs, { abortSignal, sandboxId, appId, - }, - ); + }); if (affected) { filesAffected.push(...affected); @@ -600,7 +622,8 @@ CRITICAL RULES: // Ignore build check errors during error handling } } - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); logger.error("AI execution failed", { sandboxId, error: errorMessage }); yield { type: "error", error: errorMessage }; @@ -618,7 +641,9 @@ CRITICAL RULES: type: "complete", result: { output: errorFinalOutput, - reasoning: errorShouldIncludeReasoning ? errorFinalReasoning : undefined, + reasoning: errorShouldIncludeReasoning + ? errorFinalReasoning + : undefined, filesAffected: [...new Set(filesAffected)], success: false, error: errorMessage, diff --git a/packages/lib/services/app-cleanup.ts b/packages/lib/services/app-cleanup.ts index 89ce0c509..90838d73a 100644 --- a/packages/lib/services/app-cleanup.ts +++ b/packages/lib/services/app-cleanup.ts @@ -52,7 +52,10 @@ interface CleanupOptions { /** * Make authenticated request to Vercel API */ -async function vercelFetch(path: string, options: RequestInit = {}): Promise { +async function vercelFetch( + path: string, + options: RequestInit = {}, +): Promise { if (!VERCEL_TOKEN) { throw new Error("VERCEL_TOKEN is not configured"); } @@ -106,8 +109,11 @@ async function stopAppSandboxes(appId: string): Promise<{ }) .where(eq(appSandboxSessions.id, session.id)); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - errors.push(`Failed to stop sandbox ${session.sandbox_id}: ${errorMessage}`); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + errors.push( + `Failed to stop sandbox ${session.sandbox_id}: ${errorMessage}`, + ); logger.warn("[AppCleanup] Failed to stop sandbox", { appId, sessionId: session.id, @@ -117,7 +123,8 @@ async function stopAppSandboxes(appId: string): Promise<{ } } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; errors.push(`Failed to query sandbox sessions: ${errorMessage}`); logger.error("[AppCleanup] Failed to query sandbox sessions", { appId, @@ -163,8 +170,11 @@ async function removeAppDomains(appId: string): Promise<{ domain: domain.custom_domain, }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - errors.push(`Failed to remove domain ${domain.custom_domain}: ${errorMessage}`); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + errors.push( + `Failed to remove domain ${domain.custom_domain}: ${errorMessage}`, + ); logger.warn("[AppCleanup] Failed to remove custom domain", { appId, domain: domain.custom_domain, @@ -176,9 +186,12 @@ async function removeAppDomains(appId: string): Promise<{ // Remove subdomain from Vercel project if it exists if (fullSubdomain && domain.vercel_project_id) { try { - await vercelFetch(`/v9/projects/${domain.vercel_project_id}/domains/${fullSubdomain}`, { - method: "DELETE", - }); + await vercelFetch( + `/v9/projects/${domain.vercel_project_id}/domains/${fullSubdomain}`, + { + method: "DELETE", + }, + ); removed++; logger.info("[AppCleanup] Removed subdomain from Vercel project", { appId, @@ -187,14 +200,20 @@ async function removeAppDomains(appId: string): Promise<{ }); } catch (error) { // Ignore 404 errors - domain may not exist - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; if (!errorMessage.includes("404")) { - errors.push(`Failed to remove subdomain from project: ${errorMessage}`); - logger.warn("[AppCleanup] Failed to remove subdomain from project", { - appId, - subdomain: domain.subdomain, - error: errorMessage, - }); + errors.push( + `Failed to remove subdomain from project: ${errorMessage}`, + ); + logger.warn( + "[AppCleanup] Failed to remove subdomain from project", + { + appId, + subdomain: domain.subdomain, + error: errorMessage, + }, + ); } } @@ -204,30 +223,38 @@ async function removeAppDomains(appId: string): Promise<{ await vercelFetch(`/v6/domains/${fullSubdomain}`, { method: "DELETE", }); - logger.info("[AppCleanup] Removed subdomain from Vercel domain registry", { - appId, - subdomain: fullSubdomain, - }); + logger.info( + "[AppCleanup] Removed subdomain from Vercel domain registry", + { + appId, + subdomain: fullSubdomain, + }, + ); } catch (error) { // Ignore 404 and 403 errors - domain may not exist in registry or may not be owned - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; if ( !errorMessage.includes("404") && !errorMessage.includes("403") && !errorMessage.includes("not_found") && !errorMessage.includes("forbidden") ) { - logger.warn("[AppCleanup] Failed to remove subdomain from Vercel registry", { - appId, - subdomain: fullSubdomain, - error: errorMessage, - }); + logger.warn( + "[AppCleanup] Failed to remove subdomain from Vercel registry", + { + appId, + subdomain: fullSubdomain, + error: errorMessage, + }, + ); } } } } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; errors.push(`Failed to query app domains: ${errorMessage}`); logger.error("[AppCleanup] Failed to query app domains", { appId, @@ -273,7 +300,8 @@ async function deleteVercelProject(appId: string): Promise<{ return { deleted: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; // Ignore 404 errors - project may not exist if (errorMessage.includes("404")) { logger.info("[AppCleanup] Vercel project not found (already deleted)", { @@ -292,7 +320,9 @@ async function deleteVercelProject(appId: string): Promise<{ /** * Delete the GitHub repository for an app */ -async function deleteGitHubRepo(app: App): Promise<{ deleted: boolean; error?: string }> { +async function deleteGitHubRepo( + app: App, +): Promise<{ deleted: boolean; error?: string }> { if (!app.github_repo) { logger.info("[AppCleanup] No GitHub repo to delete", { appId: app.id }); return { deleted: false }; @@ -317,7 +347,8 @@ async function deleteGitHubRepo(app: App): Promise<{ deleted: boolean; error?: s return { deleted: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("[AppCleanup] Failed to delete GitHub repo", { appId: app.id, githubRepo: app.github_repo, @@ -341,7 +372,12 @@ async function cleanupSecretBindings(appId: string): Promise<{ // Delete secret bindings where project_id matches this app const result = await dbWrite .delete(secretBindings) - .where(and(eq(secretBindings.project_id, appId), eq(secretBindings.project_type, "app"))) + .where( + and( + eq(secretBindings.project_id, appId), + eq(secretBindings.project_type, "app"), + ), + ) .returning(); removed = result.length; @@ -353,7 +389,8 @@ async function cleanupSecretBindings(appId: string): Promise<{ }); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; errors.push(`Failed to cleanup secret bindings: ${errorMessage}`); logger.error("[AppCleanup] Failed to cleanup secret bindings", { appId, @@ -399,7 +436,8 @@ async function unlinkManagedDomains(appId: string): Promise<{ }); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; errors.push(`Failed to unlink managed domains: ${errorMessage}`); logger.error("[AppCleanup] Failed to unlink managed domains", { appId, @@ -530,7 +568,8 @@ export async function deleteAppWithCleanup( await appsService.delete(appId); logger.info("[AppCleanup] App deleted successfully", { appId }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; cleanupResult.errors.push(`Failed to delete app record: ${errorMessage}`); cleanupResult.success = false; logger.error("[AppCleanup] Failed to delete app record", { diff --git a/packages/lib/services/app-credits.ts b/packages/lib/services/app-credits.ts index c9424b9a6..085c5ff18 100644 --- a/packages/lib/services/app-credits.ts +++ b/packages/lib/services/app-credits.ts @@ -153,7 +153,10 @@ export class AppCreditsService { totalPurchased: number; totalSpent: number; } | null> { - const creditBalance = await appCreditBalancesRepository.findByAppAndUser(appId, userId); + const creditBalance = await appCreditBalancesRepository.findByAppAndUser( + appId, + userId, + ); if (!creditBalance) { return null; @@ -171,7 +174,11 @@ export class AppCreditsService { userId: string, organizationId: string, ): Promise { - return await appCreditBalancesRepository.getOrCreate(appId, userId, organizationId); + return await appCreditBalancesRepository.getOrCreate( + appId, + userId, + organizationId, + ); } /** @@ -209,8 +216,16 @@ export class AppCreditsService { return { newBalance }; } - async processPurchase(params: AppCreditPurchaseParams): Promise { - const { appId, userId, organizationId, purchaseAmount, stripePaymentIntentId } = params; + async processPurchase( + params: AppCreditPurchaseParams, + ): Promise { + const { + appId, + userId, + organizationId, + purchaseAmount, + stripePaymentIntentId, + } = params; const app = await appsRepository.findById(appId); if (!app) { @@ -219,10 +234,11 @@ export class AppCreditsService { // Deduplication check for Stripe webhook retries if (stripePaymentIntentId) { - const existingTransaction = await appEarningsRepository.findTransactionByPaymentIntent( - appId, - stripePaymentIntentId, - ); + const existingTransaction = + await appEarningsRepository.findTransactionByPaymentIntent( + appId, + stripePaymentIntentId, + ); if (existingTransaction) { logger.info("[AppCredits] Duplicate purchase detected, skipping", { appId, @@ -267,12 +283,13 @@ export class AppCreditsService { creditsToAdd, }); - const { balance, newBalance } = await appCreditBalancesRepository.addCredits( - appId, - userId, - organizationId, - creditsToAdd, - ); + const { balance, newBalance } = + await appCreditBalancesRepository.addCredits( + appId, + userId, + organizationId, + creditsToAdd, + ); // Track app user activity for purchase (this will create app_users record if new user) await this.trackAppUserActivity(app, userId, "0.00", { @@ -334,7 +351,9 @@ export class AppCreditsService { }; } - async deductCredits(params: AppCreditDeductionParams): Promise { + async deductCredits( + params: AppCreditDeductionParams, + ): Promise { const { appId, userId, @@ -363,11 +382,17 @@ export class AppCreditsService { // Only apply markup if monetization is enabled // Otherwise, users pay base cost only and creator earns nothing - const markupPercentage = app.monetization_enabled ? Number(app.inference_markup_percentage) : 0; + const markupPercentage = app.monetization_enabled + ? Number(app.inference_markup_percentage) + : 0; const creatorMarkup = baseCost * (markupPercentage / 100); const totalCost = baseCost + creatorMarkup; - const result = await appCreditBalancesRepository.deductCredits(appId, userId, totalCost); + const result = await appCreditBalancesRepository.deductCredits( + appId, + userId, + totalCost, + ); if (!result.success) { return { @@ -384,7 +409,12 @@ export class AppCreditsService { } // Track app user activity (creates/updates app_users record) - await this.trackAppUserActivity(app, userId, totalCost.toFixed(4), metadata); + await this.trackAppUserActivity( + app, + userId, + totalCost.toFixed(4), + metadata, + ); if (app.monetization_enabled && creatorMarkup > 0) { await this.recordCreatorEarnings( @@ -461,7 +491,10 @@ export class AppCreditsService { // Skip reconciliation for negligible differences if (Math.abs(baseCostDifference) < RECONCILIATION_THRESHOLD) { - const currentBalance = await appCreditBalancesRepository.getBalance(appId, userId); + const currentBalance = await appCreditBalancesRepository.getBalance( + appId, + userId, + ); return { reconciled: false, difference: 0, @@ -477,7 +510,10 @@ export class AppCreditsService { logger.error("[AppCredits] App not found during reconciliation", { appId, }); - const currentBalance = await appCreditBalancesRepository.getBalance(appId, userId); + const currentBalance = await appCreditBalancesRepository.getBalance( + appId, + userId, + ); return { reconciled: false, difference: baseCostDifference, @@ -488,10 +524,13 @@ export class AppCreditsService { } // Calculate the total cost difference including markup - const markupPercentage = app.monetization_enabled ? Number(app.inference_markup_percentage) : 0; + const markupPercentage = app.monetization_enabled + ? Number(app.inference_markup_percentage) + : 0; const markupMultiplier = 1 + markupPercentage / 100; const totalCostDifference = baseCostDifference * markupMultiplier; - const creatorMarkupDifference = baseCostDifference * (markupPercentage / 100); + const creatorMarkupDifference = + baseCostDifference * (markupPercentage / 100); if (baseCostDifference < 0) { // REFUND: Actual was less than estimated - need user for org ID @@ -500,7 +539,10 @@ export class AppCreditsService { logger.error("[AppCredits] User not found during reconciliation", { userId, }); - const currentBalance = await appCreditBalancesRepository.getBalance(appId, userId); + const currentBalance = await appCreditBalancesRepository.getBalance( + appId, + userId, + ); return { reconciled: false, difference: baseCostDifference, @@ -522,14 +564,19 @@ export class AppCreditsService { // Reverse creator earnings if monetization is enabled and there was markup if (app.monetization_enabled && creatorEarningsReduction > 0) { - await this.reverseCreatorEarnings(appId, userId, creatorEarningsReduction, { - type: "reconciliation_refund", - baseCostDifference, - estimatedBaseCost, - actualBaseCost, - description, - ...metadata, - }); + await this.reverseCreatorEarnings( + appId, + userId, + creatorEarningsReduction, + { + type: "reconciliation_refund", + baseCostDifference, + estimatedBaseCost, + actualBaseCost, + description, + ...metadata, + }, + ); // Update app revenue counters await dbWrite @@ -570,7 +617,12 @@ export class AppCreditsService { const [balance] = await tx .select() .from(appCreditBalances) - .where(and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId))) + .where( + and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), + ) .for("update"); if (!balance) { @@ -599,7 +651,12 @@ export class AppCreditsService { total_spent: sql`${appCreditBalances.total_spent} + ${additionalCharge}`, updated_at: new Date(), }) - .where(and(eq(appCreditBalances.app_id, appId), eq(appCreditBalances.user_id, userId))); + .where( + and( + eq(appCreditBalances.app_id, appId), + eq(appCreditBalances.user_id, userId), + ), + ); // Update app revenue counters atomically with credit deduction if (app.monetization_enabled && creatorMarkupDifference > 0) { @@ -717,7 +774,9 @@ export class AppCreditsService { } // Only apply markup if monetization is enabled - const markupPercentage = app.monetization_enabled ? Number(app.inference_markup_percentage) : 0; + const markupPercentage = app.monetization_enabled + ? Number(app.inference_markup_percentage) + : 0; const creatorMarkup = baseCost * (markupPercentage / 100); const totalCost = baseCost + creatorMarkup; @@ -769,7 +828,9 @@ export class AppCreditsService { type, amount: String(amount), description: - type === "inference_markup" ? "Inference markup earnings" : "Credit purchase share", + type === "inference_markup" + ? "Inference markup earnings" + : "Credit purchase share", metadata, }); @@ -886,7 +947,12 @@ export class AppCreditsService { creditsUsed: string, metadata?: Record, ): Promise { - await appsRepository.trackAppUserActivity(app.id, userId, creditsUsed, metadata); + await appsRepository.trackAppUserActivity( + app.id, + userId, + creditsUsed, + metadata, + ); } async getMonetizationSettings(appId: string): Promise<{ @@ -918,14 +984,16 @@ export class AppCreditsService { ): Promise { if ( settings.inferenceMarkupPercentage !== undefined && - (settings.inferenceMarkupPercentage < 0 || settings.inferenceMarkupPercentage > 1000) + (settings.inferenceMarkupPercentage < 0 || + settings.inferenceMarkupPercentage > 1000) ) { throw new Error("Inference markup must be between 0% and 1000%"); } if ( settings.purchaseSharePercentage !== undefined && - (settings.purchaseSharePercentage < 0 || settings.purchaseSharePercentage > 100) + (settings.purchaseSharePercentage < 0 || + settings.purchaseSharePercentage > 100) ) { throw new Error("Purchase share must be between 0% and 100%"); } diff --git a/packages/lib/services/app-earnings.ts b/packages/lib/services/app-earnings.ts index 7bec65d2e..555d9bd83 100644 --- a/packages/lib/services/app-earnings.ts +++ b/packages/lib/services/app-earnings.ts @@ -2,7 +2,10 @@ * Service for managing app earnings and revenue tracking. */ -import { type AppEarningsTransaction, appEarningsRepository } from "@/db/repositories/app-earnings"; +import { + type AppEarningsTransaction, + appEarningsRepository, +} from "@/db/repositories/app-earnings"; import { appsRepository } from "@/db/repositories/apps"; import { logger } from "@/lib/utils/logger"; @@ -58,18 +61,39 @@ export class AppEarningsService { allTime: EarningsBreakdown; }> { const now = new Date(); - const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const startOfDay = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ); const startOfWeek = new Date(startOfDay); startOfWeek.setDate(startOfDay.getDate() - startOfDay.getDay()); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const allTimeStart = new Date(2020, 0, 1); // Far past date - const [todayTotals, weekTotals, monthTotals, allTimeTotals] = await Promise.all([ - appEarningsRepository.getTransactionTotalsByType(appId, startOfDay, now), - appEarningsRepository.getTransactionTotalsByType(appId, startOfWeek, now), - appEarningsRepository.getTransactionTotalsByType(appId, startOfMonth, now), - appEarningsRepository.getTransactionTotalsByType(appId, allTimeStart, now), - ]); + const [todayTotals, weekTotals, monthTotals, allTimeTotals] = + await Promise.all([ + appEarningsRepository.getTransactionTotalsByType( + appId, + startOfDay, + now, + ), + appEarningsRepository.getTransactionTotalsByType( + appId, + startOfWeek, + now, + ), + appEarningsRepository.getTransactionTotalsByType( + appId, + startOfMonth, + now, + ), + appEarningsRepository.getTransactionTotalsByType( + appId, + allTimeStart, + now, + ), + ]); return { today: { @@ -114,7 +138,11 @@ export class AppEarningsService { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); - const data = await appEarningsRepository.getDailyEarnings(appId, startDate, endDate); + const data = await appEarningsRepository.getDailyEarnings( + appId, + startDate, + endDate, + ); return data.map((d) => ({ date: d.date, @@ -129,7 +157,11 @@ export class AppEarningsService { options?: { limit?: number; offset?: number; - type?: "inference_markup" | "purchase_share" | "withdrawal" | "adjustment"; + type?: + | "inference_markup" + | "purchase_share" + | "withdrawal" + | "adjustment"; }, ): Promise { if (options?.type) { @@ -182,10 +214,11 @@ export class AppEarningsService { ): Promise<{ success: boolean; message: string; transactionId?: string }> { // Idempotency check: return existing transaction if key was already used if (idempotencyKey) { - const existing = await appEarningsRepository.findTransactionByIdempotencyKey( - appId, - idempotencyKey, - ); + const existing = + await appEarningsRepository.findTransactionByIdempotencyKey( + appId, + idempotencyKey, + ); if (existing) { logger.info("[AppEarnings] Idempotent withdrawal request (duplicate)", { appId, diff --git a/packages/lib/services/app-factory.ts b/packages/lib/services/app-factory.ts index 2c5d3515a..f7dc03811 100644 --- a/packages/lib/services/app-factory.ts +++ b/packages/lib/services/app-factory.ts @@ -63,7 +63,10 @@ export class AppFactoryService { * This is the primary method for app creation that should be used * throughout the application to ensure consistency. */ - async createApp(data: CreateAppInput, options: CreateAppOptions = {}): Promise { + async createApp( + data: CreateAppInput, + options: CreateAppOptions = {}, + ): Promise { const { createGitHubRepo = true, repoPrivate = true, @@ -124,7 +127,8 @@ export class AppFactoryService { const repoTask = (async () => { try { const repoName = - options.repoName || githubReposService.generateRepoName(app.id, app.slug); + options.repoName || + githubReposService.generateRepoName(app.id, app.slug); logger.info("AppFactory: Creating GitHub repo", { appId: app.id, @@ -147,7 +151,9 @@ export class AppFactoryService { }); } catch (repoError) { const errorMessage = - repoError instanceof Error ? repoError.message : "Unknown error creating GitHub repo"; + repoError instanceof Error + ? repoError.message + : "Unknown error creating GitHub repo"; errors.push(`GitHub repo creation failed: ${errorMessage}`); @@ -169,10 +175,11 @@ export class AppFactoryService { preferredSubdomain, }); - const subdomainResult = await vercelDeploymentsService.assignSubdomain( - app.id, - preferredSubdomain || app.slug, - ); + const subdomainResult = + await vercelDeploymentsService.assignSubdomain( + app.id, + preferredSubdomain || app.slug, + ); if (subdomainResult.success && subdomainResult.subdomain) { subdomain = subdomainResult.subdomain; @@ -187,7 +194,9 @@ export class AppFactoryService { productionUrl, }); } else { - errors.push(`Subdomain assignment failed: ${subdomainResult.error || "Unknown error"}`); + errors.push( + `Subdomain assignment failed: ${subdomainResult.error || "Unknown error"}`, + ); logger.warn("AppFactory: Failed to assign subdomain", { appId: app.id, error: subdomainResult.error, @@ -265,10 +274,13 @@ export class AppFactoryService { }); }); } else { - logger.info("AppFactory: Skipping Discord notification for draft app (no production URL)", { - appId: app.id, - appUrl: finalAppUrl, - }); + logger.info( + "AppFactory: Skipping Discord notification for draft app (no production URL)", + { + appId: app.id, + appUrl: finalAppUrl, + }, + ); } return { @@ -287,7 +299,9 @@ export class AppFactoryService { * Create an app without GitHub repository. * Use this for apps that don't need version control. */ - async createAppWithoutRepo(data: CreateAppInput): Promise<{ app: App; apiKey: string }> { + async createAppWithoutRepo( + data: CreateAppInput, + ): Promise<{ app: App; apiKey: string }> { return appsService.create(data); } @@ -341,11 +355,15 @@ export class AppFactoryService { return { githubRepo: repoInfo.fullName, created: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - logger.error("AppFactory: Failed to create GitHub repo for existing app", { - appId, - error: errorMessage, - }); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + logger.error( + "AppFactory: Failed to create GitHub repo for existing app", + { + appId, + error: errorMessage, + }, + ); return { githubRepo: null, created: false, error: errorMessage }; } } @@ -380,7 +398,8 @@ export class AppFactoryService { githubRepo: app.github_repo, }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; errors.push(`Failed to delete GitHub repo: ${errorMessage}`); logger.warn("AppFactory: Failed to delete GitHub repo", { appId, diff --git a/packages/lib/services/app-promotion-assets.ts b/packages/lib/services/app-promotion-assets.ts index ce79048c5..82d81a540 100644 --- a/packages/lib/services/app-promotion-assets.ts +++ b/packages/lib/services/app-promotion-assets.ts @@ -98,12 +98,15 @@ class AppPromotionAssetsService { const response = await Promise.race([ fetch(safeUrl, { headers: { - "User-Agent": "Mozilla/5.0 (compatible; ElizaCloudBot/1.0; +https://www.elizacloud.ai)", + "User-Agent": + "Mozilla/5.0 (compatible; ElizaCloudBot/1.0; +https://www.elizacloud.ai)", Accept: "text/html", }, redirect: "error", }), - new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 10_000)), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout")), 10_000), + ), ]); if (!response.ok) { @@ -212,11 +215,20 @@ class AppPromotionAssetsService { combinedText.includes("payment") ) { context.industry = "Fintech"; - } else if (combinedText.includes("health") || combinedText.includes("medical")) { + } else if ( + combinedText.includes("health") || + combinedText.includes("medical") + ) { context.industry = "Healthcare"; - } else if (combinedText.includes("education") || combinedText.includes("learning")) { + } else if ( + combinedText.includes("education") || + combinedText.includes("learning") + ) { context.industry = "EdTech"; - } else if (combinedText.includes("real estate") || combinedText.includes("property")) { + } else if ( + combinedText.includes("real estate") || + combinedText.includes("property") + ) { context.industry = "Real Estate"; } @@ -319,7 +331,10 @@ class AppPromotionAssetsService { }); break; } - if (delta.type === "file" && delta.file.mediaType.startsWith("image/")) { + if ( + delta.type === "file" && + delta.file.mediaType.startsWith("image/") + ) { const uint8Array = delta.file.uint8Array; const base64 = Buffer.from(uint8Array).toString("base64"); imageBase64 = `data:${delta.file.mediaType};base64,${base64}`; @@ -336,11 +351,14 @@ class AppPromotionAssetsService { } if (!imageBase64) { - logger.warn("[PromotionAssets] Failed to generate image - no image returned", { - appId: app.id, - size, - streamError, - }); + logger.warn( + "[PromotionAssets] Failed to generate image - no image returned", + { + appId: app.id, + size, + streamError, + }, + ); return null; } @@ -353,10 +371,14 @@ class AppPromotionAssetsService { bufferSize: buffer.length, }); - const blob = await put(`promotion-assets/${app.id}/${size}-${Date.now()}.png`, buffer, { - access: "public", - contentType: "image/png", - }); + const blob = await put( + `promotion-assets/${app.id}/${size}-${Date.now()}.png`, + buffer, + { + access: "public", + contentType: "image/png", + }, + ); if (!blob.url) { logger.error("[PromotionAssets] Blob upload returned no URL", { @@ -384,7 +406,10 @@ class AppPromotionAssetsService { /** * Generate ad banners - runs in parallel for speed */ - async generateAdBanners(app: App, sizes: AdSize[]): Promise { + async generateAdBanners( + app: App, + sizes: AdSize[], + ): Promise { const results = await Promise.all( sizes.map(async (size) => { const asset = await this.generateSocialCard(app, size); @@ -401,7 +426,11 @@ class AppPromotionAssetsService { async generateAdCopy( app: App, targetAudience?: string, - tone: "professional" | "casual" | "exciting" | "informative" = "professional", + tone: + | "professional" + | "casual" + | "exciting" + | "informative" = "professional", ): Promise { // Detect app category for better copy const description = (app.description || "").toLowerCase(); @@ -416,8 +445,15 @@ class AppPromotionAssetsService { valueProps = ["scale your business", "save time", "boost productivity"]; } else if (combinedText.includes("ai") || combinedText.includes("agent")) { category = "AI application"; - valueProps = ["AI-powered automation", "intelligent assistance", "24/7 availability"]; - } else if (combinedText.includes("crypto") || combinedText.includes("web3")) { + valueProps = [ + "AI-powered automation", + "intelligent assistance", + "24/7 availability", + ]; + } else if ( + combinedText.includes("crypto") || + combinedText.includes("web3") + ) { category = "Web3 app"; valueProps = ["decentralized", "secure", "transparent"]; } @@ -500,8 +536,14 @@ Return ONLY valid JSON. No markdown, no explanation.`; // Generate social card (just 1 for speed) if (options.includeSocialCards !== false) { imagePromises.push( - this.generateSocialCard(app, "twitter_card", options.customPrompt).catch((err) => { - errors.push(`Failed to generate social card: ${extractErrorMessage(err)}`); + this.generateSocialCard( + app, + "twitter_card", + options.customPrompt, + ).catch((err) => { + errors.push( + `Failed to generate social card: ${extractErrorMessage(err)}`, + ); return null; }), ); @@ -511,9 +553,13 @@ Return ONLY valid JSON. No markdown, no explanation.`; if (options.includeAdBanners) { imagePromises.push( this.generateSocialCard(app, "instagram_square", options.customPrompt) - .then((asset) => (asset ? ({ ...asset, type: "banner" } as GeneratedAsset) : null)) + .then((asset) => + asset ? ({ ...asset, type: "banner" } as GeneratedAsset) : null, + ) .catch((err) => { - errors.push(`Failed to generate banner: ${extractErrorMessage(err)}`); + errors.push( + `Failed to generate banner: ${extractErrorMessage(err)}`, + ); return null; }), ); @@ -529,7 +575,10 @@ Return ONLY valid JSON. No markdown, no explanation.`; : Promise.resolve(undefined); // Wait for all in parallel - const [imageResults, copy] = await Promise.all([Promise.all(imagePromises), copyPromise]); + const [imageResults, copy] = await Promise.all([ + Promise.all(imagePromises), + copyPromise, + ]); const assets = imageResults.filter((a): a is GeneratedAsset => a !== null); @@ -578,25 +627,30 @@ Return ONLY valid JSON. No markdown, no explanation.`; case "SaaS Platform": visualTheme = "sleek dashboard mockup with floating UI cards, charts, and data visualizations"; - colorScheme = "dark navy to indigo gradient with electric blue highlights"; + colorScheme = + "dark navy to indigo gradient with electric blue highlights"; break; case "Developer Tool / API": visualTheme = "code editor aesthetic with syntax highlighting, terminal windows, floating code snippets"; - colorScheme = "dark charcoal background with green terminal text and purple accents"; + colorScheme = + "dark charcoal background with green terminal text and purple accents"; break; case "AI-Powered Application": visualTheme = "neural network patterns, glowing synapses, abstract AI brain with flowing data streams"; - colorScheme = "black to deep purple gradient with neon pink and cyan glows"; + colorScheme = + "black to deep purple gradient with neon pink and cyan glows"; break; case "Web3 / Blockchain": visualTheme = "geometric blockchain patterns, interconnected nodes, digital ledger visualization"; - colorScheme = "dark charcoal to emerald green gradient with gold accents"; + colorScheme = + "dark charcoal to emerald green gradient with gold accents"; break; case "Analytics Platform": - visualTheme = "3D floating charts, real-time data streams, holographic metric displays"; + visualTheme = + "3D floating charts, real-time data streams, holographic metric displays"; colorScheme = "dark mode with teal and lime green data highlights"; break; case "Marketing Tool": @@ -607,10 +661,12 @@ Return ONLY valid JSON. No markdown, no explanation.`; case "Landing Page Builder": visualTheme = "layered website wireframes, drag-and-drop elements, responsive device frames"; - colorScheme = "clean white to soft blue gradient with accent highlights"; + colorScheme = + "clean white to soft blue gradient with accent highlights"; break; case "E-commerce": - visualTheme = "shopping cart elements, product cards, checkout flow visualization"; + visualTheme = + "shopping cart elements, product cards, checkout flow visualization"; colorScheme = "warm coral to soft pink gradient with gold accents"; break; } @@ -622,8 +678,10 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("subscription") ) { category = "SaaS platform"; - visualTheme = "sleek dashboard mockup with floating UI cards and data visualizations"; - colorScheme = "dark navy to indigo gradient with electric blue highlights"; + visualTheme = + "sleek dashboard mockup with floating UI cards and data visualizations"; + colorScheme = + "dark navy to indigo gradient with electric blue highlights"; } else if ( allText.includes("ai") || allText.includes("agent") || @@ -632,8 +690,10 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("gpt") ) { category = "AI-powered application"; - visualTheme = "neural network patterns, glowing nodes, abstract AI brain visualization"; - colorScheme = "black to deep purple gradient with neon pink and cyan glows"; + visualTheme = + "neural network patterns, glowing nodes, abstract AI brain visualization"; + colorScheme = + "black to deep purple gradient with neon pink and cyan glows"; } else if ( allText.includes("defi") || allText.includes("crypto") || @@ -642,7 +702,8 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("token") ) { category = "Web3/Crypto application"; - visualTheme = "geometric blockchain patterns, floating coins, digital vault aesthetic"; + visualTheme = + "geometric blockchain patterns, floating coins, digital vault aesthetic"; colorScheme = "dark charcoal to green gradient with gold accents"; } else if ( allText.includes("game") || @@ -650,7 +711,8 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("play") ) { category = "gaming application"; - visualTheme = "dynamic action elements, game controller motifs, particle effects"; + visualTheme = + "dynamic action elements, game controller motifs, particle effects"; colorScheme = "black to red gradient with orange fire effects"; } else if ( allText.includes("social") || @@ -658,7 +720,8 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("network") ) { category = "social platform"; - visualTheme = "connected people silhouettes, chat bubbles, community gathering"; + visualTheme = + "connected people silhouettes, chat bubbles, community gathering"; colorScheme = "warm sunset gradient with friendly orange tones"; } else if ( allText.includes("analytics") || @@ -667,7 +730,8 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("metrics") ) { category = "analytics tool"; - visualTheme = "floating charts, data streams, holographic displays with metrics"; + visualTheme = + "floating charts, data streams, holographic displays with metrics"; colorScheme = "dark mode with teal and lime green data highlights"; } else if ( allText.includes("api") || @@ -675,7 +739,8 @@ Return ONLY valid JSON. No markdown, no explanation.`; allText.includes("sdk") ) { category = "developer tool"; - visualTheme = "code editor windows, terminal interfaces, API endpoint visualizations"; + visualTheme = + "code editor windows, terminal interfaces, API endpoint visualizations"; colorScheme = "dark charcoal with green terminal highlights"; } } @@ -753,10 +818,21 @@ The image should make viewers immediately understand this is a ${category} produ Make it look like a premium tech company's marketing material that would appear on ProductHunt or TechCrunch.`; } - getRecommendedSizes(platform: "meta" | "google" | "twitter" | "linkedin"): AdSize[] { + getRecommendedSizes( + platform: "meta" | "google" | "twitter" | "linkedin", + ): AdSize[] { const recommendations: Record = { - meta: ["facebook_feed", "facebook_story", "instagram_square", "instagram_story"], - google: ["google_display_leaderboard", "google_display_medium", "google_display_large"], + meta: [ + "facebook_feed", + "facebook_story", + "instagram_square", + "instagram_story", + ], + google: [ + "google_display_leaderboard", + "google_display_medium", + "google_display_large", + ], twitter: ["twitter_card"], linkedin: ["linkedin_post"], }; diff --git a/packages/lib/services/app-promotion.ts b/packages/lib/services/app-promotion.ts index 5060949e8..0502ee7bc 100644 --- a/packages/lib/services/app-promotion.ts +++ b/packages/lib/services/app-promotion.ts @@ -172,7 +172,9 @@ class AppPromotionService { * Validate promotional content structure manually. * Bypasses Zod to avoid Turbopack bundling issues with Zod internals. */ - private validatePromotionalContent(data: unknown): GeneratedPromotionalContent { + private validatePromotionalContent( + data: unknown, + ): GeneratedPromotionalContent { if (!data || typeof data !== "object") { throw new Error("Promotional content must be an object"); } @@ -180,7 +182,12 @@ class AppPromotionService { const obj = data as Record; // Validate required string fields - const requiredStrings = ["headline", "shortDescription", "longDescription", "callToAction"]; + const requiredStrings = [ + "headline", + "shortDescription", + "longDescription", + "callToAction", + ]; for (const field of requiredStrings) { if (typeof obj[field] !== "string") { throw new Error(`Missing or invalid field: ${field}`); @@ -222,7 +229,8 @@ class AppPromotionService { app: App, targetAudience?: string, ): Promise { - const appDescription = app.description || `${app.name} - An app built on Eliza Cloud`; + const appDescription = + app.description || `${app.name} - An app built on Eliza Cloud`; const appUrl = app.app_url; const prompt = `Generate promotional content for this app: @@ -292,7 +300,8 @@ Return ONLY valid JSON, no markdown.`; } catch (parseError) { logger.error("[AppPromotion] Failed to parse AI response JSON", { appId: app.id, - error: parseError instanceof Error ? parseError.message : "Unknown error", + error: + parseError instanceof Error ? parseError.message : "Unknown error", jsonString: jsonString.substring(0, 500), }); throw new Error("Failed to parse promotional content JSON"); @@ -304,7 +313,10 @@ Return ONLY valid JSON, no markdown.`; } catch (validationError) { logger.error("[AppPromotion] Content validation failed", { appId: app.id, - error: validationError instanceof Error ? validationError.message : "Unknown error", + error: + validationError instanceof Error + ? validationError.message + : "Unknown error", }); throw new Error("Promotional content validation failed"); } @@ -341,7 +353,10 @@ Return ONLY valid JSON, no markdown.`; // Generate promotional content if needed let promotionalContent: GeneratedPromotionalContent | undefined; - if (config.channels.includes("social") || config.channels.includes("advertising")) { + if ( + config.channels.includes("social") || + config.channels.includes("advertising") + ) { const contentDeduction = await creditsService.deductCredits({ organizationId, amount: PROMOTION_COSTS.contentGeneration, @@ -362,10 +377,13 @@ Return ONLY valid JSON, no markdown.`; metadata: { appId, type: "content_generation_refund" }, }); result.totalCreditsUsed -= PROMOTION_COSTS.contentGeneration; - logger.error("[AppPromotion] Content generation failed, credits refunded", { - appId, - error: error instanceof Error ? error.message : "Unknown error", - }); + logger.error( + "[AppPromotion] Content generation failed, credits refunded", + { + appId, + error: error instanceof Error ? error.message : "Unknown error", + }, + ); result.errors.push("Content generation failed - credits refunded"); } } @@ -383,7 +401,8 @@ Return ONLY valid JSON, no markdown.`; if (!result.channels.social.success) { result.errors.push("Social media promotion partially failed"); } - result.totalCreditsUsed += config.social.platforms.length * PROMOTION_COSTS.socialPostBase; + result.totalCreditsUsed += + config.social.platforms.length * PROMOTION_COSTS.socialPostBase; } if (config.channels.includes("seo") && config.seo) { @@ -394,7 +413,9 @@ Return ONLY valid JSON, no markdown.`; config.seo, ); if (!result.channels.seo.success) { - result.errors.push(`SEO optimization failed: ${result.channels.seo.error}`); + result.errors.push( + `SEO optimization failed: ${result.channels.seo.error}`, + ); } result.totalCreditsUsed += PROMOTION_COSTS.seoBundle; } @@ -407,18 +428,25 @@ Return ONLY valid JSON, no markdown.`; promotionalContent, ); if (!result.channels.advertising.success) { - result.errors.push(`Ad campaign creation failed: ${result.channels.advertising.error}`); + result.errors.push( + `Ad campaign creation failed: ${result.channels.advertising.error}`, + ); } } - if (config.channels.includes("twitter_automation") && config.twitterAutomation) { + if ( + config.channels.includes("twitter_automation") && + config.twitterAutomation + ) { result.channels.twitterAutomation = await this.executeTwitterAutomation( organizationId, app, config.twitterAutomation, ); if (!result.channels.twitterAutomation.success) { - result.errors.push(`Twitter automation failed: ${result.channels.twitterAutomation.error}`); + result.errors.push( + `Twitter automation failed: ${result.channels.twitterAutomation.error}`, + ); } result.totalCreditsUsed += PROMOTION_COSTS.twitterAutomationSetup + @@ -427,7 +455,10 @@ Return ONLY valid JSON, no markdown.`; : 0); } - if (config.channels.includes("telegram_automation") && config.telegramAutomation) { + if ( + config.channels.includes("telegram_automation") && + config.telegramAutomation + ) { result.channels.telegramAutomation = await this.executeTelegramAutomation( organizationId, app, @@ -445,14 +476,19 @@ Return ONLY valid JSON, no markdown.`; : 0); } - if (config.channels.includes("discord_automation") && config.discordAutomation) { + if ( + config.channels.includes("discord_automation") && + config.discordAutomation + ) { result.channels.discordAutomation = await this.executeDiscordAutomation( organizationId, app, config.discordAutomation, ); if (!result.channels.discordAutomation.success) { - result.errors.push(`Discord automation failed: ${result.channels.discordAutomation.error}`); + result.errors.push( + `Discord automation failed: ${result.channels.discordAutomation.error}`, + ); } result.totalCreditsUsed += PROMOTION_COSTS.discordAutomationSetup + @@ -502,7 +538,9 @@ Return ONLY valid JSON, no markdown.`; platforms: [platform], }); - const platformResult = postResult.results.find((r) => r.platform === platform); + const platformResult = postResult.results.find( + (r) => r.platform === platform, + ); results.push({ platform, success: platformResult?.success ?? false, @@ -549,7 +587,8 @@ Return ONLY valid JSON, no markdown.`; type: a.type, data: a.data as Record, })), - error: result.request.status === "failed" ? "SEO request failed" : undefined, + error: + result.request.status === "failed" ? "SEO request failed" : undefined, }; } @@ -584,11 +623,18 @@ Return ONLY valid JSON, no markdown.`; startDate, endDate, appId: app.id, - targeting: config.targetLocations?.length ? { locations: config.targetLocations } : undefined, + targeting: config.targetLocations?.length + ? { locations: config.targetLocations } + : undefined, }); if (content) { - await this.createDefaultCreative(organizationId, campaign.id, app, content); + await this.createDefaultCreative( + organizationId, + campaign.id, + app, + content, + ); } return { @@ -635,25 +681,32 @@ Return ONLY valid JSON, no markdown.`; ): Promise> { try { // Enable automation with the provided config - await twitterAppAutomationService.enableAutomation(organizationId, app.id, { - enabled: config.enabled, - autoPost: config.autoPost, - autoReply: config.autoReply, - autoEngage: config.autoEngage, - discovery: config.discovery, - postIntervalMin: config.postIntervalMin, - postIntervalMax: config.postIntervalMax, - vibeStyle: config.vibeStyle, - topics: config.topics, - agentCharacterId: config.agentCharacterId, - }); + await twitterAppAutomationService.enableAutomation( + organizationId, + app.id, + { + enabled: config.enabled, + autoPost: config.autoPost, + autoReply: config.autoReply, + autoEngage: config.autoEngage, + discovery: config.discovery, + postIntervalMin: config.postIntervalMin, + postIntervalMax: config.postIntervalMax, + vibeStyle: config.vibeStyle, + topics: config.topics, + agentCharacterId: config.agentCharacterId, + }, + ); // Post an initial announcement tweet if autoPost is enabled let initialTweetId: string | undefined; let initialTweetUrl: string | undefined; if (config.autoPost) { - const tweetResult = await twitterAppAutomationService.postAppTweet(organizationId, app.id); + const tweetResult = await twitterAppAutomationService.postAppTweet( + organizationId, + app.id, + ); if (tweetResult.success) { initialTweetId = tweetResult.tweetId; @@ -699,10 +752,13 @@ Return ONLY valid JSON, no markdown.`; try { // If useExisting is true, just post using existing automation config if (config.useExisting) { - logger.info("[AppPromotion] Using existing Telegram automation, posting only", { - appId: app.id, - organizationId, - }); + logger.info( + "[AppPromotion] Using existing Telegram automation, posting only", + { + appId: app.id, + organizationId, + }, + ); const postResult = await telegramAppAutomationService.postAnnouncement( organizationId, @@ -730,17 +786,21 @@ Return ONLY valid JSON, no markdown.`; } // Enable automation with the provided config - await telegramAppAutomationService.enableAutomation(organizationId, app.id, { - enabled: config.enabled ?? true, - channelId: config.channelId, - groupId: config.groupId, - autoAnnounce: config.autoAnnounce ?? true, - autoReply: config.autoReply, - announceIntervalMin: config.announceIntervalMin ?? 120, - announceIntervalMax: config.announceIntervalMax ?? 240, - vibeStyle: config.vibeStyle, - agentCharacterId: config.agentCharacterId, - }); + await telegramAppAutomationService.enableAutomation( + organizationId, + app.id, + { + enabled: config.enabled ?? true, + channelId: config.channelId, + groupId: config.groupId, + autoAnnounce: config.autoAnnounce ?? true, + autoReply: config.autoReply, + announceIntervalMin: config.announceIntervalMin ?? 120, + announceIntervalMax: config.announceIntervalMax ?? 240, + vibeStyle: config.vibeStyle, + agentCharacterId: config.agentCharacterId, + }, + ); // Post an initial announcement if autoAnnounce is enabled let initialMessageId: string | undefined; @@ -795,10 +855,13 @@ Return ONLY valid JSON, no markdown.`; try { // If useExisting is true, just post using existing automation config if (config.useExisting) { - logger.info("[AppPromotion] Using existing Discord automation, posting only", { - appId: app.id, - organizationId, - }); + logger.info( + "[AppPromotion] Using existing Discord automation, posting only", + { + appId: app.id, + organizationId, + }, + ); const postResult = await discordAppAutomationService.postAnnouncement( organizationId, @@ -824,16 +887,20 @@ Return ONLY valid JSON, no markdown.`; } // Enable automation with the provided config - await discordAppAutomationService.enableAutomation(organizationId, app.id, { - enabled: config.enabled ?? true, - guildId: config.guildId, - channelId: config.channelId, - autoAnnounce: config.autoAnnounce ?? true, - announceIntervalMin: config.announceIntervalMin ?? 120, - announceIntervalMax: config.announceIntervalMax ?? 240, - vibeStyle: config.vibeStyle, - agentCharacterId: config.agentCharacterId, - }); + await discordAppAutomationService.enableAutomation( + organizationId, + app.id, + { + enabled: config.enabled ?? true, + guildId: config.guildId, + channelId: config.channelId, + autoAnnounce: config.autoAnnounce ?? true, + announceIntervalMin: config.announceIntervalMin ?? 120, + announceIntervalMax: config.announceIntervalMax ?? 240, + vibeStyle: config.vibeStyle, + agentCharacterId: config.agentCharacterId, + }, + ); // Post an initial announcement if autoAnnounce is enabled let initialMessageId: string | undefined; @@ -901,9 +968,14 @@ Return ONLY valid JSON, no markdown.`; } // Check Twitter automation status - let twitterAutomationStatus: { connected: boolean; enabled: boolean } | undefined; + let twitterAutomationStatus: + | { connected: boolean; enabled: boolean } + | undefined; try { - const status = await twitterAppAutomationService.getAutomationStatus(organizationId, appId); + const status = await twitterAppAutomationService.getAutomationStatus( + organizationId, + appId, + ); twitterAutomationStatus = { connected: status.twitterConnected, enabled: status.enabled, @@ -924,7 +996,9 @@ Return ONLY valid JSON, no markdown.`; ]; if (twitterAutomationStatus.connected && !twitterAutomationStatus.enabled) { - tips.unshift("🚀 Enable Twitter Automation for 24/7 AI-powered vibe marketing!"); + tips.unshift( + "🚀 Enable Twitter Automation for 24/7 AI-powered vibe marketing!", + ); } return { diff --git a/packages/lib/services/app-signup-tracking.ts b/packages/lib/services/app-signup-tracking.ts index 277ddb440..264183360 100644 --- a/packages/lib/services/app-signup-tracking.ts +++ b/packages/lib/services/app-signup-tracking.ts @@ -49,7 +49,10 @@ export class AppSignupTrackingService { } // Create or update app user record - const existingAppUser = await appsRepository.findAppUser(appId, data.userId); + const existingAppUser = await appsRepository.findAppUser( + appId, + data.userId, + ); if (existingAppUser) { // User already exists for this app, just update metadata @@ -103,7 +106,9 @@ export class AppSignupTrackingService { // Check cookies if (cookies) { const storedCode = - cookies.get("affiliate_code") || cookies.get("ref_code") || cookies.get("app_code"); + cookies.get("affiliate_code") || + cookies.get("ref_code") || + cookies.get("app_code"); if (storedCode) return storedCode; } diff --git a/packages/lib/services/apps.ts b/packages/lib/services/apps.ts index 07f971164..a8dbd6c23 100644 --- a/packages/lib/services/apps.ts +++ b/packages/lib/services/apps.ts @@ -3,7 +3,12 @@ */ import crypto from "crypto"; -import { type App, type AppUser, appsRepository, type NewApp } from "@/db/repositories/apps"; +import { + type App, + type AppUser, + appsRepository, + type NewApp, +} from "@/db/repositories/apps"; import { cache } from "@/lib/cache/client"; import { CacheKeys, CacheTTL } from "@/lib/cache/keys"; import { logger } from "@/lib/utils/logger"; @@ -128,7 +133,10 @@ export class AppsService { return await appsRepository.listByOrganization(organizationId); } - async listAll(filters?: { isActive?: boolean; isApproved?: boolean }): Promise { + async listAll(filters?: { + isActive?: boolean; + isApproved?: boolean; + }): Promise { return await appsRepository.listAll(filters); } @@ -219,10 +227,13 @@ export class AppsService { logger.info("Cleaned up user database for app", { appId: id }); } catch (error) { // Log but don't fail deletion - database might already be gone - logger.warn("Failed to clean up user database (continuing with deletion)", { - appId: id, - error: error instanceof Error ? error.message : "Unknown", - }); + logger.warn( + "Failed to clean up user database (continuing with deletion)", + { + appId: id, + error: error instanceof Error ? error.message : "Unknown", + }, + ); } } @@ -239,7 +250,10 @@ export class AppsService { * Increment app usage counters (requests, credits) * This is a fire-and-forget operation for tracking */ - async incrementUsage(appId: string, creditsUsed: string = "0.00"): Promise { + async incrementUsage( + appId: string, + creditsUsed: string = "0.00", + ): Promise { await appsRepository.incrementUsage(appId, creditsUsed); } @@ -249,7 +263,12 @@ export class AppsService { creditsUsed: string = "0.00", metadata?: Record, ): Promise { - await appsRepository.trackAppUserActivity(appId, userId, creditsUsed, metadata); + await appsRepository.trackAppUserActivity( + appId, + userId, + creditsUsed, + metadata, + ); } /** @@ -330,9 +349,14 @@ export class AppsService { ]); if (requestData.userId) { - await this.trackUsage(app.id, requestData.userId, requestData.creditsUsed || "0.00", { - requestType: requestData.requestType, - }); + await this.trackUsage( + app.id, + requestData.userId, + requestData.creditsUsed || "0.00", + { + requestType: requestData.requestType, + }, + ); } logger.debug("[Apps] Logged detailed request", { @@ -424,7 +448,12 @@ export class AppsService { /** * Get top visitors/IPs for an app. */ - async getTopVisitors(appId: string, limit?: number, startDate?: Date, endDate?: Date) { + async getTopVisitors( + appId: string, + limit?: number, + startDate?: Date, + endDate?: Date, + ) { return appsRepository.getTopVisitors(appId, limit, startDate, endDate); } @@ -437,7 +466,12 @@ export class AppsService { startDate: Date, endDate: Date, ) { - return appsRepository.getRequestsOverTime(appId, periodType, startDate, endDate); + return appsRepository.getRequestsOverTime( + appId, + periodType, + startDate, + endDate, + ); } async getAppUsers(appId: string, limit?: number): Promise { @@ -450,7 +484,12 @@ export class AppsService { startDate: Date, endDate: Date, ) { - return await appsRepository.getAnalytics(appId, periodType, startDate, endDate); + return await appsRepository.getAnalytics( + appId, + periodType, + startDate, + endDate, + ); } async getTotalStats(appId: string): Promise<{ diff --git a/packages/lib/services/auto-top-up.ts b/packages/lib/services/auto-top-up.ts index 0e46f9540..3202e3df9 100644 --- a/packages/lib/services/auto-top-up.ts +++ b/packages/lib/services/auto-top-up.ts @@ -35,10 +35,14 @@ export interface AutoTopUpCheckResult { export class AutoTopUpService { validateSettings(amount: number, threshold: number): void { if (amount < AUTO_TOP_UP_LIMITS.MIN_AMOUNT) { - throw new Error(`Auto top-up amount must be at least $${AUTO_TOP_UP_LIMITS.MIN_AMOUNT}`); + throw new Error( + `Auto top-up amount must be at least $${AUTO_TOP_UP_LIMITS.MIN_AMOUNT}`, + ); } if (amount > AUTO_TOP_UP_LIMITS.MAX_AMOUNT) { - throw new Error(`Auto top-up amount cannot exceed $${AUTO_TOP_UP_LIMITS.MAX_AMOUNT}`); + throw new Error( + `Auto top-up amount cannot exceed $${AUTO_TOP_UP_LIMITS.MAX_AMOUNT}`, + ); } if (threshold < AUTO_TOP_UP_LIMITS.MIN_THRESHOLD) { throw new Error( @@ -46,7 +50,9 @@ export class AutoTopUpService { ); } if (threshold > AUTO_TOP_UP_LIMITS.MAX_THRESHOLD) { - throw new Error(`Auto top-up threshold cannot exceed $${AUTO_TOP_UP_LIMITS.MAX_THRESHOLD}`); + throw new Error( + `Auto top-up threshold cannot exceed $${AUTO_TOP_UP_LIMITS.MAX_THRESHOLD}`, + ); } if (!Number.isFinite(amount) || !Number.isFinite(threshold)) { throw new Error("Auto top-up settings must be valid numbers"); @@ -57,7 +63,9 @@ export class AutoTopUpService { const startTime = new Date(); const results: AutoTopUpResult[] = []; - logger.info(`[AutoTopUp] Starting auto top-up check at ${startTime.toISOString()}`); + logger.info( + `[AutoTopUp] Starting auto top-up check at ${startTime.toISOString()}`, + ); const orgsNeedingTopUp = await dbRead .select() @@ -69,7 +77,9 @@ export class AutoTopUpService { ), ); - logger.info(`[AutoTopUp] Found ${orgsNeedingTopUp.length} organizations needing auto top-up`); + logger.info( + `[AutoTopUp] Found ${orgsNeedingTopUp.length} organizations needing auto top-up`, + ); const settledResults = await Promise.allSettled( orgsNeedingTopUp.map((org) => this.executeAutoTopUp(org)), @@ -83,7 +93,10 @@ export class AutoTopUpService { results.push({ organizationId: "unknown", success: false, - error: settled.reason instanceof Error ? settled.reason.message : String(settled.reason), + error: + settled.reason instanceof Error + ? settled.reason.message + : String(settled.reason), }); } } @@ -122,10 +135,16 @@ export class AutoTopUpService { const userId = billingUser?.id || (users.length > 0 ? users[0].id : null); trackingId = userId || `org:${organizationId}`; } catch (userLookupError) { - logger.warn(`[AutoTopUp] Failed to fetch users for analytics, using org ID`, { - organizationId, - error: userLookupError instanceof Error ? userLookupError.message : "Unknown error", - }); + logger.warn( + `[AutoTopUp] Failed to fetch users for analytics, using org ID`, + { + organizationId, + error: + userLookupError instanceof Error + ? userLookupError.message + : "Unknown error", + }, + ); } const currentBalance = Number(org.credit_balance); @@ -155,13 +174,18 @@ export class AutoTopUpService { } if (!org.stripe_default_payment_method) { - logger.error(`[AutoTopUp] Org ${organizationId} missing default payment method`); + logger.error( + `[AutoTopUp] Org ${organizationId} missing default payment method`, + ); trackServerEvent(trackingId, "auto_topup_failed", { organization_id: organizationId, error_reason: "missing_payment_method", amount: topUpAmount, }); - await this.disableAutoTopUp(organizationId, "Missing default payment method"); + await this.disableAutoTopUp( + organizationId, + "Missing default payment method", + ); return { organizationId, success: false, @@ -171,7 +195,9 @@ export class AutoTopUpService { const amount = Number(org.auto_top_up_amount || 0); if (amount <= 0 || amount > AUTO_TOP_UP_LIMITS.MAX_AMOUNT) { - logger.error(`[AutoTopUp] Org ${organizationId} has invalid top-up amount: ${amount}`); + logger.error( + `[AutoTopUp] Org ${organizationId} has invalid top-up amount: ${amount}`, + ); trackServerEvent(trackingId, "auto_topup_failed", { organization_id: organizationId, error_reason: "invalid_amount", @@ -217,7 +243,10 @@ export class AutoTopUpService { } } } catch (e) { - logger.error(`[AutoTopUp] Failed to lookup affiliate for org ${organizationId}`, e); + logger.error( + `[AutoTopUp] Failed to lookup affiliate for org ${organizationId}`, + e, + ); } totalAmount = amount + affiliateFeeAmount + platformFeeAmount; @@ -267,7 +296,9 @@ export class AutoTopUpService { { idempotencyKey }, ); - logger.info(`[AutoTopUp] PaymentIntent ${paymentIntent.id} status: ${paymentIntent.status}`); + logger.info( + `[AutoTopUp] PaymentIntent ${paymentIntent.id} status: ${paymentIntent.status}`, + ); if (paymentIntent.status === "succeeded") { const previousBalance = Number(org.credit_balance); @@ -294,8 +325,16 @@ export class AutoTopUpService { credits_added: amount, }); - logger.info(`[AutoTopUp] About to call queueAutoTopUpSuccessEmail for org ${organizationId}`); - this.queueAutoTopUpSuccessEmail(org, amount, previousBalance, newBalance, paymentIntent.id); + logger.info( + `[AutoTopUp] About to call queueAutoTopUpSuccessEmail for org ${organizationId}`, + ); + this.queueAutoTopUpSuccessEmail( + org, + amount, + previousBalance, + newBalance, + paymentIntent.id, + ); return { organizationId, @@ -317,7 +356,10 @@ export class AutoTopUpService { error_reason: `Payment ${paymentIntent.status}`, }); - await this.disableAutoTopUp(organizationId, `Payment ${paymentIntent.status}`); + await this.disableAutoTopUp( + organizationId, + `Payment ${paymentIntent.status}`, + ); return { organizationId, success: false, @@ -342,8 +384,13 @@ export class AutoTopUpService { } } - private async disableAutoTopUp(organizationId: string, reason: string): Promise { - logger.info(`[AutoTopUp] Disabling auto top-up for org ${organizationId}: ${reason}`); + private async disableAutoTopUp( + organizationId: string, + reason: string, + ): Promise { + logger.info( + `[AutoTopUp] Disabling auto top-up for org ${organizationId}: ${reason}`, + ); const org = await organizationsRepository.findById(organizationId); if (!org) { @@ -366,25 +413,32 @@ export class AutoTopUpService { newBalance: number, paymentIntentId: string, ): Promise { - logger.info(`[AutoTopUp] queueAutoTopUpSuccessEmail START for org ${org.id}`); + logger.info( + `[AutoTopUp] queueAutoTopUpSuccessEmail START for org ${org.id}`, + ); const recipientEmail = await this.getUserEmail(org.id); logger.info(`[AutoTopUp] User email: ${recipientEmail || "NONE"}`); if (!recipientEmail) { - logger.error(`[AutoTopUp] CRITICAL: No user email for org ${org.id} - EMAIL NOT SENT`); + logger.error( + `[AutoTopUp] CRITICAL: No user email for org ${org.id} - EMAIL NOT SENT`, + ); return; } let paymentMethodDisplay = "Card on file"; if (org.stripe_default_payment_method) { - const pm = await requireStripe().paymentMethods.retrieve(org.stripe_default_payment_method); + const pm = await requireStripe().paymentMethods.retrieve( + org.stripe_default_payment_method, + ); if (pm.card) { paymentMethodDisplay = `${pm.card.brand} ••••${pm.card.last4}`; } } - const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const appUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const emailData = { email: recipientEmail, organizationName: org.name, @@ -396,27 +450,37 @@ export class AutoTopUpService { billingUrl: `${appUrl}/dashboard/settings`, }; - logger.info(`[AutoTopUp] Calling emailService.sendAutoTopUpSuccessEmail with:`); + logger.info( + `[AutoTopUp] Calling emailService.sendAutoTopUpSuccessEmail with:`, + ); logger.info(JSON.stringify(emailData, null, 2)); const result = await emailService.sendAutoTopUpSuccessEmail(emailData); logger.info(`[AutoTopUp] Email service returned: ${result}`); if (result) { - logger.info(`[AutoTopUp] ✓ SUCCESS: Auto top-up email sent to ${recipientEmail}`); + logger.info( + `[AutoTopUp] ✓ SUCCESS: Auto top-up email sent to ${recipientEmail}`, + ); } else { - logger.error(`[AutoTopUp] ✗ FAILED: Email service returned false for ${recipientEmail}`); + logger.error( + `[AutoTopUp] ✗ FAILED: Email service returned false for ${recipientEmail}`, + ); } } - private async queueAutoTopUpDisabledEmail(org: Organization, reason: string): Promise { + private async queueAutoTopUpDisabledEmail( + org: Organization, + reason: string, + ): Promise { const recipientEmail = await this.getUserEmail(org.id); if (!recipientEmail) { logger.error(`[AutoTopUp] No user email for org ${org.id}`); return; } - const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const appUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; await emailService.sendAutoTopUpDisabledEmail({ email: recipientEmail, organizationName: org.name, @@ -477,14 +541,16 @@ export class AutoTopUpService { } const amount = settings.amount ?? Number(org.auto_top_up_amount || 0); - const threshold = settings.threshold ?? Number(org.auto_top_up_threshold || 0); + const threshold = + settings.threshold ?? Number(org.auto_top_up_threshold || 0); this.validateSettings(amount, threshold); } if (settings.amount !== undefined || settings.threshold !== undefined) { const amount = settings.amount ?? Number(org.auto_top_up_amount || 0); - const threshold = settings.threshold ?? Number(org.auto_top_up_threshold || 0); + const threshold = + settings.threshold ?? Number(org.auto_top_up_threshold || 0); this.validateSettings(amount, threshold); } @@ -504,7 +570,10 @@ export class AutoTopUpService { await organizationsRepository.update(organizationId, updates); - logger.info(`[AutoTopUp] Updated settings for org ${organizationId}:`, updates); + logger.info( + `[AutoTopUp] Updated settings for org ${organizationId}:`, + updates, + ); } } diff --git a/packages/lib/services/automation-constants.ts b/packages/lib/services/automation-constants.ts index 74f51a81b..f4148ff3d 100644 --- a/packages/lib/services/automation-constants.ts +++ b/packages/lib/services/automation-constants.ts @@ -48,7 +48,9 @@ export const TWITTER_AUTOMATION_DEFAULTS = { /** * Helper to get Discord config with defaults applied */ -export function getDiscordConfigWithDefaults(config: Record | null | undefined) { +export function getDiscordConfigWithDefaults( + config: Record | null | undefined, +) { return { ...DISCORD_AUTOMATION_DEFAULTS, ...config, @@ -58,7 +60,9 @@ export function getDiscordConfigWithDefaults(config: Record | n /** * Helper to get Telegram config with defaults applied */ -export function getTelegramConfigWithDefaults(config: Record | null | undefined) { +export function getTelegramConfigWithDefaults( + config: Record | null | undefined, +) { return { ...TELEGRAM_AUTOMATION_DEFAULTS, ...config, @@ -68,7 +72,9 @@ export function getTelegramConfigWithDefaults(config: Record | /** * Helper to get Twitter config with defaults applied */ -export function getTwitterConfigWithDefaults(config: Record | null | undefined) { +export function getTwitterConfigWithDefaults( + config: Record | null | undefined, +) { return { ...TWITTER_AUTOMATION_DEFAULTS, ...config, diff --git a/packages/lib/services/blooio-automation/index.ts b/packages/lib/services/blooio-automation/index.ts index 5e716baa2..b98275393 100644 --- a/packages/lib/services/blooio-automation/index.ts +++ b/packages/lib/services/blooio-automation/index.ts @@ -16,7 +16,9 @@ import { logger } from "@/lib/utils/logger"; // Use ELIZA_API_URL (ngrok) for local dev webhooks, otherwise NEXT_PUBLIC_APP_URL const WEBHOOK_BASE_URL = - process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + process.env.ELIZA_API_URL || + process.env.NEXT_PUBLIC_APP_URL || + "https://www.elizacloud.ai"; // Cache TTL for connection status (5 minutes) const STATUS_CACHE_TTL_MS = 5 * 60 * 1000; @@ -80,7 +82,10 @@ class BlooioAutomationService { } // For network errors, fail-secure: don't allow potentially invalid keys - return { valid: false, error: "Validation failed due to network error. Please try again." }; + return { + valid: false, + error: "Validation failed due to network error. Please try again.", + }; } } @@ -119,7 +124,12 @@ class BlooioAutomationService { const existingSecrets = await secretsService.list(organizationId); const existingSecret = existingSecrets.find((s) => s.name === name); if (existingSecret) { - await secretsService.rotate(existingSecret.id, organizationId, value, audit); + await secretsService.rotate( + existingSecret.id, + organizationId, + value, + audit, + ); } else { throw err; // Re-throw if we can't find it } @@ -132,7 +142,10 @@ class BlooioAutomationService { await createOrUpdateSecret("BLOOIO_API_KEY", credentials.apiKey); if (credentials.webhookSecret) { - await createOrUpdateSecret("BLOOIO_WEBHOOK_SECRET", credentials.webhookSecret); + await createOrUpdateSecret( + "BLOOIO_WEBHOOK_SECRET", + credentials.webhookSecret, + ); } if (credentials.fromNumber) { @@ -152,14 +165,21 @@ class BlooioAutomationService { /** * Remove Blooio credentials (disconnect). */ - async removeCredentials(organizationId: string, userId: string): Promise { + async removeCredentials( + organizationId: string, + userId: string, + ): Promise { const audit = { actorType: "user" as const, actorId: userId, source: "blooio-automation", }; - const secretNames = ["BLOOIO_API_KEY", "BLOOIO_WEBHOOK_SECRET", "BLOOIO_FROM_NUMBER"]; + const secretNames = [ + "BLOOIO_API_KEY", + "BLOOIO_WEBHOOK_SECRET", + "BLOOIO_FROM_NUMBER", + ]; // Get all secrets once (not inside the loop) for efficiency const existingSecrets = await secretsService.list(organizationId); @@ -175,12 +195,19 @@ class BlooioAutomationService { organizationId, }); } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (message.includes("Secret not found") || message.includes("Failed to delete secret")) { - logger.debug("[BlooioAutomation] Secret already removed during disconnect", { - name, - organizationId, - }); + const message = + error instanceof Error ? error.message : String(error); + if ( + message.includes("Secret not found") || + message.includes("Failed to delete secret") + ) { + logger.debug( + "[BlooioAutomation] Secret already removed during disconnect", + { + name, + organizationId, + }, + ); continue; } throw error; @@ -199,7 +226,10 @@ class BlooioAutomationService { * Falls back to env var only in non-production environments. */ async getApiKey(organizationId: string): Promise { - const fromSecrets = await secretsService.get(organizationId, "BLOOIO_API_KEY"); + const fromSecrets = await secretsService.get( + organizationId, + "BLOOIO_API_KEY", + ); if (fromSecrets) return fromSecrets; // Only fall back to env var in non-production (prevents multi-tenancy violation) if (process.env.NODE_ENV !== "production") { @@ -213,7 +243,10 @@ class BlooioAutomationService { * No env fallback in production to prevent multi-tenancy violation. */ async getWebhookSecret(organizationId: string): Promise { - const fromSecrets = await secretsService.get(organizationId, "BLOOIO_WEBHOOK_SECRET"); + const fromSecrets = await secretsService.get( + organizationId, + "BLOOIO_WEBHOOK_SECRET", + ); if (fromSecrets) return fromSecrets; // Only fall back to env var in non-production (prevents multi-tenancy violation) if (process.env.NODE_ENV !== "production") { @@ -227,7 +260,10 @@ class BlooioAutomationService { * Falls back to env var only in non-production environments. */ async getFromNumber(organizationId: string): Promise { - const fromSecrets = await secretsService.get(organizationId, "BLOOIO_FROM_NUMBER"); + const fromSecrets = await secretsService.get( + organizationId, + "BLOOIO_FROM_NUMBER", + ); if (fromSecrets) return fromSecrets; // Only fall back to env var in non-production (prevents multi-tenancy violation) if (process.env.NODE_ENV !== "production") { @@ -341,7 +377,8 @@ class BlooioAutomationService { if (request.text) payload.text = request.text; if (request.attachments) payload.attachments = request.attachments; if (request.metadata) payload.metadata = request.metadata; - if (request.use_typing_indicator) payload.use_typing_indicator = request.use_typing_indicator; + if (request.use_typing_indicator) + payload.use_typing_indicator = request.use_typing_indicator; const response = await blooioApiRequest( apiKey, @@ -355,7 +392,8 @@ class BlooioAutomationService { ); const messageId = - response.message_id || (response.message_ids ? response.message_ids[0] : undefined); + response.message_id || + (response.message_ids ? response.message_ids[0] : undefined); logger.info("[BlooioAutomation] Message sent", { organizationId, diff --git a/packages/lib/services/character-prompt-helper.ts b/packages/lib/services/character-prompt-helper.ts index 1b6b373e8..8f1ca129d 100644 --- a/packages/lib/services/character-prompt-helper.ts +++ b/packages/lib/services/character-prompt-helper.ts @@ -53,7 +53,9 @@ export async function getCharacterPromptContext( return null; } - const bio = Array.isArray(character.bio) ? character.bio.join(" ") : character.bio || ""; + const bio = Array.isArray(character.bio) + ? character.bio.join(" ") + : character.bio || ""; const style = character.style || {}; const postStyle = style.post || []; @@ -87,7 +89,9 @@ export async function getCharacterPromptContext( * Build a system prompt section for character-voiced content generation. * Used by Twitter, Discord, Telegram automation services. */ -export function buildCharacterSystemPrompt(context: CharacterPromptContext): string { +export function buildCharacterSystemPrompt( + context: CharacterPromptContext, +): string { const parts: string[] = []; parts.push(`You are ${context.name}.`); @@ -114,7 +118,9 @@ export function buildCharacterSystemPrompt(context: CharacterPromptContext): str if (context.postExamples.length > 0) { const examples = getRandomSample(context.postExamples, 3); - parts.push(`Example posts you've written:\n${examples.map((ex) => `- "${ex}"`).join("\n")}`); + parts.push( + `Example posts you've written:\n${examples.map((ex) => `- "${ex}"`).join("\n")}`, + ); } const prompt = parts.join("\n\n"); diff --git a/packages/lib/services/characters/characters.ts b/packages/lib/services/characters/characters.ts index d0c7c2f35..c3555a6e7 100644 --- a/packages/lib/services/characters/characters.ts +++ b/packages/lib/services/characters/characters.ts @@ -86,7 +86,9 @@ export class CharactersService { async invalidateCache(id: string): Promise { inMemoryCharCache.delete(id); // Import dynamically to avoid circular dependency - const { invalidateCharacterCache } = await import("@/lib/cache/character-cache"); + const { invalidateCharacterCache } = await import( + "@/lib/cache/character-cache" + ); await Promise.all([ // Invalidate the simple character cache key @@ -99,7 +101,10 @@ export class CharactersService { logger.info(`[Characters] Cache invalidated for character: ${id}`); } - async getByIdForUser(characterId: string, userId: string): Promise { + async getByIdForUser( + characterId: string, + userId: string, + ): Promise { const character = await userCharactersRepository.findById(characterId); if (!character || character.user_id !== userId) { @@ -136,7 +141,10 @@ export class CharactersService { options?: { source?: "cloud" }, ): Promise { const source = options?.source ?? "cloud"; - return await userCharactersRepository.listByOrganization(organizationId, source); + return await userCharactersRepository.listByOrganization( + organizationId, + source, + ); } async listPublic(): Promise { @@ -202,7 +210,9 @@ export class CharactersService { let username = data.username; if (!username) { username = await this.generateUniqueUsername(data.name); - logger.info(`[Characters] Generated username: @${username} for "${data.name}"`); + logger.info( + `[Characters] Generated username: @${username} for "${data.name}"`, + ); } else { // Validate provided username const validation = validateUsername(username); @@ -248,7 +258,10 @@ export class CharactersService { return character; } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const updated = await userCharactersRepository.update(id, data); // Invalidate cache on update if (updated) { @@ -274,11 +287,16 @@ export class CharactersService { if (updates.username !== undefined) { if (updates.username === null) { // Allow clearing username - no validation needed - logger.info(`[Characters] Username cleared: @${character.username} → null`); + logger.info( + `[Characters] Username cleared: @${character.username} → null`, + ); } else { const normalizedUsername = updates.username.toLowerCase(); if (normalizedUsername !== character.username) { - const validation = await this.validateUsernameForUpdate(normalizedUsername, characterId); + const validation = await this.validateUsernameForUpdate( + normalizedUsername, + characterId, + ); if (!validation.valid) { throw new Error(`Invalid username: ${validation.error}`); } @@ -338,7 +356,9 @@ export class CharactersService { */ toElizaCharacter(character: UserCharacter): ElizaCharacter { // Extract affiliate data from character_data if present - const characterData = character.character_data as Record | undefined; + const characterData = character.character_data as + | Record + | undefined; const affiliateData = characterData?.affiliate as | { vibe?: string; affiliateId?: string; [key: string]: unknown } | undefined; @@ -379,7 +399,10 @@ export class CharactersService { Array.isArray(ex) && ex.every( (msg) => - typeof msg === "object" && msg !== null && "name" in msg && "content" in msg, + typeof msg === "object" && + msg !== null && + "name" in msg && + "content" in msg, ), ) ) { @@ -390,12 +413,16 @@ export class CharactersService { postExamples: character.post_examples as string[] | undefined, topics: character.topics as string[] | undefined, adjectives: character.adjectives as string[] | undefined, - knowledge: character.knowledge as (string | { path: string; shared?: boolean })[] | undefined, + knowledge: character.knowledge as + | (string | { path: string; shared?: boolean })[] + | undefined, plugins: character.plugins as string[] | undefined, settings: mergedSettings as | Record> | undefined, - secrets: character.secrets as Record | undefined, + secrets: character.secrets as + | Record + | undefined, style: character.style as | { all?: string[]; @@ -434,7 +461,8 @@ export class CharactersService { } // Check if owned by an affiliate anonymous user - const isAffiliateUser = owner.email?.includes("@anonymous.elizacloud.ai") || false; + const isAffiliateUser = + owner.email?.includes("@anonymous.elizacloud.ai") || false; const isAnonymous = owner.is_anonymous === true; const hasNoPrivyId = !owner.privy_user_id; @@ -466,14 +494,19 @@ export class CharactersService { const claimCheck = await this.isClaimableAffiliateCharacter(characterId); if (!claimCheck.claimable) { - logger.info(`[Characters] Character ${characterId} not claimable: ${claimCheck.reason}`); + logger.info( + `[Characters] Character ${characterId} not claimable: ${claimCheck.reason}`, + ); return { success: false, message: claimCheck.reason || "Not claimable" }; } const previousOwnerId = claimCheck.ownerId; - logger.info(`[Characters] 🎯 Claiming affiliate character ${characterId} for user ${userId}`, { - previousOwnerId, - }); + logger.info( + `[Characters] 🎯 Claiming affiliate character ${characterId} for user ${userId}`, + { + previousOwnerId, + }, + ); // Transfer character ownership const updated = await userCharactersRepository.update(characterId, { @@ -502,19 +535,25 @@ export class CharactersService { .returning({ room_id: elizaRoomCharactersTable.room_id }); if (roomUpdateResult.length > 0) { - logger.info(`[Characters] Transferred ${roomUpdateResult.length} room association(s)`, { - characterId, - fromUserId: previousOwnerId, - toUserId: userId, - }); + logger.info( + `[Characters] Transferred ${roomUpdateResult.length} room association(s)`, + { + characterId, + fromUserId: previousOwnerId, + toUserId: userId, + }, + ); } } - logger.info(`[Characters] ✅ Successfully claimed character ${characterId}`, { - characterName: updated.name, - newOwnerId: userId, - newOrgId: organizationId, - }); + logger.info( + `[Characters] ✅ Successfully claimed character ${characterId}`, + { + characterName: updated.name, + newOwnerId: userId, + newOrgId: organizationId, + }, + ); return { success: true, @@ -566,7 +605,9 @@ export class CharactersService { avatar_url: userCharacters.avatar_url, bio: userCharacters.bio, owner_id: userCharacters.user_id, - owner_name: sql`COALESCE(${users.name}, ${users.nickname})`.as("owner_name"), + owner_name: sql< + string | null + >`COALESCE(${users.name}, ${users.nickname})`.as("owner_name"), last_interaction_time: sql`MAX(${memoryTable.createdAt})`.as( "last_interaction_time", ), @@ -647,7 +688,9 @@ export class CharactersService { const messageResult = await dbRead .select({ count: sql`count(*)` }) .from(memoryTable) - .where(and(eq(memoryTable.entityId, userId), eq(memoryTable.agentId, agentId))); + .where( + and(eq(memoryTable.entityId, userId), eq(memoryTable.agentId, agentId)), + ); const messageCount = messageResult[0]?.count ?? 0; // Get rooms count for this user+agent combination @@ -655,7 +698,12 @@ export class CharactersService { .select({ count: sql`count(*)` }) .from(roomTable) .innerJoin(participantTable, eq(roomTable.id, participantTable.roomId)) - .where(and(eq(roomTable.agentId, agentId), eq(participantTable.entityId, userId))); + .where( + and( + eq(roomTable.agentId, agentId), + eq(participantTable.entityId, userId), + ), + ); const roomCount = roomResult[0]?.count ?? 0; return { @@ -702,7 +750,10 @@ export class CharactersService { // Verify this is a saved agent (not owned by user) const agent = await dbRead.query.userCharacters.findFirst({ - where: and(eq(userCharacters.id, agentId), ne(userCharacters.user_id, userId)), + where: and( + eq(userCharacters.id, agentId), + ne(userCharacters.user_id, userId), + ), }); if (!agent) { @@ -714,13 +765,18 @@ export class CharactersService { .select({ roomId: participantTable.roomId }) .from(participantTable) .innerJoin(roomTable, eq(participantTable.roomId, roomTable.id)) - .where(and(eq(participantTable.entityId, userId), eq(roomTable.agentId, agentId))); + .where( + and( + eq(participantTable.entityId, userId), + eq(roomTable.agentId, agentId), + ), + ); const roomIds = userRooms.map((r) => r.roomId); // Use transaction to ensure atomicity of all delete operations - const { deletedMemories, deletedParticipants, deletedRooms } = await dbWrite.transaction( - async (tx) => { + const { deletedMemories, deletedParticipants, deletedRooms } = + await dbWrite.transaction(async (tx) => { let memoryCount = 0; let participantCount = 0; let roomCount = 0; @@ -731,7 +787,12 @@ export class CharactersService { // the agent's memories and other users' data const memoryResult = await tx .delete(memoryTable) - .where(and(eq(memoryTable.entityId, userId), inArray(memoryTable.roomId, roomIds))) + .where( + and( + eq(memoryTable.entityId, userId), + inArray(memoryTable.roomId, roomIds), + ), + ) .returning({ id: memoryTable.id }); memoryCount = memoryResult.length; @@ -739,7 +800,10 @@ export class CharactersService { const participantResult = await tx .delete(participantTable) .where( - and(eq(participantTable.entityId, userId), inArray(participantTable.roomId, roomIds)), + and( + eq(participantTable.entityId, userId), + inArray(participantTable.roomId, roomIds), + ), ) .returning({ id: participantTable.id }); participantCount = participantResult.length; @@ -763,12 +827,16 @@ export class CharactersService { // Find rooms with no remaining participants (count = 0 or not in results) const emptyRoomIds = roomIds.filter( - (roomId) => !participantCountMap.has(roomId) || participantCountMap.get(roomId) === 0, + (roomId) => + !participantCountMap.has(roomId) || + participantCountMap.get(roomId) === 0, ); // Delete all empty rooms in a single batch query if (emptyRoomIds.length > 0) { - await tx.delete(roomTable).where(inArray(roomTable.id, emptyRoomIds)); + await tx + .delete(roomTable) + .where(inArray(roomTable.id, emptyRoomIds)); roomCount = emptyRoomIds.length; } } @@ -777,7 +845,12 @@ export class CharactersService { // that might be in other rooms (e.g., world rooms, etc.) const directMemoryResult = await tx .delete(memoryTable) - .where(and(eq(memoryTable.entityId, userId), eq(memoryTable.agentId, agentId))) + .where( + and( + eq(memoryTable.entityId, userId), + eq(memoryTable.agentId, agentId), + ), + ) .returning({ id: memoryTable.id }); memoryCount += directMemoryResult.length; @@ -786,8 +859,7 @@ export class CharactersService { deletedParticipants: participantCount, deletedRooms: roomCount, }; - }, - ); + }); logger.info("[Characters] Removed saved agent:", { userId, diff --git a/packages/lib/services/cli-auth-sessions.ts b/packages/lib/services/cli-auth-sessions.ts index 1546912f4..bf9d19f70 100644 --- a/packages/lib/services/cli-auth-sessions.ts +++ b/packages/lib/services/cli-auth-sessions.ts @@ -2,7 +2,10 @@ * Service for managing CLI authentication sessions. */ -import { apiKeysRepository, cliAuthSessionsRepository } from "@/db/repositories"; +import { + apiKeysRepository, + cliAuthSessionsRepository, +} from "@/db/repositories"; import type { CliAuthSession } from "@/db/schemas/cli-auth-sessions"; import { apiKeysService } from "./api-keys"; @@ -41,7 +44,8 @@ export class CliAuthSessionsService { * Get active session (not expired) */ async getActiveSession(sessionId: string): Promise { - const session = await cliAuthSessionsRepository.findActiveBySessionId(sessionId); + const session = + await cliAuthSessionsRepository.findActiveBySessionId(sessionId); // Check if session is expired if (session && new Date() > new Date(session.expires_at)) { @@ -120,7 +124,11 @@ export class CliAuthSessionsService { } | null> { const session = await this.getActiveSession(sessionId); - if (!session || session.status !== "authenticated" || !session.api_key_plain) { + if ( + !session || + session.status !== "authenticated" || + !session.api_key_plain + ) { return null; } diff --git a/packages/lib/services/cloudformation.ts b/packages/lib/services/cloudformation.ts index ae516a48e..b30782080 100644 --- a/packages/lib/services/cloudformation.ts +++ b/packages/lib/services/cloudformation.ts @@ -94,11 +94,15 @@ export class CloudFormationService { // Local development (from lib/services/) path.join(__dirname, "../../scripts/cloudformation/per-user-stack.json"), // Build output (from .next/server/) - path.join(__dirname, "../../../scripts/cloudformation/per-user-stack.json"), + path.join( + __dirname, + "../../../scripts/cloudformation/per-user-stack.json", + ), ]; // Find the first path that exists - this.templatePath = possiblePaths.find((p) => fs.existsSync(p)) || possiblePaths[0]; + this.templatePath = + possiblePaths.find((p) => fs.existsSync(p)) || possiblePaths[0]; } /** @@ -162,21 +166,29 @@ export class CloudFormationService { maxRetries: number = 3, delayMs: number = 1000, ): Promise { - logger.info(`[CloudFormation withRetry] Starting operation with ${maxRetries} max retries`); + logger.info( + `[CloudFormation withRetry] Starting operation with ${maxRetries} max retries`, + ); for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - logger.info(`[CloudFormation withRetry] Attempt ${attempt}/${maxRetries}`); + logger.info( + `[CloudFormation withRetry] Attempt ${attempt}/${maxRetries}`, + ); const result = await operation(); logger.info(`[CloudFormation withRetry] Attempt ${attempt} succeeded`); return result; } catch (error: unknown) { // Don't retry validation errors if (error instanceof Error && error.name === "ValidationError") { - logger.error(`[CloudFormation withRetry] ValidationError, not retrying:`, error.message); + logger.error( + `[CloudFormation withRetry] ValidationError, not retrying:`, + error.message, + ); throw error; } - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); logger.error( `[CloudFormation withRetry] Attempt ${attempt}/${maxRetries} failed:`, errorMessage, @@ -220,20 +232,32 @@ export class CloudFormationService { ); // Load template and parse as JSON for dynamic modification - const templateJson = JSON.parse(fs.readFileSync(this.templatePath, "utf-8")); + const templateJson = JSON.parse( + fs.readFileSync(this.templatePath, "utf-8"), + ); // Inject environment variables into container definition if provided - if (config.environmentVars && Object.keys(config.environmentVars).length > 0) { - const envArray = Object.entries(config.environmentVars).map(([name, value]) => ({ - Name: name, - Value: value, - })); + if ( + config.environmentVars && + Object.keys(config.environmentVars).length > 0 + ) { + const envArray = Object.entries(config.environmentVars).map( + ([name, value]) => ({ + Name: name, + Value: value, + }), + ); // Find the TaskDefinition resource and inject environment variables - if (templateJson.Resources?.TaskDefinition?.Properties?.ContainerDefinitions?.[0]) { + if ( + templateJson.Resources?.TaskDefinition?.Properties + ?.ContainerDefinitions?.[0] + ) { templateJson.Resources.TaskDefinition.Properties.ContainerDefinitions[0].Environment = envArray; - logger.info(`Injected ${envArray.length} environment variables into task definition`); + logger.info( + `Injected ${envArray.length} environment variables into task definition`, + ); } } @@ -271,7 +295,9 @@ export class CloudFormationService { ParameterKey: "Architecture", // Translate x86_64 to amd64 for CloudFormation (underscores not allowed in mapping keys) ParameterValue: - config.architecture === "x86_64" ? "amd64" : config.architecture || "arm64", + config.architecture === "x86_64" + ? "amd64" + : config.architecture || "arm64", }, { ParameterKey: "SharedVPCId", ParameterValue: sharedOutputs.vpcId }, { @@ -321,7 +347,10 @@ export class CloudFormationService { return response.StackId!; } catch (createError) { logger.error(`❌ [CloudFormation] CreateStack API call failed:`, { - error: createError instanceof Error ? createError.message : String(createError), + error: + createError instanceof Error + ? createError.message + : String(createError), stack: createError instanceof Error ? createError.stack : undefined, name: createError instanceof Error ? createError.name : undefined, }); @@ -342,17 +371,27 @@ export class CloudFormationService { const stackName = this.getStackName(config.userId, config.projectName); // Load template and parse as JSON for dynamic modification - const templateJson = JSON.parse(fs.readFileSync(this.templatePath, "utf-8")); + const templateJson = JSON.parse( + fs.readFileSync(this.templatePath, "utf-8"), + ); // Inject environment variables into container definition if provided - if (config.environmentVars && Object.keys(config.environmentVars).length > 0) { - const envArray = Object.entries(config.environmentVars).map(([name, value]) => ({ - Name: name, - Value: value, - })); + if ( + config.environmentVars && + Object.keys(config.environmentVars).length > 0 + ) { + const envArray = Object.entries(config.environmentVars).map( + ([name, value]) => ({ + Name: name, + Value: value, + }), + ); // Find the TaskDefinition resource and inject environment variables - if (templateJson.Resources?.TaskDefinition?.Properties?.ContainerDefinitions?.[0]) { + if ( + templateJson.Resources?.TaskDefinition?.Properties + ?.ContainerDefinitions?.[0] + ) { templateJson.Resources.TaskDefinition.Properties.ContainerDefinitions[0].Environment = envArray; logger.info( @@ -368,7 +407,10 @@ export class CloudFormationService { const sharedOutputs = await this.getSharedInfrastructureOutputs(); // Get current ALB priority for this stack (don't allocate a new one for updates) - const currentStack = await this.getStack(config.userId, config.projectName); + const currentStack = await this.getStack( + config.userId, + config.projectName, + ); let albPriority: number; if (!currentStack) { @@ -382,7 +424,9 @@ export class CloudFormationService { if (priorityParam?.ParameterValue) { albPriority = parseInt(priorityParam.ParameterValue, 10); } else { - throw new Error(`Unable to find existing ALB priority for stack ${stackName}`); + throw new Error( + `Unable to find existing ALB priority for stack ${stackName}`, + ); } const command = new UpdateStackCommand({ @@ -413,7 +457,9 @@ export class CloudFormationService { ParameterKey: "Architecture", // Translate x86_64 to amd64 for CloudFormation (underscores not allowed in mapping keys) ParameterValue: - config.architecture === "x86_64" ? "amd64" : config.architecture || "arm64", + config.architecture === "x86_64" + ? "amd64" + : config.architecture || "arm64", }, { ParameterKey: "SharedVPCId", ParameterValue: sharedOutputs.vpcId }, { @@ -510,7 +556,9 @@ export class CloudFormationService { const failureMessage = failureDetails.length > 0 - ? failureDetails.map((f) => `\n • ${f.resource}: ${f.reason}`).join("") + ? failureDetails + .map((f) => `\n • ${f.resource}: ${f.reason}`) + .join("") : "\n (No detailed failure events found - check AWS CloudFormation console)"; throw new Error( @@ -525,7 +573,9 @@ export class CloudFormationService { await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds } - throw new Error(`Stack ${stackName} creation timeout after ${timeoutMinutes} minutes`); + throw new Error( + `Stack ${stackName} creation timeout after ${timeoutMinutes} minutes`, + ); } /** @@ -560,7 +610,8 @@ export class CloudFormationService { .slice(0, 10); return rollbackEvents.map((event) => ({ - resource: `${event.LogicalResourceId} (${event.ResourceType})` || "Unknown", + resource: + `${event.LogicalResourceId} (${event.ResourceType})` || "Unknown", reason: event.ResourceStatusReason || "No reason provided", status: event.ResourceStatus || "Unknown", })); @@ -569,7 +620,8 @@ export class CloudFormationService { return failedEvents .slice(0, 10) // Get top 10 failures .map((event) => ({ - resource: `${event.LogicalResourceId} (${event.ResourceType})` || "Unknown", + resource: + `${event.LogicalResourceId} (${event.ResourceType})` || "Unknown", reason: event.ResourceStatusReason || "No reason provided", status: event.ResourceStatus || "Unknown", })); @@ -599,7 +651,8 @@ export class CloudFormationService { // AWS throws ValidationError if stack doesn't exist if ( error instanceof Error && - (error.name === "ValidationError" || error.message.includes("does not exist")) + (error.name === "ValidationError" || + error.message.includes("does not exist")) ) { return null; } @@ -610,7 +663,10 @@ export class CloudFormationService { /** * Get stack outputs */ - async getStackOutputs(userId: string, projectName: string): Promise { + async getStackOutputs( + userId: string, + projectName: string, + ): Promise { const stack = await this.getStack(userId, projectName); if (!stack || !stack.Outputs) { @@ -682,14 +738,20 @@ export class CloudFormationService { if (status === "DELETE_FAILED") { const failureReason = stack.StackStatusReason || "Unknown failure"; - throw new Error(`Stack ${stackName} deletion failed. Reason: ${failureReason}`); + throw new Error( + `Stack ${stackName} deletion failed. Reason: ${failureReason}`, + ); } - logger.info(`Stack ${stackName} status: ${status}, waiting for deletion...`); + logger.info( + `Stack ${stackName} status: ${status}, waiting for deletion...`, + ); await new Promise((resolve) => setTimeout(resolve, 10000)); } - throw new Error(`Stack ${stackName} deletion timeout after ${timeoutMinutes} minutes`); + throw new Error( + `Stack ${stackName} deletion timeout after ${timeoutMinutes} minutes`, + ); } /** @@ -723,7 +785,9 @@ export class CloudFormationService { const getOutput = (key: string): string => { const output = stack.Outputs!.find((o) => o.OutputKey === key); if (!output?.OutputValue) { - throw new Error(`Missing output ${key} in shared infrastructure stack`); + throw new Error( + `Missing output ${key} in shared infrastructure stack`, + ); } return output.OutputValue; }; @@ -738,8 +802,12 @@ export class CloudFormationService { albSecurityGroupId: getOutput("ALBSecurityGroupId"), }; } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error("Failed to get shared infrastructure outputs:", errorMessage); + const errorMessage = + error instanceof Error ? error.message : String(error); + logger.error( + "Failed to get shared infrastructure outputs:", + errorMessage, + ); throw new Error( `Cannot provision user stack: shared infrastructure not deployed. Run deploy-shared.sh first.`, ); @@ -797,7 +865,9 @@ export class CloudFormationService { await new Promise((resolve) => setTimeout(resolve, 10000)); } - throw new Error(`Stack ${stackName} update timeout after ${timeoutMinutes} minutes`); + throw new Error( + `Stack ${stackName} update timeout after ${timeoutMinutes} minutes`, + ); } } diff --git a/packages/lib/services/container-quota.ts b/packages/lib/services/container-quota.ts index 812343e9e..8a9361d7c 100644 --- a/packages/lib/services/container-quota.ts +++ b/packages/lib/services/container-quota.ts @@ -23,7 +23,9 @@ export class ContainerQuotaService { async createContainerWithQuotaCheck( data: Parameters[0], - transaction?: Parameters[1], + transaction?: Parameters< + typeof containersRepository.createWithQuotaCheck + >[1], ) { return await containersRepository.createWithQuotaCheck(data, transaction); } diff --git a/packages/lib/services/containers.ts b/packages/lib/services/containers.ts index cf5b741e0..fbaa615b1 100644 --- a/packages/lib/services/containers.ts +++ b/packages/lib/services/containers.ts @@ -71,7 +71,10 @@ export class ContainersService { return await containersRepository.checkQuota(organizationId); } - async createWithQuotaCheck(data: NewContainer, transaction?: Database): Promise { + async createWithQuotaCheck( + data: NewContainer, + transaction?: Database, + ): Promise { return await containersRepository.createWithQuotaCheck(data, transaction); } @@ -98,10 +101,14 @@ export const listContainers = (organizationId: string) => export const getContainer = (id: string, organizationId: string) => containersService.getById(id, organizationId); -export const createContainer = (data: NewContainer) => containersService.create(data); +export const createContainer = (data: NewContainer) => + containersService.create(data); -export const updateContainer = (id: string, organizationId: string, data: Partial) => - containersService.update(id, organizationId, data); +export const updateContainer = ( + id: string, + organizationId: string, + data: Partial, +) => containersService.update(id, organizationId, data); export const deleteContainer = (id: string, organizationId: string) => containersService.delete(id, organizationId); @@ -181,10 +188,16 @@ export const updateContainerStatus = async ( return container; }; -export const updateContainerHealth = (id: string) => containersService.updateHealthCheck(id); +export const updateContainerHealth = (id: string) => + containersService.updateHealthCheck(id); export const createContainerWithCreditDeduction = ( containerData: NewContainer, userId: string, deploymentCost: number, -) => containersService.createContainerWithCreditDeduction(containerData, userId, deploymentCost); +) => + containersService.createContainerWithCreditDeduction( + containerData, + userId, + deploymentCost, + ); diff --git a/packages/lib/services/content-moderation.ts b/packages/lib/services/content-moderation.ts index 09ad9d8b0..fcb6e9cc6 100644 --- a/packages/lib/services/content-moderation.ts +++ b/packages/lib/services/content-moderation.ts @@ -138,7 +138,9 @@ const THRESHOLDS = { */ function containsMinimalBadWords(text: string): boolean { const lowerText = text.toLowerCase(); - return minimalBadWordsArray.some((word: string) => lowerText.includes(word.toLowerCase())); + return minimalBadWordsArray.some((word: string) => + lowerText.includes(word.toLowerCase()), + ); } /** @@ -462,10 +464,12 @@ class ContentModerationService { } | { type: "work"; result: T }; - const moderationRacer: Promise = moderationPromise.then((result) => ({ - type: "moderation" as const, - result, - })); + const moderationRacer: Promise = moderationPromise.then( + (result) => ({ + type: "moderation" as const, + result, + }), + ); const workRacer: Promise = workPromise.then((result) => ({ type: "work" as const, @@ -479,12 +483,15 @@ class ContentModerationService { if (modResult.flagged && modResult.action) { // Moderation finished first with a violation - BLOCK - logger.warn("[ContentModeration] Blocking response - moderation detected violation", { - userId, - roomId, - categories: modResult.flaggedCategories, - action: modResult.action, - }); + logger.warn( + "[ContentModeration] Blocking response - moderation detected violation", + { + userId, + roomId, + categories: modResult.flaggedCategories, + action: modResult.action, + }, + ); throw new ModerationBlockedError( `Content policy violation detected: ${modResult.flaggedCategories.join(", ")}`, @@ -501,18 +508,24 @@ class ContentModerationService { moderationPromise .then((result) => { if (result.flagged) { - logger.info("[ContentModeration] Violation detected after response sent", { - userId, - roomId, - categories: result.flaggedCategories, - action: result.action, - }); + logger.info( + "[ContentModeration] Violation detected after response sent", + { + userId, + roomId, + categories: result.flaggedCategories, + action: result.action, + }, + ); } }) .catch((error) => { - logger.error("[ContentModeration] Background moderation failed after response", { - error: error instanceof Error ? error.message : String(error), - }); + logger.error( + "[ContentModeration] Background moderation failed after response", + { + error: error instanceof Error ? error.message : String(error), + }, + ); }); return firstResult.result; @@ -571,7 +584,8 @@ class ContentModerationService { moderationResult = result; }) .catch((error) => { - moderationError = error instanceof Error ? error : new Error(String(error)); + moderationError = + error instanceof Error ? error : new Error(String(error)); }); return { @@ -581,12 +595,15 @@ class ContentModerationService { // If moderation completed with violation, block if (moderationResult?.flagged && moderationResult.action) { - logger.warn("[ContentModeration] Blocking stream - moderation detected violation", { - userId, - roomId, - categories: moderationResult.flaggedCategories, - action: moderationResult.action, - }); + logger.warn( + "[ContentModeration] Blocking stream - moderation detected violation", + { + userId, + roomId, + categories: moderationResult.flaggedCategories, + action: moderationResult.action, + }, + ); throw new ModerationBlockedError( `Content policy violation detected: ${moderationResult.flaggedCategories.join(", ")}`, @@ -596,9 +613,12 @@ class ContentModerationService { // If moderation errored, log but don't block if (moderationError) { - logger.error("[ContentModeration] Moderation check failed, allowing stream", { - error: moderationError.message, - }); + logger.error( + "[ContentModeration] Moderation check failed, allowing stream", + { + error: moderationError.message, + }, + ); } // Otherwise, allow stream to proceed @@ -668,7 +688,9 @@ class ContentModerationService { async getUsersFlaggedForBan(): Promise { const flagged = await adminService.getUsersFlaggedForReview(); return flagged - .filter((u) => u.totalViolations >= THRESHOLDS.FLAG_FOR_BAN_AFTER_VIOLATIONS) + .filter( + (u) => u.totalViolations >= THRESHOLDS.FLAG_FOR_BAN_AFTER_VIOLATIONS, + ) .map((u) => u.userId); } } diff --git a/packages/lib/services/conversations.ts b/packages/lib/services/conversations.ts index 7ab40fb57..c1ba6b04e 100644 --- a/packages/lib/services/conversations.ts +++ b/packages/lib/services/conversations.ts @@ -19,7 +19,9 @@ export class ConversationsService { return await conversationsRepository.findById(id); } - async getWithMessages(id: string): Promise { + async getWithMessages( + id: string, + ): Promise { return await conversationsRepository.findWithMessages(id); } @@ -27,15 +29,24 @@ export class ConversationsService { return await conversationsRepository.listByUser(userId, limit); } - async listByOrganization(organizationId: string, limit?: number): Promise { - return await conversationsRepository.listByOrganization(organizationId, limit); + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { + return await conversationsRepository.listByOrganization( + organizationId, + limit, + ); } async create(data: NewConversation): Promise { return await conversationsRepository.create(data); } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { return await conversationsRepository.update(id, data); } @@ -80,7 +91,10 @@ export class ConversationsService { conversationId: string, data: Omit, ): Promise { - return await conversationsRepository.addMessageWithSequence(conversationId, data); + return await conversationsRepository.addMessageWithSequence( + conversationId, + data, + ); } } diff --git a/packages/lib/services/conversion-tracking.ts b/packages/lib/services/conversion-tracking.ts index d79548abd..34223f8d5 100644 --- a/packages/lib/services/conversion-tracking.ts +++ b/packages/lib/services/conversion-tracking.ts @@ -103,11 +103,17 @@ class ConversionTrackingService { const [existing] = await dbRead .select() .from(appUsers) - .where(and(eq(appUsers.app_id, event.appId), eq(appUsers.user_id, event.userId))) + .where( + and( + eq(appUsers.app_id, event.appId), + eq(appUsers.user_id, event.userId), + ), + ) .limit(1); if (existing) { - const currentMetadata = (existing.metadata as Record) || {}; + const currentMetadata = + (existing.metadata as Record) || {}; await dbWrite .update(appUsers) .set({ @@ -124,7 +130,11 @@ class ConversionTrackingService { } } - async trackSignupFromUTM(userId: string, appId: string, utmParams: UTMParams): Promise { + async trackSignupFromUTM( + userId: string, + appId: string, + utmParams: UTMParams, + ): Promise { if (!utmParams.utm_campaign) return; const [campaign] = await dbRead .select() diff --git a/packages/lib/services/credits.ts b/packages/lib/services/credits.ts index 508858ba7..3f3732656 100644 --- a/packages/lib/services/credits.ts +++ b/packages/lib/services/credits.ts @@ -16,7 +16,10 @@ import { creditTransactions } from "@/db/schemas/credit-transactions"; import { organizations } from "@/db/schemas/organizations"; import { CacheInvalidation } from "@/lib/cache/invalidation"; import { invalidateOrganizationCache } from "@/lib/cache/organizations-cache"; -import { canSendLowCreditsEmail, markLowCreditsEmailSent } from "@/lib/email/utils/rate-limiter"; +import { + canSendLowCreditsEmail, + markLowCreditsEmailSent, +} from "@/lib/email/utils/rate-limiter"; import { calculateCost, getProviderFromModel } from "@/lib/pricing"; import { logger } from "@/lib/utils/logger"; import type { PricingBillingSource } from "./ai-pricing-definitions"; @@ -117,24 +120,34 @@ export class CreditsService { async getTransactionByStripePaymentIntent( paymentIntentId: string, ): Promise { - return await creditTransactionsRepository.findByStripePaymentIntent(paymentIntentId); + return await creditTransactionsRepository.findByStripePaymentIntent( + paymentIntentId, + ); } async listTransactionsByOrganization( organizationId: string, limit?: number, ): Promise { - return await creditTransactionsRepository.listByOrganization(organizationId, limit); + return await creditTransactionsRepository.listByOrganization( + organizationId, + limit, + ); } async listTransactionsByOrganizationAndType( organizationId: string, type: string, ): Promise { - return await creditTransactionsRepository.listByOrganizationAndType(organizationId, type); + return await creditTransactionsRepository.listByOrganizationAndType( + organizationId, + type, + ); } - async createTransaction(data: NewCreditTransaction): Promise { + async createTransaction( + data: NewCreditTransaction, + ): Promise { return await creditTransactionsRepository.create(data); } @@ -142,7 +155,13 @@ export class CreditsService { transaction: CreditTransaction; newBalance: number; }> { - const { organizationId, amount, description, metadata, stripePaymentIntentId } = params; + const { + organizationId, + amount, + description, + metadata, + stripePaymentIntentId, + } = params; // IDEMPOTENCY: If stripePaymentIntentId is provided, check for existing transaction // This prevents race conditions when both synchronous and webhook calls try to add credits @@ -176,7 +195,10 @@ export class CreditsService { // threads passed the first check but haven't inserted yet if (stripePaymentIntentId) { const existingInTx = await tx.query.creditTransactions.findFirst({ - where: eq(creditTransactions.stripe_payment_intent_id, stripePaymentIntentId), + where: eq( + creditTransactions.stripe_payment_intent_id, + stripePaymentIntentId, + ), }); if (existingInTx) { @@ -242,7 +264,10 @@ export class CreditsService { .then(async (result) => { // Invalidate organization cache since balance changed invalidateOrganizationCache(organizationId).catch((error) => { - logger.error("[CreditsService] Failed to invalidate org cache:", error); + logger.error( + "[CreditsService] Failed to invalidate org cache:", + error, + ); }); return result; }); @@ -314,7 +339,10 @@ export class CreditsService { const currentBalance = Number.parseFloat(String(org.credit_balance)); // Check if balance meets minimum requirement BEFORE deduction - if (minimumBalanceRequired > 0 && currentBalance < minimumBalanceRequired) { + if ( + minimumBalanceRequired > 0 && + currentBalance < minimumBalanceRequired + ) { return { success: false, newBalance: currentBalance, @@ -365,7 +393,10 @@ export class CreditsService { // Invalidate organization cache if balance changed if (result.success) { invalidateOrganizationCache(organizationId).catch((error) => { - logger.error("[CreditsService] Failed to invalidate org cache:", error); + logger.error( + "[CreditsService] Failed to invalidate org cache:", + error, + ); }); // Invalidate balance cache immediately after successful deduction await CacheInvalidation.onCreditMutation(organizationId); @@ -380,19 +411,33 @@ export class CreditsService { tokens_consumed: tokens_consumed || 0, }) .catch((error) => { - logger.error("[CreditsService] Failed to track session usage:", error); + logger.error( + "[CreditsService] Failed to track session usage:", + error, + ); }); } // Check if auto top-up should be triggered - this.checkAndTriggerAutoTopUp(organizationId, result.newBalance).catch((error) => { - logger.error("[CreditsService] Failed to check auto top-up:", error); + this.checkAndTriggerAutoTopUp( + organizationId, + result.newBalance, + ).catch((error) => { + logger.error( + "[CreditsService] Failed to check auto top-up:", + error, + ); }); // Queue low credits email - this.queueLowCreditsEmail(organizationId, result.newBalance).catch((error) => { - logger.error("[CreditsService] Failed to queue low credits email:", error); - }); + this.queueLowCreditsEmail(organizationId, result.newBalance).catch( + (error) => { + logger.error( + "[CreditsService] Failed to queue low credits email:", + error, + ); + }, + ); } return result; }); @@ -440,7 +485,10 @@ export class CreditsService { ); }); } catch (error) { - logger.error(`[CreditsService] Error checking auto top-up for org ${organizationId}:`, error); + logger.error( + `[CreditsService] Error checking auto top-up for org ${organizationId}:`, + error, + ); } } @@ -449,7 +497,10 @@ export class CreditsService { currentBalance: number, ): Promise { try { - const threshold = parseInt(process.env.LOW_CREDITS_THRESHOLD || "1000", 10); + const threshold = parseInt( + process.env.LOW_CREDITS_THRESHOLD || "1000", + 10, + ); if (currentBalance <= 0 || currentBalance > threshold) { return; @@ -549,7 +600,10 @@ export class CreditsService { .then(async (result) => { // Invalidate organization cache since balance changed invalidateOrganizationCache(organizationId).catch((error) => { - logger.error("[CreditsService] Failed to invalidate org cache:", error); + logger.error( + "[CreditsService] Failed to invalidate org cache:", + error, + ); }); return result; }); @@ -571,7 +625,13 @@ export class CreditsService { description: string; metadata?: Record; }): Promise { - const { organizationId, reservedAmount, actualCost, description, metadata } = params; + const { + organizationId, + reservedAmount, + actualCost, + description, + metadata, + } = params; const difference = reservedAmount - actualCost; if (Math.abs(difference) < EPSILON) { @@ -636,7 +696,9 @@ export class CreditsService { organizationId, error: error instanceof Error ? error.message : "Unknown error", }); - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt)); + await new Promise((resolve) => + setTimeout(resolve, RETRY_DELAY_MS * attempt), + ); } } } @@ -675,7 +737,8 @@ export class CreditsService { model = params.model; const provider = params.provider ?? getProviderFromModel(params.model); const estimatedInputTokens = params.estimatedInputTokens ?? 0; - const estimatedOutputTokens = params.estimatedOutputTokens ?? DEFAULT_OUTPUT_TOKENS; + const estimatedOutputTokens = + params.estimatedOutputTokens ?? DEFAULT_OUTPUT_TOKENS; const { totalCost: estimatedCost } = await calculateCost( params.model, @@ -708,7 +771,11 @@ export class CreditsService { available: result.newBalance, reason: result.reason, }); - throw new InsufficientCreditsError(reservedAmount, result.newBalance, result.reason); + throw new InsufficientCreditsError( + reservedAmount, + result.newBalance, + result.reason, + ); } logger.info("[Credits] Reserved", { @@ -746,7 +813,9 @@ export class CreditsService { return await creditPacksRepository.findById(id); } - async getCreditPackByStripePriceId(stripePriceId: string): Promise { + async getCreditPackByStripePriceId( + stripePriceId: string, + ): Promise { return await creditPacksRepository.findByStripePriceId(stripePriceId); } diff --git a/packages/lib/services/crypto-payments.ts b/packages/lib/services/crypto-payments.ts index 5fb95bc1e..441bcbdd6 100644 --- a/packages/lib/services/crypto-payments.ts +++ b/packages/lib/services/crypto-payments.ts @@ -3,16 +3,29 @@ import { eq } from "drizzle-orm"; import { validate as uuidValidate } from "uuid"; import { z } from "zod"; import { dbWrite } from "@/db/client"; -import { type CryptoPayment, cryptoPaymentsRepository } from "@/db/repositories/crypto-payments"; +import { + type CryptoPayment, + cryptoPaymentsRepository, +} from "@/db/repositories/crypto-payments"; import { organizationsRepository } from "@/db/repositories/organizations"; import { cryptoPayments } from "@/db/schemas/crypto-payments"; -import { PAYMENT_EXPIRATION_SECONDS, validatePaymentAmount } from "@/lib/config/crypto"; -import { createCryptoCustomerId, createCryptoInvoiceId } from "@/lib/constants/invoice-ids"; +import { + PAYMENT_EXPIRATION_SECONDS, + validatePaymentAmount, +} from "@/lib/config/crypto"; +import { + createCryptoCustomerId, + createCryptoInvoiceId, +} from "@/lib/constants/invoice-ids"; import { logger, redact } from "@/lib/utils/logger"; import { creditsService } from "./credits"; import { discordService } from "./discord"; import { invoicesService } from "./invoices"; -import { isOxaPayConfigured, type OxaPayNetwork, oxaPayService } from "./oxapay"; +import { + isOxaPayConfigured, + type OxaPayNetwork, + oxaPayService, +} from "./oxapay"; /** * Typed error codes for crypto payment operations. @@ -117,7 +130,10 @@ function getTrackId(metadata: unknown): string { */ function validateUuid(id: string, fieldName: string): void { if (!uuidValidate(id)) { - throw new CryptoPaymentError("INVALID_UUID", `Invalid ${fieldName}: must be a valid UUID`); + throw new CryptoPaymentError( + "INVALID_UUID", + `Invalid ${fieldName}: must be a valid UUID`, + ); } } @@ -133,7 +149,14 @@ class CryptoPaymentsService { trackId: string; creditsToAdd: string; }> { - const { organizationId, userId, amount, currency = "USD", payCurrency, network } = params; + const { + organizationId, + userId, + amount, + currency = "USD", + payCurrency, + network, + } = params; validateUuid(organizationId, "organization ID"); @@ -142,7 +165,10 @@ class CryptoPaymentsService { } if (!isOxaPayConfigured()) { - throw new CryptoPaymentError("SERVICE_NOT_CONFIGURED", "Payment service not configured"); + throw new CryptoPaymentError( + "SERVICE_NOT_CONFIGURED", + "Payment service not configured", + ); } const amountDecimal = new Decimal(amount); @@ -152,16 +178,21 @@ class CryptoPaymentsService { const errorCode = validation.error?.includes("at least") ? "AMOUNT_TOO_SMALL" : "AMOUNT_TOO_LARGE"; - throw new CryptoPaymentError(errorCode, validation.error || "Invalid amount"); + throw new CryptoPaymentError( + errorCode, + validation.error || "Invalid amount", + ); } // OXAPAY_CALLBACK_URL: Override for local development with ngrok. // In production, falls back to NEXT_PUBLIC_APP_URL which points to the live domain. const callbackUrl = - process.env.OXAPAY_CALLBACK_URL || `${process.env.NEXT_PUBLIC_APP_URL}/api/crypto/webhook`; + process.env.OXAPAY_CALLBACK_URL || + `${process.env.NEXT_PUBLIC_APP_URL}/api/crypto/webhook`; const returnUrl = - process.env.OXAPAY_RETURN_URL || `${process.env.NEXT_PUBLIC_APP_URL}/payment/success`; + process.env.OXAPAY_RETURN_URL || + `${process.env.NEXT_PUBLIC_APP_URL}/payment/success`; // Add random suffix to prevent collision if two payments created in same millisecond const randomSuffix = Math.random().toString(36).slice(2, 6); @@ -256,11 +287,16 @@ class CryptoPaymentsService { if (oxaPayService.isPaymentConfirmed(oxaStatus.status)) { const tx = oxaStatus.transactions[0]; if (!tx) { - logger.error("[Crypto Payments] Payment confirmed but no transactions found", { - paymentId: redact.paymentId(paymentId), - trackId: redact.trackId(trackId), - }); - throw new Error("Payment confirmed but no transaction data available"); + logger.error( + "[Crypto Payments] Payment confirmed but no transactions found", + { + paymentId: redact.paymentId(paymentId), + trackId: redact.trackId(trackId), + }, + ); + throw new Error( + "Payment confirmed but no transaction data available", + ); } // tx.amount now correctly contains the USD credit amount (handles auto-conversion) @@ -275,9 +311,16 @@ class CryptoPaymentsService { network: payment.network, }); - await this.confirmPayment(payment.id, tx.txHash, receivedAmount.toString(), tx.currency); + await this.confirmPayment( + payment.id, + tx.txHash, + receivedAmount.toString(), + tx.currency, + ); - const confirmedPayment = await cryptoPaymentsRepository.findById(payment.id); + const confirmedPayment = await cryptoPaymentsRepository.findById( + payment.id, + ); if (!confirmedPayment) { throw new Error("Failed to retrieve confirmed payment"); } @@ -290,7 +333,9 @@ class CryptoPaymentsService { if (oxaPayService.isPaymentExpired(oxaStatus.status)) { await cryptoPaymentsRepository.markAsExpired(payment.id); - const expiredPayment = await cryptoPaymentsRepository.findById(payment.id); + const expiredPayment = await cryptoPaymentsRepository.findById( + payment.id, + ); if (!expiredPayment) { throw new Error("Failed to retrieve expired payment"); } @@ -302,8 +347,13 @@ class CryptoPaymentsService { } if (oxaPayService.isPaymentFailed(oxaStatus.status)) { - await cryptoPaymentsRepository.markAsFailed(payment.id, oxaStatus.status); - const failedPayment = await cryptoPaymentsRepository.findById(payment.id); + await cryptoPaymentsRepository.markAsFailed( + payment.id, + oxaStatus.status, + ); + const failedPayment = await cryptoPaymentsRepository.findById( + payment.id, + ); if (!failedPayment) { throw new Error("Failed to retrieve failed payment"); } @@ -519,10 +569,13 @@ class CryptoPaymentsService { try { trackId = getTrackId(payment.metadata); } catch { - logger.error("[Crypto Payments] Missing track ID for on-chain verification", { - paymentId: redact.paymentId(paymentId), - txHash: redact.txHash(txHash), - }); + logger.error( + "[Crypto Payments] Missing track ID for on-chain verification", + { + paymentId: redact.paymentId(paymentId), + txHash: redact.txHash(txHash), + }, + ); return { success: false, message: "Payment configuration error - missing track ID", @@ -534,12 +587,15 @@ class CryptoPaymentsService { // Check if the payment is confirmed on OxaPay's side if (!oxaPayService.isPaymentConfirmed(oxaStatus.status)) { - logger.warn("[Crypto Payments] On-chain verification failed - payment not confirmed", { - paymentId: redact.paymentId(paymentId), - txHash: redact.txHash(txHash), - trackId: redact.trackId(trackId), - oxaPayStatus: oxaStatus.status, - }); + logger.warn( + "[Crypto Payments] On-chain verification failed - payment not confirmed", + { + paymentId: redact.paymentId(paymentId), + txHash: redact.txHash(txHash), + trackId: redact.trackId(trackId), + oxaPayStatus: oxaStatus.status, + }, + ); return { success: false, message: `Payment not yet confirmed by blockchain. Current status: ${oxaStatus.status}`, @@ -553,13 +609,18 @@ class CryptoPaymentsService { if (!matchingTx) { // List the valid transaction hashes for debugging (redacted) - const validHashes = oxaStatus.transactions.map((txn) => redact.txHash(txn.txHash)); - logger.warn("[Crypto Payments] Transaction hash not found in OxaPay records", { - paymentId: redact.paymentId(paymentId), - providedTxHash: redact.txHash(txHash), - trackId: redact.trackId(trackId), - validTransactions: validHashes, - }); + const validHashes = oxaStatus.transactions.map((txn) => + redact.txHash(txn.txHash), + ); + logger.warn( + "[Crypto Payments] Transaction hash not found in OxaPay records", + { + paymentId: redact.paymentId(paymentId), + providedTxHash: redact.txHash(txHash), + trackId: redact.trackId(trackId), + validTransactions: validHashes, + }, + ); return { success: false, message: @@ -569,15 +630,18 @@ class CryptoPaymentsService { // matchingTx.amount now correctly contains the USD credit amount (handles auto-conversion) const receivedAmount = new Decimal(matchingTx.amount); - logger.info("[Crypto Payments] Manual verification - payment received", { - paymentId: redact.paymentId(paymentId), - txHash: redact.txHash(txHash), - expectedAmount: payment.expected_amount, - creditAmount: receivedAmount.toString(), - nativeAmount: matchingTx.nativeAmount, - usdAmount: matchingTx.usdAmount, - payCurrency: matchingTx.currency, - }); + logger.info( + "[Crypto Payments] Manual verification - payment received", + { + paymentId: redact.paymentId(paymentId), + txHash: redact.txHash(txHash), + expectedAmount: payment.expected_amount, + creditAmount: receivedAmount.toString(), + nativeAmount: matchingTx.nativeAmount, + usdAmount: matchingTx.usdAmount, + payCurrency: matchingTx.currency, + }, + ); // Check if this transaction hash is already used by another payment const existingTxResult = await tx @@ -586,7 +650,10 @@ class CryptoPaymentsService { .where(eq(cryptoPayments.transaction_hash, txHash)) .for("update"); - if (existingTxResult.length > 0 && existingTxResult[0].id !== paymentId) { + if ( + existingTxResult.length > 0 && + existingTxResult[0].id !== paymentId + ) { logger.error("[Crypto Payments] Double-spend attempt detected", { paymentId: redact.paymentId(paymentId), txHash: redact.txHash(txHash), @@ -696,7 +763,13 @@ class CryptoPaymentsService { address?: string; txID?: string; }): Promise<{ success: boolean; message: string }> { - const { track_id, status, amount: webhookAmount, pay_amount: webhookPayAmount, txID } = payload; + const { + track_id, + status, + amount: webhookAmount, + pay_amount: webhookPayAmount, + txID, + } = payload; if (typeof track_id !== "string" || typeof status !== "string") { throw new Error("Invalid webhook payload"); @@ -731,11 +804,14 @@ class CryptoPaymentsService { const oxaStatus = await oxaPayService.getPaymentStatus(track_id); if (!oxaPayService.isPaymentConfirmed(oxaStatus.status)) { - logger.warn("[Crypto Payments] Webhook status mismatch - OxaPay API disagrees", { - track_id: redact.trackId(track_id), - webhookStatus: status, - apiStatus: oxaStatus.status, - }); + logger.warn( + "[Crypto Payments] Webhook status mismatch - OxaPay API disagrees", + { + track_id: redact.trackId(track_id), + webhookStatus: status, + apiStatus: oxaStatus.status, + }, + ); return { success: false, message: "Payment status verification failed", @@ -744,9 +820,12 @@ class CryptoPaymentsService { const tx = oxaStatus.transactions[0]; if (!tx) { - logger.error("[Crypto Payments] Webhook confirmed but no transaction data from API", { - track_id: redact.trackId(track_id), - }); + logger.error( + "[Crypto Payments] Webhook confirmed but no transaction data from API", + { + track_id: redact.trackId(track_id), + }, + ); return { success: false, message: "No transaction data available" }; } @@ -794,10 +873,13 @@ class CryptoPaymentsService { } } - async listPaymentsByOrganization(organizationId: string): Promise { + async listPaymentsByOrganization( + organizationId: string, + ): Promise { validateUuid(organizationId, "organization ID"); - const payments = await cryptoPaymentsRepository.listByOrganization(organizationId); + const payments = + await cryptoPaymentsRepository.listByOrganization(organizationId); return payments.map((p) => this.formatPaymentStatus(p)); } @@ -818,14 +900,18 @@ class CryptoPaymentsService { return { id: payment.id, - trackId: typeof metadata.oxapay_track_id === "string" ? metadata.oxapay_track_id : "", + trackId: + typeof metadata.oxapay_track_id === "string" + ? metadata.oxapay_track_id + : "", status: payment.status, expectedAmount: payment.expected_amount, receivedAmount: payment.received_amount || undefined, creditsToAdd: payment.credits_to_add, network: payment.network, token: payment.token, - payLink: typeof metadata.pay_link === "string" ? metadata.pay_link : undefined, + payLink: + typeof metadata.pay_link === "string" ? metadata.pay_link : undefined, transactionHash: payment.transaction_hash || undefined, expiresAt: payment.expires_at, createdAt: payment.created_at, diff --git a/packages/lib/services/deployments/discovery.ts b/packages/lib/services/deployments/discovery.ts index 428f6abca..cdfdd856d 100644 --- a/packages/lib/services/deployments/discovery.ts +++ b/packages/lib/services/deployments/discovery.ts @@ -17,7 +17,10 @@ import { createHash } from "node:crypto"; import type { UserCharacter } from "@/db/repositories"; import { memoriesRepository } from "@/db/repositories"; import { roomsRepository } from "@/db/repositories/agents"; -import { type AgentStats, agentStateCache } from "@/lib/cache/agent-state-cache"; +import { + type AgentStats, + agentStateCache, +} from "@/lib/cache/agent-state-cache"; import { logger } from "@/lib/utils/logger"; import { charactersService } from "../characters/characters"; import { containersService } from "../containers"; @@ -87,7 +90,10 @@ export class CharacterDeploymentDiscoveryService { const filterHash = this.hashFilters(filters || {}); // Check cache first - const cached = await agentStateCache.getAgentList(organizationId, filterHash); + const cached = await agentStateCache.getAgentList( + organizationId, + filterHash, + ); if (cached) { logger.debug(`[Character Discovery] Cache hit for org ${organizationId}`); return { @@ -97,7 +103,9 @@ export class CharacterDeploymentDiscoveryService { }; } - logger.debug(`[Character Discovery] Cache miss, fetching for org ${organizationId}`); + logger.debug( + `[Character Discovery] Cache miss, fetching for org ${organizationId}`, + ); // Fetch characters and containers in parallel const [characters, containers] = await Promise.all([ @@ -107,7 +115,9 @@ export class CharacterDeploymentDiscoveryService { // Build character info with deployment status const characterInfos = await Promise.all( - characters.map((char) => this.buildCharacterInfo(char, containers, includeStats)), + characters.map((char) => + this.buildCharacterInfo(char, containers, includeStats), + ), ); // Filter by deployment status if requested @@ -119,7 +129,11 @@ export class CharacterDeploymentDiscoveryService { } // Cache the result - await agentStateCache.setAgentList(organizationId, filterHash, filteredCharacters); + await agentStateCache.setAgentList( + organizationId, + filterHash, + filteredCharacters, + ); return { characters: filteredCharacters, @@ -167,7 +181,9 @@ export class CharacterDeploymentDiscoveryService { */ private async buildCharacterInfo( character: UserCharacter, - containers: Awaited>, + containers: Awaited< + ReturnType + >, includeStats: boolean, ): Promise { // Find deployment container by character_id FK @@ -251,13 +267,15 @@ export class CharacterDeploymentDiscoveryService { // Character is deployed - fetch statistics from database directly // Get message count for this character's agent across all rooms - const messageCount = await memoriesRepository.countMessagesByAgent(characterId); + const messageCount = + await memoriesRepository.countMessagesByAgent(characterId); // Get room count const roomCount = await roomsRepository.countByAgentId(characterId); // Determine last active time from most recent message - const lastActiveAt = await memoriesRepository.getLastMessageTime(characterId); + const lastActiveAt = + await memoriesRepository.getLastMessageTime(characterId); // Calculate uptime (time since last deployment) let uptime = 0; @@ -286,7 +304,9 @@ export class CharacterDeploymentDiscoveryService { * @param characterIds - Array of character IDs * @returns Map of character ID to stats */ - async getCharacterStatisticsBatch(characterIds: string[]): Promise> { + async getCharacterStatisticsBatch( + characterIds: string[], + ): Promise> { const statsMap = new Map(); if (characterIds.length === 0) { @@ -370,4 +390,5 @@ export class CharacterDeploymentDiscoveryService { } // Export singleton instance -export const characterDeploymentDiscoveryService = new CharacterDeploymentDiscoveryService(); +export const characterDeploymentDiscoveryService = + new CharacterDeploymentDiscoveryService(); diff --git a/packages/lib/services/discord-automation/app-automation.ts b/packages/lib/services/discord-automation/app-automation.ts index 60abd8d4d..6d21cbe4d 100644 --- a/packages/lib/services/discord-automation/app-automation.ts +++ b/packages/lib/services/discord-automation/app-automation.ts @@ -22,7 +22,11 @@ import { } from "@/lib/utils/discord-helpers"; import { logger } from "@/lib/utils/logger"; import { discordAutomationService } from "./index"; -import type { DiscordAutomationConfig, DiscordAutomationStatus, PostResult } from "./types"; +import type { + DiscordAutomationConfig, + DiscordAutomationStatus, + PostResult, +} from "./types"; // Content length constants const MAX_ANNOUNCEMENT_LENGTH = 300; // Max chars for AI-generated announcement @@ -32,7 +36,10 @@ class DiscordAppAutomationService { /** * Get app for organization, checking ownership. */ - private async getAppForOrg(organizationId: string, appId: string): Promise { + private async getAppForOrg( + organizationId: string, + appId: string, + ): Promise { const app = await appsRepository.findById(appId); if (!app || app.organization_id !== organizationId) { throw new Error("App not found"); @@ -51,16 +58,24 @@ class DiscordAppAutomationService { const app = await this.getAppForOrg(organizationId, appId); // Verify Discord is connected - const status = await discordAutomationService.getConnectionStatus(organizationId); + const status = + await discordAutomationService.getConnectionStatus(organizationId); if (!status.connected) { - throw new Error("Discord not connected. Add the bot to a server in Settings first."); + throw new Error( + "Discord not connected. Add the bot to a server in Settings first.", + ); } // Verify the guild exists if specified if (config.guildId) { - const guild = await discordGuildsRepository.findByGuildId(organizationId, config.guildId); + const guild = await discordGuildsRepository.findByGuildId( + organizationId, + config.guildId, + ); if (!guild) { - throw new Error("Guild not found. Please reconnect the Discord server."); + throw new Error( + "Guild not found. Please reconnect the Discord server.", + ); } } @@ -138,7 +153,8 @@ class DiscordAppAutomationService { appId: string, ): Promise { const app = await this.getAppForOrg(organizationId, appId); - const connectionStatus = await discordAutomationService.getConnectionStatus(organizationId); + const connectionStatus = + await discordAutomationService.getConnectionStatus(organizationId); const config = getDiscordConfigWithDefaults( app.discord_automation as Record | null, @@ -149,7 +165,10 @@ class DiscordAppAutomationService { let channelName: string | undefined; if (config.guildId) { - const guild = await discordGuildsRepository.findByGuildId(organizationId, config.guildId); + const guild = await discordGuildsRepository.findByGuildId( + organizationId, + config.guildId, + ); guildName = guild?.guild_name; } @@ -177,7 +196,10 @@ class DiscordAppAutomationService { }; } - async generateAnnouncement(organizationId: string, app: App): Promise { + async generateAnnouncement( + organizationId: string, + app: App, + ): Promise { const deduction = await creditsService.deductCredits({ organizationId, amount: DISCORD_POST_COST, @@ -196,7 +218,9 @@ class DiscordAppAutomationService { let characterPrompt = ""; if (config?.agentCharacterId) { - const characterContext = await getCharacterPromptContext(config.agentCharacterId); + const characterContext = await getCharacterPromptContext( + config.agentCharacterId, + ); if (characterContext) { characterPrompt = buildCharacterSystemPrompt(characterContext); logger.info("[DiscordAppAutomation] Using character voice", { @@ -205,15 +229,21 @@ class DiscordAppAutomationService { characterName: characterContext.name, }); } else { - logger.warn("[DiscordAppAutomation] Character not found, using default", { - appId: app.id, - characterId: config.agentCharacterId, - }); + logger.warn( + "[DiscordAppAutomation] Character not found, using default", + { + appId: app.id, + characterId: config.agentCharacterId, + }, + ); } } else { - logger.info("[DiscordAppAutomation] No character selected, using default voice", { - appId: app.id, - }); + logger.info( + "[DiscordAppAutomation] No character selected, using default voice", + { + appId: app.id, + }, + ); } const systemPrompt = characterPrompt @@ -316,7 +346,8 @@ Maximum ${MAX_ANNOUNCEMENT_LENGTH} characters. Do not include the URL in your re }; } - const messageText = text || (await this.generateAnnouncement(organizationId, app)); + const messageText = + text || (await this.generateAnnouncement(organizationId, app)); // Get promotional image if available const promotionalImageUrl = this.getPromotionalImage(app); @@ -333,12 +364,18 @@ Maximum ${MAX_ANNOUNCEMENT_LENGTH} characters. Do not include the URL in your re // Build button const buttonUrl = app.website_url || app.app_url; - const components = [createActionRow([{ label: "Try It Now", url: buttonUrl }])]; + const components = [ + createActionRow([{ label: "Try It Now", url: buttonUrl }]), + ]; - const result = await discordAutomationService.sendMessage(config.channelId, messageText, { - embeds: [embed], - components, - }); + const result = await discordAutomationService.sendMessage( + config.channelId, + messageText, + { + embeds: [embed], + components, + }, + ); if (result.success) { // Update stats @@ -391,14 +428,18 @@ Maximum ${MAX_ANNOUNCEMENT_LENGTH} characters. Do not include the URL in your re const lastAnnouncement = new Date(config.lastAnnouncementAt); const now = new Date(); - const minutesSince = (now.getTime() - lastAnnouncement.getTime()) / (1000 * 60); + const minutesSince = + (now.getTime() - lastAnnouncement.getTime()) / (1000 * 60); // Use a random interval between min and max for natural timing const minInterval = - config.announceIntervalMin || DISCORD_AUTOMATION_DEFAULTS.announceIntervalMin; + config.announceIntervalMin || + DISCORD_AUTOMATION_DEFAULTS.announceIntervalMin; const maxInterval = - config.announceIntervalMax || DISCORD_AUTOMATION_DEFAULTS.announceIntervalMax; - const targetInterval = minInterval + Math.random() * (maxInterval - minInterval); + config.announceIntervalMax || + DISCORD_AUTOMATION_DEFAULTS.announceIntervalMax; + const targetInterval = + minInterval + Math.random() * (maxInterval - minInterval); return minutesSince >= targetInterval; } diff --git a/packages/lib/services/discord-automation/index.ts b/packages/lib/services/discord-automation/index.ts index b973bd4ba..6800edc46 100644 --- a/packages/lib/services/discord-automation/index.ts +++ b/packages/lib/services/discord-automation/index.ts @@ -77,7 +77,9 @@ class DiscordAutomationService { * Use this for checking if users can add the bot to servers */ isOAuthConfigured(): boolean { - return Boolean(DISCORD_CLIENT_ID && DISCORD_CLIENT_SECRET && DISCORD_BOT_TOKEN); + return Boolean( + DISCORD_CLIENT_ID && DISCORD_CLIENT_SECRET && DISCORD_BOT_TOKEN, + ); } /** @@ -145,7 +147,9 @@ class DiscordAutomationService { throw new Error("Invalid Discord OAuth state signature"); } - const parsed = JSON.parse(Buffer.from(payloadBase64, "base64url").toString("utf8")); + const parsed = JSON.parse( + Buffer.from(payloadBase64, "base64url").toString("utf8"), + ); if (!parsed || typeof parsed !== "object") { throw new Error("Invalid Discord OAuth state payload"); } @@ -153,7 +157,9 @@ class DiscordAutomationService { return parsed as OAuthState; } - async resolveOAuthIdentity(code: string): Promise { + async resolveOAuthIdentity( + code: string, + ): Promise { if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET) { logger.error("[Discord] Discord OAuth is not configured"); return null; @@ -282,7 +288,9 @@ class DiscordAutomationService { return { success: false, error: "Failed to verify Discord account" }; } - const guildAccess = identity.guilds.find((guild) => guild.id === args.guildId); + const guildAccess = identity.guilds.find( + (guild) => guild.id === args.guildId, + ); if (!guildAccess) { return { success: false, @@ -298,11 +306,14 @@ class DiscordAutomationService { } // Fetch guild info using bot token - const guildResponse = await fetch(`${DISCORD_API_BASE}/guilds/${args.guildId}`, { - headers: { - Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + const guildResponse = await fetch( + `${DISCORD_API_BASE}/guilds/${args.guildId}`, + { + headers: { + Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + }, }, - }); + ); if (!guildResponse.ok) { const errorText = await guildResponse.text(); @@ -361,7 +372,10 @@ class DiscordAutomationService { } } - async setGuildBotNickname(guildId: string, nickname: string): Promise { + async setGuildBotNickname( + guildId: string, + nickname: string, + ): Promise { if (!DISCORD_BOT_TOKEN) { return false; } @@ -372,16 +386,19 @@ class DiscordAutomationService { } try { - const response = await fetch(`${DISCORD_API_BASE}/guilds/${guildId}/members/@me`, { - method: "PATCH", - headers: { - Authorization: `Bot ${DISCORD_BOT_TOKEN}`, - "Content-Type": "application/json", + const response = await fetch( + `${DISCORD_API_BASE}/guilds/${guildId}/members/@me`, + { + method: "PATCH", + headers: { + Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nick: trimmed.slice(0, 32), + }), }, - body: JSON.stringify({ - nick: trimmed.slice(0, 32), - }), - }); + ); if (!response.ok) { const errorText = await response.text(); @@ -408,7 +425,9 @@ class DiscordAutomationService { throw new Error("Discord OAuth is not configured"); } - const payloadBase64 = Buffer.from(JSON.stringify(state), "utf8").toString("base64url"); + const payloadBase64 = Buffer.from(JSON.stringify(state), "utf8").toString( + "base64url", + ); const signature = createHmac("sha256", DISCORD_CLIENT_SECRET) .update(payloadBase64) .digest("base64url"); @@ -419,7 +438,9 @@ class DiscordAutomationService { * Get connection status for an organization * Uses canSendMessages() to check if bot can actually post (only needs bot token) */ - async getConnectionStatus(organizationId: string): Promise { + async getConnectionStatus( + organizationId: string, + ): Promise { // Check if bot can send messages (only needs DISCORD_BOT_TOKEN) if (!this.canSendMessages()) { return { @@ -430,7 +451,8 @@ class DiscordAutomationService { } try { - const guilds = await discordGuildsRepository.findByOrganization(organizationId); + const guilds = + await discordGuildsRepository.findByOrganization(organizationId); if (guilds.length === 0) { return { connected: false, guilds: [] }; @@ -465,18 +487,24 @@ class DiscordAutomationService { /** * Fetch and cache channels for a guild */ - async refreshChannels(organizationId: string, guildId: string): Promise { + async refreshChannels( + organizationId: string, + guildId: string, + ): Promise { if (!DISCORD_BOT_TOKEN) { logger.error("[Discord] Bot token not configured"); return []; } try { - const response = await fetch(`${DISCORD_API_BASE}/guilds/${guildId}/channels`, { - headers: { - Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + const response = await fetch( + `${DISCORD_API_BASE}/guilds/${guildId}/channels`, + { + headers: { + Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + }, }, - }); + ); if (!response.ok) { const error = await response.text(); @@ -538,7 +566,10 @@ class DiscordAutomationService { try { // Split message if too long - const chunks = splitMessage(content, DISCORD_RATE_LIMITS.MAX_MESSAGE_LENGTH); + const chunks = splitMessage( + content, + DISCORD_RATE_LIMITS.MAX_MESSAGE_LENGTH, + ); let lastMessageId: string | undefined; for (let i = 0; i < chunks.length; i++) { @@ -608,7 +639,10 @@ class DiscordAutomationService { * Get sendable channels for a guild */ async getSendableChannels(organizationId: string, guildId: string) { - return discordChannelsRepository.findSendableByGuild(organizationId, guildId); + return discordChannelsRepository.findSendableByGuild( + organizationId, + guildId, + ); } /** @@ -638,19 +672,25 @@ class DiscordAutomationService { try { // Try to leave the guild via API - const response = await fetch(`${DISCORD_API_BASE}/users/@me/guilds/${guildId}`, { - method: "DELETE", - headers: { - Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + const response = await fetch( + `${DISCORD_API_BASE}/users/@me/guilds/${guildId}`, + { + method: "DELETE", + headers: { + Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + }, }, - }); + ); // Even if the API call fails (maybe already removed), clean up database if (!response.ok && response.status !== 404) { - logger.warn("[Discord] Failed to leave guild via API, cleaning up database anyway", { - guildId, - status: response.status, - }); + logger.warn( + "[Discord] Failed to leave guild via API, cleaning up database anyway", + { + guildId, + status: response.status, + }, + ); } // Remove from database @@ -690,11 +730,14 @@ class DiscordAutomationService { if (!DISCORD_BOT_TOKEN) return false; try { - const response = await fetch(`${DISCORD_API_BASE}/channels/${channelId}`, { - headers: { - Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + const response = await fetch( + `${DISCORD_API_BASE}/channels/${channelId}`, + { + headers: { + Authorization: `Bot ${DISCORD_BOT_TOKEN}`, + }, }, - }); + ); return response.ok; } catch { return false; diff --git a/packages/lib/services/discord.ts b/packages/lib/services/discord.ts index 4ccc33202..e813fa2e6 100644 --- a/packages/lib/services/discord.ts +++ b/packages/lib/services/discord.ts @@ -83,7 +83,9 @@ class DiscordService { const channelId = process.env.DISCORD_CHANNEL_ID; if (!botToken || !channelId) { - logger.warn("[DiscordService] Not configured. Set DISCORD_BOT_TOKEN and DISCORD_CHANNEL_ID"); + logger.warn( + "[DiscordService] Not configured. Set DISCORD_BOT_TOKEN and DISCORD_CHANNEL_ID", + ); this.initialized = false; return; } @@ -97,7 +99,10 @@ class DiscordService { /** * Send a message to Discord */ - async send(options: DiscordMessageOptions, channelId?: string): Promise { + async send( + options: DiscordMessageOptions, + channelId?: string, + ): Promise { this.initialize(); if (!this.initialized || !this.rest || !this.defaultChannelId) { @@ -121,7 +126,10 @@ class DiscordService { /** * Send a message to a specific channel */ - async sendToChannel(channelId: string, options: DiscordMessageOptions): Promise { + async sendToChannel( + channelId: string, + options: DiscordMessageOptions, + ): Promise { return this.send(options, channelId); } @@ -164,9 +172,12 @@ class DiscordService { threadData.message = { content: data.message }; } - const response = (await this.rest.post(Routes.threads(this.defaultChannelId), { - body: threadData, - })) as { id: string }; + const response = (await this.rest.post( + Routes.threads(this.defaultChannelId), + { + body: threadData, + }, + )) as { id: string }; logger.info(`[DiscordService] Thread created: ${response.id}`); @@ -264,7 +275,9 @@ class DiscordService { }, { name: "New Organization", - value: userData.isNewOrganization ? "✅ Yes" : "❌ No (Joined via invite)", + value: userData.isNewOrganization + ? "✅ Yes" + : "❌ No (Joined via invite)", inline: true, }, ); @@ -355,7 +368,9 @@ class DiscordService { const fields: DiscordEmbedField[] = [ { name: "Prompt", - value: videoData.prompt.slice(0, 200) + (videoData.prompt.length > 200 ? "..." : ""), + value: + videoData.prompt.slice(0, 200) + + (videoData.prompt.length > 200 ? "..." : ""), inline: false, }, { @@ -466,7 +481,9 @@ class DiscordService { const fields: DiscordEmbedField[] = [ { name: "Prompt", - value: imageData.prompt.slice(0, 200) + (imageData.prompt.length > 200 ? "..." : ""), + value: + imageData.prompt.slice(0, 200) + + (imageData.prompt.length > 200 ? "..." : ""), inline: false, }, { @@ -588,7 +605,9 @@ class DiscordService { if (appData.description) { fields.push({ name: "Description", - value: appData.description.slice(0, 200) + (appData.description.length > 200 ? "..." : ""), + value: + appData.description.slice(0, 200) + + (appData.description.length > 200 ? "..." : ""), inline: false, }); } @@ -652,7 +671,8 @@ class DiscordService { isUpdate: boolean; stackName?: string; }): Promise { - const instanceType = containerData.architecture === "arm64" ? "t4g.small" : "t3.small"; + const instanceType = + containerData.architecture === "arm64" ? "t4g.small" : "t3.small"; const fields: DiscordEmbedField[] = [ { @@ -744,7 +764,9 @@ class DiscordService { }); const embed: DiscordEmbed = { - title: containerData.isUpdate ? "🐳 Container Updated" : "🐳 New Container Launched", + title: containerData.isUpdate + ? "🐳 Container Updated" + : "🐳 New Container Launched", description: containerData.isUpdate ? `A container has been updated and redeployed on Eliza Cloud!` : `A new container has been launched on Eliza Cloud!`, @@ -817,7 +839,9 @@ class DiscordService { name: "Plugins", value: characterData.plugins.slice(0, 5).join(", ") + - (characterData.plugins.length > 5 ? `, +${characterData.plugins.length - 5} more` : ""), + (characterData.plugins.length > 5 + ? `, +${characterData.plugins.length - 5} more` + : ""), inline: false, }); } @@ -977,7 +1001,10 @@ class DiscordService { const embed: DiscordEmbed = { title: `${celebration.emoji} ${celebration.title}`, - description: celebration.message.replace("{amount}", paymentData.amount.toFixed(2)), + description: celebration.message.replace( + "{amount}", + paymentData.amount.toFixed(2), + ), color: celebration.color, fields, timestamp: new Date().toISOString(), @@ -1011,7 +1038,8 @@ class DiscordService { return { emoji: "🎰", title: "MASSIVE PAYMENT RECEIVED!", - message: "🚀 **HUGE WIN!** A payment of **$${amount}** just landed! 🎊🎉🎈", + message: + "🚀 **HUGE WIN!** A payment of **$${amount}** just landed! 🎊🎉🎈", announcement: "💎💎💎 **WHALE ALERT** 💎💎💎", color: 0xffd700, // Gold }; @@ -1020,7 +1048,8 @@ class DiscordService { return { emoji: "🎉", title: "Big Payment Received!", - message: "🔥 A solid **$${amount}** payment just came through! Nice! 🔥", + message: + "🔥 A solid **$${amount}** payment just came through! Nice! 🔥", announcement: "🎊 **Cha-ching!** Big money incoming!", color: 0x00ff88, // Bright green }; diff --git a/packages/lib/services/docker-node-manager.ts b/packages/lib/services/docker-node-manager.ts index 54aed4fa5..472482567 100644 --- a/packages/lib/services/docker-node-manager.ts +++ b/packages/lib/services/docker-node-manager.ts @@ -11,7 +11,10 @@ import { and, eq, notInArray, sql } from "drizzle-orm"; import { dbRead } from "@/db/helpers"; import { dockerNodesRepository } from "@/db/repositories/docker-nodes"; import type { DockerNode, DockerNodeStatus } from "@/db/schemas/docker-nodes"; -import { type MiladySandboxStatus, miladySandboxes } from "@/db/schemas/milady-sandboxes"; +import { + type MiladySandboxStatus, + miladySandboxes, +} from "@/db/schemas/milady-sandboxes"; import { DockerSSHClient } from "@/lib/services/docker-ssh"; import { logger } from "@/lib/utils/logger"; @@ -117,7 +120,10 @@ export class DockerNodeManager { node.ssh_user ?? undefined, ); await ssh.connect(); - const dockerId = await ssh.exec("docker info --format '{{.ID}}'", 10_000); + const dockerId = await ssh.exec( + "docker info --format '{{.ID}}'", + 10_000, + ); if (dockerId.trim()) { await dockerNodesRepository.updateStatus(node.node_id, "healthy"); @@ -140,7 +146,9 @@ export class DockerNodeManager { logger.warn( `[docker-node-manager] Health check failed for ${node.node_id} after ${MAX_RETRIES} attempts: ${lastError}`, ); - const status: DockerNodeStatus = lastError.includes("empty ID") ? "degraded" : "offline"; + const status: DockerNodeStatus = lastError.includes("empty ID") + ? "degraded" + : "offline"; await dockerNodesRepository.updateStatus(node.node_id, status); return status; } @@ -158,7 +166,9 @@ export class DockerNodeManager { hostname: node.hostname, capacity: node.capacity, allocated: node.allocated_count, - available: node.enabled ? Math.max(0, node.capacity - node.allocated_count) : 0, + available: node.enabled + ? Math.max(0, node.capacity - node.allocated_count) + : 0, status: node.status, enabled: node.enabled, lastHealthCheck: node.last_health_check, @@ -182,7 +192,9 @@ export class DockerNodeManager { * * Active sandboxes are those not in terminal states (stopped/error). */ - async syncAllocatedCounts(): Promise> { + async syncAllocatedCounts(): Promise< + Map + > { const nodes = await dockerNodesRepository.findEnabled(); const changes = new Map(); @@ -206,7 +218,10 @@ export class DockerNodeManager { logger.info( `[docker-node-manager] Sync ${node.node_id}: allocated_count ${node.allocated_count} → ${actualCount}`, ); - await dockerNodesRepository.setAllocatedCount(node.node_id, actualCount); + await dockerNodesRepository.setAllocatedCount( + node.node_id, + actualCount, + ); changes.set(node.node_id, { before: node.allocated_count, after: actualCount, @@ -215,7 +230,9 @@ export class DockerNodeManager { } if (changes.size > 0) { - logger.info(`[docker-node-manager] Synced allocated counts for ${changes.size} node(s)`); + logger.info( + `[docker-node-manager] Synced allocated counts for ${changes.size} node(s)`, + ); } return changes; @@ -229,7 +246,9 @@ export class DockerNodeManager { */ async getRuntimeContainers( node: DockerNode, - ): Promise<{ name: string; id: string; state: string; status: string }[] | null> { + ): Promise< + { name: string; id: string; state: string; status: string }[] | null + > { try { const ssh = DockerSSHClient.getClient( node.hostname, @@ -254,7 +273,9 @@ export class DockerNodeManager { }); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); - logger.warn(`[docker-node-manager] Failed to list containers on ${node.node_id}: ${msg}`); + logger.warn( + `[docker-node-manager] Failed to list containers on ${node.node_id}: ${msg}`, + ); return null; } } diff --git a/packages/lib/services/docker-sandbox-provider.ts b/packages/lib/services/docker-sandbox-provider.ts index 879b1e0c4..045131ee6 100644 --- a/packages/lib/services/docker-sandbox-provider.ts +++ b/packages/lib/services/docker-sandbox-provider.ts @@ -33,7 +33,11 @@ import { WEBUI_PORT_MIN, } from "./docker-sandbox-utils"; import { headscaleIntegration } from "./headscale-integration"; -import type { SandboxCreateConfig, SandboxHandle, SandboxProvider } from "./sandbox-provider"; +import type { + SandboxCreateConfig, + SandboxHandle, + SandboxProvider, +} from "./sandbox-provider"; // --------------------------------------------------------------------------- // Exported metadata type for strongly-typed provider metadata @@ -73,7 +77,8 @@ interface ContainerMeta { // Helpers // --------------------------------------------------------------------------- -const DOCKER_IMAGE = process.env.MILADY_DOCKER_IMAGE || "ghcr.io/milady-ai/agent:v2.0.0-steward-5"; +const DOCKER_IMAGE = + process.env.MILADY_DOCKER_IMAGE || "ghcr.io/milady-ai/agent:v2.0.0-steward-5"; const DOCKER_NETWORK = process.env.MILADY_DOCKER_NETWORK || "milady-isolated"; // URL for host-side Steward API calls (registration, token minting). // The orchestrator (or SSH-executed scripts on the Docker host) can reach Steward via localhost. @@ -144,7 +149,9 @@ async function getUsedPorts(nodeId: string): Promise> { function getDockerHealthCmd(port: string): string { if (!/^\d+$/.test(port)) { - throw new Error(`[docker-sandbox] Invalid port "${port}": must be a numeric string.`); + throw new Error( + `[docker-sandbox] Invalid port "${port}": must be a numeric string.`, + ); } // /api/health returns 200 or 401 (auth required) — both mean the server is up. // Use curl with -o /dev/null and check status code to accept either. @@ -154,7 +161,9 @@ function getDockerHealthCmd(port: string): string { function extractStewardToken(raw: string): string { const trimmed = raw.trim(); if (!trimmed) { - throw new Error("[docker-sandbox] Steward token endpoint returned an empty response"); + throw new Error( + "[docker-sandbox] Steward token endpoint returned an empty response", + ); } try { @@ -336,7 +345,10 @@ export class DockerSandboxProvider implements SandboxProvider { } // Unreachable, but satisfies the compiler - throw lastError ?? new Error("[docker-sandbox] create exhausted all retry attempts"); + throw ( + lastError ?? + new Error("[docker-sandbox] create exhausted all retry attempts") + ); } /** @@ -347,14 +359,18 @@ export class DockerSandboxProvider implements SandboxProvider { * sandboxes, so a duplicate will fail at INSERT time. The public `create()` * method wraps this in a retry loop to handle port collisions automatically. */ - private async _createOnce(config: SandboxCreateConfig): Promise { + private async _createOnce( + config: SandboxCreateConfig, + ): Promise { const { agentId, agentName, environmentVars } = config; // Resolve Docker image: explicit config > env var > hardcoded default // DOCKER_IMAGE (from env) takes precedence over per-agent DB override. // This prevents stale images from being sticky after the env is updated. const resolvedImage = - DOCKER_IMAGE || config.dockerImage || "ghcr.io/milady-ai/agent:v2.0.0-steward-5"; + DOCKER_IMAGE || + config.dockerImage || + "ghcr.io/milady-ai/agent:v2.0.0-steward-5"; // 1. Input validation validateAgentName(agentName); @@ -406,7 +422,11 @@ export class DockerSandboxProvider implements SandboxProvider { // 3. Allocate ports (check DB for existing assignments to avoid collisions) const usedPorts = await getUsedPorts(nodeId); - const bridgePort = allocatePort(BRIDGE_PORT_MIN, BRIDGE_PORT_MAX, usedPorts); + const bridgePort = allocatePort( + BRIDGE_PORT_MIN, + BRIDGE_PORT_MAX, + usedPorts, + ); // No need to add bridgePort to exclusion set — web UI port range [20000,25000) // never overlaps bridge range [18790,19790) const webUiPort = allocatePort(WEBUI_PORT_MIN, WEBUI_PORT_MAX, usedPorts); @@ -421,7 +441,8 @@ export class DockerSandboxProvider implements SandboxProvider { let vpnEnvVars: Record = {}; if (headscaleEnabled) { try { - const vpnSetup = await headscaleIntegration.prepareContainerVPN(agentId); + const vpnSetup = + await headscaleIntegration.prepareContainerVPN(agentId); vpnEnvVars = vpnSetup.envVars; logger.info(`[docker-sandbox] Headscale VPN enabled for ${agentId}`); } catch (err) { @@ -457,16 +478,29 @@ export class DockerSandboxProvider implements SandboxProvider { // 6. SSH to node, ensure volume dir, pull image, register in Steward, // then create/start the container. Pass hostKeyFingerprint so pooled // clients pin the key when available. - const ssh = DockerSSHClient.getClient(hostname, sshPort, hostKeyFingerprint, sshUser); + const ssh = DockerSSHClient.getClient( + hostname, + sshPort, + hostKeyFingerprint, + sshUser, + ); try { // Ensure volume directory exists - await ssh.exec(`mkdir -p ${shellQuote(volumePath)}`, DOCKER_CMD_TIMEOUT_MS); + await ssh.exec( + `mkdir -p ${shellQuote(volumePath)}`, + DOCKER_CMD_TIMEOUT_MS, + ); // Pull image (may take a while on first run) - logger.info(`[docker-sandbox] Pulling image ${resolvedImage} on ${nodeId}`); + logger.info( + `[docker-sandbox] Pulling image ${resolvedImage} on ${nodeId}`, + ); try { - await ssh.exec(`docker pull ${shellQuote(resolvedImage)}`, PULL_TIMEOUT_MS); + await ssh.exec( + `docker pull ${shellQuote(resolvedImage)}`, + PULL_TIMEOUT_MS, + ); logger.info(`[docker-sandbox] Image pulled successfully on ${nodeId}`); } catch (pullErr) { logger.warn( @@ -474,8 +508,14 @@ export class DockerSandboxProvider implements SandboxProvider { ); } - logger.info(`[docker-sandbox] Registering ${agentId} with Steward on ${nodeId}`); - const stewardAgentToken = await registerAgentWithSteward(ssh, agentId, agentName); + logger.info( + `[docker-sandbox] Registering ${agentId} with Steward on ${nodeId}`, + ); + const stewardAgentToken = await registerAgentWithSteward( + ssh, + agentId, + agentName, + ); const allEnv: Record = { ...baseEnv, @@ -522,7 +562,9 @@ export class DockerSandboxProvider implements SandboxProvider { "--health-timeout 5s", "--health-start-period 15s", "--health-retries 6", - ...(headscaleEnabled ? ["--cap-add=NET_ADMIN", "--device /dev/net/tun"] : []), + ...(headscaleEnabled + ? ["--cap-add=NET_ADMIN", "--device /dev/net/tun"] + : []), `-v ${shellQuote(volumePath)}:/app/data`, // The cloud image serves both API and web UI from PORT (default 2139). // Publish both externally allocated host ports to that live listener so @@ -536,7 +578,10 @@ export class DockerSandboxProvider implements SandboxProvider { const containerId = extractDockerCreateContainerId( await ssh.exec(dockerCreateCmd, DOCKER_CMD_TIMEOUT_MS), ); - await ssh.exec(`docker start ${shellQuote(containerName)}`, DOCKER_CMD_TIMEOUT_MS); + await ssh.exec( + `docker start ${shellQuote(containerName)}`, + DOCKER_CMD_TIMEOUT_MS, + ); logger.info( `[docker-sandbox] Container created on ${nodeId}: ${containerId} (${containerName})`, ); @@ -548,7 +593,9 @@ export class DockerSandboxProvider implements SandboxProvider { `curl -s -X DELETE -H ${shellQuote(`X-Steward-Tenant: ${STEWARD_TENANT_ID}`)} ${STEWARD_TENANT_API_KEY ? `-H ${shellQuote(`X-Steward-Key: ${STEWARD_TENANT_API_KEY}`)}` : ""} ${shellQuote(`${STEWARD_HOST_URL}/agents/${agentId}`)} || true`, DOCKER_CMD_TIMEOUT_MS, ); - logger.info(`[docker-sandbox] Cleaned up Steward agent ${agentId} after container failure`); + logger.info( + `[docker-sandbox] Cleaned up Steward agent ${agentId} after container failure`, + ); } catch (cleanupErr) { logger.warn( `[docker-sandbox] Failed to cleanup Steward agent ${agentId}: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`, @@ -556,7 +603,10 @@ export class DockerSandboxProvider implements SandboxProvider { } await ssh - .exec(`docker rm -f ${shellQuote(containerName)}`, DOCKER_CMD_TIMEOUT_MS) + .exec( + `docker rm -f ${shellQuote(containerName)}`, + DOCKER_CMD_TIMEOUT_MS, + ) .catch(() => {}); // Rollback allocated_count on failure @@ -565,11 +615,13 @@ export class DockerSandboxProvider implements SandboxProvider { } // Clean up Headscale pre-auth key if VPN was prepared if (headscaleEnabled) { - await headscaleIntegration.cleanupContainerVPN(agentId).catch((cleanupErr) => { - logger.warn( - `[docker-sandbox] Headscale cleanup failed during rollback for ${agentId}: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`, - ); - }); + await headscaleIntegration + .cleanupContainerVPN(agentId) + .catch((cleanupErr) => { + logger.warn( + `[docker-sandbox] Headscale cleanup failed during rollback for ${agentId}: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`, + ); + }); } throw new Error( `[docker-sandbox] Failed to create container on ${nodeId}: ${err instanceof Error ? err.message : String(err)}`, @@ -579,7 +631,10 @@ export class DockerSandboxProvider implements SandboxProvider { // 8. Wait for Headscale VPN registration if enabled if (headscaleEnabled) { try { - headscaleIp = await headscaleIntegration.waitForVPNRegistration(agentId, 60_000); + headscaleIp = await headscaleIntegration.waitForVPNRegistration( + agentId, + 60_000, + ); if (headscaleIp) { logger.info( `[docker-sandbox] Container ${containerName} registered on VPN: ${headscaleIp}`, @@ -654,7 +709,10 @@ export class DockerSandboxProvider implements SandboxProvider { try { // Graceful stop with 10s timeout, then force-remove - await ssh.exec(`docker stop -t 10 ${shellQuote(meta.containerName)}`, DOCKER_CMD_TIMEOUT_MS); + await ssh.exec( + `docker stop -t 10 ${shellQuote(meta.containerName)}`, + DOCKER_CMD_TIMEOUT_MS, + ); logger.info(`[docker-sandbox] Container stopped: ${meta.containerName}`); } catch (stopErr) { logger.warn( @@ -663,7 +721,10 @@ export class DockerSandboxProvider implements SandboxProvider { } try { - await ssh.exec(`docker rm -f ${shellQuote(meta.containerName)}`, DOCKER_CMD_TIMEOUT_MS); + await ssh.exec( + `docker rm -f ${shellQuote(meta.containerName)}`, + DOCKER_CMD_TIMEOUT_MS, + ); logger.info(`[docker-sandbox] Container removed: ${meta.containerName}`); } catch (rmErr) { logger.error( @@ -680,11 +741,13 @@ export class DockerSandboxProvider implements SandboxProvider { // Clean up Headscale VPN registration if enabled if (process.env.HEADSCALE_API_KEY && meta.agentId) { - await headscaleIntegration.cleanupContainerVPN(meta.agentId).catch((err) => { - logger.warn( - `[docker-sandbox] Headscale cleanup failed for ${meta.agentId}: ${err instanceof Error ? err.message : String(err)}`, - ); - }); + await headscaleIntegration + .cleanupContainerVPN(meta.agentId) + .catch((err) => { + logger.warn( + `[docker-sandbox] Headscale cleanup failed for ${meta.agentId}: ${err instanceof Error ? err.message : String(err)}`, + ); + }); } // Remove from in-memory registry @@ -735,10 +798,14 @@ export class DockerSandboxProvider implements SandboxProvider { // Wait before retrying (but don't overshoot the deadline) const remaining = deadline - Date.now(); if (remaining > HEALTH_CHECK_POLL_INTERVAL_MS) { - await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_POLL_INTERVAL_MS)); + await new Promise((resolve) => + setTimeout(resolve, HEALTH_CHECK_POLL_INTERVAL_MS), + ); } else if (remaining > 0) { // One last attempt after a short wait - await new Promise((resolve) => setTimeout(resolve, Math.min(remaining, 1000))); + await new Promise((resolve) => + setTimeout(resolve, Math.min(remaining, 1000)), + ); } else { break; } @@ -754,12 +821,19 @@ export class DockerSandboxProvider implements SandboxProvider { // runCommand // ------------------------------------------------------------------ - async runCommand(sandboxId: string, cmd: string, args?: string[]): Promise { + async runCommand( + sandboxId: string, + cmd: string, + args?: string[], + ): Promise { const meta = await this.resolveContainer(sandboxId); // Shell-escape each argument to prevent command injection - const escapedArgs = args && args.length > 0 ? args.map((a) => shellQuote(a)).join(" ") : ""; - const fullCmd = escapedArgs ? `${shellQuote(cmd)} ${escapedArgs}` : shellQuote(cmd); + const escapedArgs = + args && args.length > 0 ? args.map((a) => shellQuote(a)).join(" ") : ""; + const fullCmd = escapedArgs + ? `${shellQuote(cmd)} ${escapedArgs}` + : shellQuote(cmd); logger.info( `[docker-sandbox] Executing command in ${meta.containerName}: ${cmd} ${(args ?? []).join(" ").slice(0, 80)}`, @@ -798,7 +872,8 @@ export class DockerSandboxProvider implements SandboxProvider { // DB lookup: hydrate from persisted metadata after restart try { - const sandbox = await miladySandboxesRepository.findBySandboxId(sandboxId); + const sandbox = + await miladySandboxesRepository.findBySandboxId(sandboxId); if (sandbox && sandbox.node_id && sandbox.container_name) { // Find hostname + SSH config from DB node record or env var let hostname = ""; @@ -806,7 +881,9 @@ export class DockerSandboxProvider implements SandboxProvider { let sshUser = DEFAULT_SSH_USERNAME; let hostKeyFingerprint: string | undefined; - const dbNode = await dockerNodesRepository.findByNodeId(sandbox.node_id); + const dbNode = await dockerNodesRepository.findByNodeId( + sandbox.node_id, + ); if (dbNode) { hostname = dbNode.hostname; sshPort = dbNode.ssh_port ?? DEFAULT_SSH_PORT; diff --git a/packages/lib/services/docker-sandbox-utils.ts b/packages/lib/services/docker-sandbox-utils.ts index ed3758397..1fea9fb75 100644 --- a/packages/lib/services/docker-sandbox-utils.ts +++ b/packages/lib/services/docker-sandbox-utils.ts @@ -76,7 +76,9 @@ export function validateAgentName(name: string): void { } // Block characters that could break shell commands even inside quotes if (hasControlChars(name)) { - throw new Error(`Invalid agent name "${name}": contains control characters.`); + throw new Error( + `Invalid agent name "${name}": contains control characters.`, + ); } } @@ -104,7 +106,10 @@ export function validateEnvValue(key: string, value: string): void { /** Docker container names must be simple shell-safe identifiers. */ export function validateContainerName(containerName: string): void { - if (hasControlChars(containerName) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(containerName)) { + if ( + hasControlChars(containerName) || + !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(containerName) + ) { throw new Error( `Invalid container name "${containerName}": must match ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$.`, ); @@ -131,7 +136,9 @@ export function validateVolumePath(volumePath: string): void { volumePath.endsWith("/..") || (volumePath.length > 1 && volumePath.endsWith("/")) ) { - throw new Error(`Invalid volume path "${volumePath}": path must be normalized.`); + throw new Error( + `Invalid volume path "${volumePath}": path must be normalized.`, + ); } } @@ -148,7 +155,8 @@ export function validateVolumePath(volumePath: string): void { * - Non-loopback host URLs pass through unchanged. */ export function resolveStewardContainerUrl( - stewardHostUrl: string = process.env.STEWARD_API_URL || "http://localhost:3200", + stewardHostUrl: string = process.env.STEWARD_API_URL || + "http://localhost:3200", stewardContainerUrl?: string, ): string { const override = stewardContainerUrl?.trim(); @@ -210,7 +218,11 @@ export function extractDockerCreateContainerId(output: string): string { * for active sandboxes, so a duplicate insert will fail and the caller * should retry the entire provisioning flow. */ -export function allocatePort(min: number, max: number, excluded: Set): number { +export function allocatePort( + min: number, + max: number, + excluded: Set, +): number { const range = max - min; if (excluded.size >= range) { throw new Error( @@ -290,7 +302,9 @@ export function parseDockerNodes(): DockerNodeEnv[] { const parts = trimmed.split(":"); if (parts.length < 3) { - logger.warn(`[docker-sandbox] Skipping malformed node entry: "${trimmed}"`); + logger.warn( + `[docker-sandbox] Skipping malformed node entry: "${trimmed}"`, + ); continue; } @@ -305,7 +319,9 @@ export function parseDockerNodes(): DockerNodeEnv[] { } if (nodes.length === 0) { - throw new Error("[docker-sandbox] No valid nodes parsed from MILADY_DOCKER_NODES"); + throw new Error( + "[docker-sandbox] No valid nodes parsed from MILADY_DOCKER_NODES", + ); } _cachedDockerNodes = nodes; diff --git a/packages/lib/services/docker-ssh.ts b/packages/lib/services/docker-ssh.ts index c892022f3..39c190199 100644 --- a/packages/lib/services/docker-ssh.ts +++ b/packages/lib/services/docker-ssh.ts @@ -45,7 +45,8 @@ const DEFAULT_SSH_USERNAME = process.env.MILADY_SSH_USER || "root"; * resolvePrivateKey() so that tests can manipulate process.env between calls. */ const DEFAULT_SSH_KEY_PATH = - process.env.MILADY_SSH_KEY_PATH || path.join(os.homedir(), ".ssh", "id_ed25519"); + process.env.MILADY_SSH_KEY_PATH || + path.join(os.homedir(), ".ssh", "id_ed25519"); /** TCP / handshake timeout for new connections (ms). */ const CONNECTION_TIMEOUT_MS = 10_000; @@ -126,8 +127,14 @@ export class DockerSSHClient { client = undefined; } // Evict if fingerprint requirements changed - if (client && hostKeyFingerprint && client.pinnedFingerprint !== hostKeyFingerprint) { - logger.info(`[docker-ssh] Evicting pooled connection for ${poolKey} — fingerprint changed`); + if ( + client && + hostKeyFingerprint && + client.pinnedFingerprint !== hostKeyFingerprint + ) { + logger.info( + `[docker-ssh] Evicting pooled connection for ${poolKey} — fingerprint changed`, + ); client.disconnect().catch(() => {}); DockerSSHClient.pool.delete(poolKey); client = undefined; @@ -194,7 +201,9 @@ export class DockerSSHClient { this.privateKeyPath = config.privateKeyPath ?? DEFAULT_SSH_KEY_PATH; this.hostKeyFingerprint = config.hostKeyFingerprint; - this.privateKey = config.privateKey ?? DockerSSHClient.resolvePrivateKey(this.privateKeyPath); + this.privateKey = + config.privateKey ?? + DockerSSHClient.resolvePrivateKey(this.privateKeyPath); } // ---- Private key resolution ------------------------------------------ @@ -238,7 +247,9 @@ export class DockerSSHClient { // so we replace it with the redacted form. const safePath = keyPath.split("/").pop() ?? "unknown"; const innerMsg = err instanceof Error ? err.message : String(err); - const safeInnerMsg = keyPath ? innerMsg.replaceAll(keyPath, `.../${safePath}`) : innerMsg; + const safeInnerMsg = keyPath + ? innerMsg.replaceAll(keyPath, `.../${safePath}`) + : innerMsg; throw new Error( `[docker-ssh] Failed to load SSH key (file: .../${safePath}). ` + `Set MILADY_SSH_KEY env var (base64) for serverless deployments. ` + @@ -282,7 +293,11 @@ export class DockerSSHClient { clearTimeout(timeout); this.connected = false; this.client = null; - reject(new Error(`[docker-ssh] Connection error for ${this.hostname}: ${err.message}`)); + reject( + new Error( + `[docker-ssh] Connection error for ${this.hostname}: ${err.message}`, + ), + ); }); conn.on("close", () => { @@ -297,7 +312,10 @@ export class DockerSSHClient { privateKey: this.privateKey, readyTimeout: CONNECTION_TIMEOUT_MS, hostVerifier: (key: Buffer) => { - const fingerprint = crypto.createHash("sha256").update(key).digest("base64"); + const fingerprint = crypto + .createHash("sha256") + .update(key) + .digest("base64"); if (!this.hostKeyFingerprint) { // No fingerprint configured — TOFU: accept and log at warn for operator visibility logger.warn( @@ -371,7 +389,11 @@ export class DockerSSHClient { clearTimeout(timer); if (!settled) { settled = true; - reject(new Error(`[docker-ssh] exec error on ${this.hostname}: ${err.message}`)); + reject( + new Error( + `[docker-ssh] exec error on ${this.hostname}: ${err.message}`, + ), + ); } return; } @@ -382,7 +404,10 @@ export class DockerSSHClient { stream.stderr.on("data", (data: Buffer) => { const text = data.toString(); - output += output && !output.endsWith("\n") ? `\n[stderr] ${text}` : `[stderr] ${text}`; + output += + output && !output.endsWith("\n") + ? `\n[stderr] ${text}` + : `[stderr] ${text}`; }); stream.on("close", (code: number) => { @@ -406,7 +431,9 @@ export class DockerSSHClient { if (!settled) { settled = true; reject( - new Error(`[docker-ssh] stream error on ${this.hostname}: ${streamErr.message}`), + new Error( + `[docker-ssh] stream error on ${this.hostname}: ${streamErr.message}`, + ), ); } }); diff --git a/packages/lib/services/ecr.ts b/packages/lib/services/ecr.ts index a6ccf3160..8576a6cb2 100644 --- a/packages/lib/services/ecr.ts +++ b/packages/lib/services/ecr.ts @@ -152,7 +152,10 @@ export class ECRManager { (createError.name === "RepositoryAlreadyExistsException" || createError.message.includes("already exists"))) ) { - logger.info("Repository was created by another process, fetching:", repositoryName); + logger.info( + "Repository was created by another process, fetching:", + repositoryName, + ); const existingRepo = await this.getRepository(repositoryName); if (existingRepo) { return { @@ -165,7 +168,10 @@ export class ECRManager { logger.error("Failed to create ECR repository:", { errorName: createError instanceof Error ? createError.name : "Unknown", - errorMessage: createError instanceof Error ? createError.message : String(createError), + errorMessage: + createError instanceof Error + ? createError.message + : String(createError), repositoryName, }); throw createError; @@ -226,7 +232,9 @@ export class ECRManager { }); await this.client.send(command); - logger.info(`✅ ECR lifecycle policy set for repository: ${repositoryName}`); + logger.info( + `✅ ECR lifecycle policy set for repository: ${repositoryName}`, + ); } /** @@ -271,7 +279,10 @@ export class ECRManager { /** * Delete images from a repository */ - async deleteImages(repositoryName: string, imageIds: ImageIdentifier[]): Promise { + async deleteImages( + repositoryName: string, + imageIds: ImageIdentifier[], + ): Promise { if (imageIds.length === 0) { return; } diff --git a/packages/lib/services/elevenlabs.ts b/packages/lib/services/elevenlabs.ts index a8ff12537..2228b97e5 100644 --- a/packages/lib/services/elevenlabs.ts +++ b/packages/lib/services/elevenlabs.ts @@ -71,12 +71,15 @@ export class ElevenLabsService { apiKey, voiceId: process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL", modelId: process.env.ELEVENLABS_MODEL_ID || "eleven_flash_v2_5", - voiceStability: Number.parseFloat(process.env.ELEVENLABS_VOICE_STABILITY || "0.5"), + voiceStability: Number.parseFloat( + process.env.ELEVENLABS_VOICE_STABILITY || "0.5", + ), voiceSimilarityBoost: Number.parseFloat( process.env.ELEVENLABS_VOICE_SIMILARITY_BOOST || "0.75", ), voiceStyle: Number.parseFloat(process.env.ELEVENLABS_VOICE_STYLE || "0"), - voiceUseSpeakerBoost: process.env.ELEVENLABS_VOICE_USE_SPEAKER_BOOST !== "false", + voiceUseSpeakerBoost: + process.env.ELEVENLABS_VOICE_USE_SPEAKER_BOOST !== "false", optimizeStreamingLatency: Number.parseInt( process.env.ELEVENLABS_OPTIMIZE_STREAMING_LATENCY || "4", ), @@ -90,8 +93,10 @@ export class ElevenLabsService { * Convert text to speech (streaming) */ async textToSpeech(options: TTSOptions): Promise> { - const voiceId = options.voiceId || this.config.voiceId || "EXAVITQu4vr4xnSDxMaL"; - const modelId = options.modelId || this.config.modelId || "eleven_flash_v2_5"; + const voiceId = + options.voiceId || this.config.voiceId || "EXAVITQu4vr4xnSDxMaL"; + const modelId = + options.modelId || this.config.modelId || "eleven_flash_v2_5"; logger.info( `[ElevenLabs TTS] Generating speech: voice=${voiceId}, model=${modelId}, length=${options.text.length}`, @@ -204,7 +209,9 @@ export class ElevenLabsService { files: File[]; language?: string; }): Promise<{ voiceId: string; name: string }> { - logger.info(`[ElevenLabs] Creating professional voice clone: ${options.name}`); + logger.info( + `[ElevenLabs] Creating professional voice clone: ${options.name}`, + ); // Use PVC (Professional Voice Cloning) endpoint // Language parameter is required by ElevenLabs API (SDK types are outdated) diff --git a/packages/lib/services/eliza-app/config.ts b/packages/lib/services/eliza-app/config.ts index f9d94d2c5..8c6e4039e 100644 --- a/packages/lib/services/eliza-app/config.ts +++ b/packages/lib/services/eliza-app/config.ts @@ -37,15 +37,19 @@ function optionalRuntimeEnv(name: string, fallback = ""): string { return process.env[name] || (!isProduction ? fallback : ""); } -const ELIZA_APP_SMALL_MODEL = process.env.ELIZA_APP_SMALL_MODEL || "minimax/minimax-m2.7"; -const ELIZA_APP_LARGE_MODEL = process.env.ELIZA_APP_LARGE_MODEL || "anthropic/claude-sonnet-4.6"; +const ELIZA_APP_SMALL_MODEL = + process.env.ELIZA_APP_SMALL_MODEL || "minimax/minimax-m2.7"; +const ELIZA_APP_LARGE_MODEL = + process.env.ELIZA_APP_LARGE_MODEL || "anthropic/claude-sonnet-4.6"; export const elizaAppConfig = { // Frontend URL (the consumer-facing app, e.g. eliza.app) appUrl: process.env.ELIZA_APP_URL || "https://eliza.app", // Agent configuration - defaultAgentId: process.env.ELIZA_APP_DEFAULT_AGENT_ID || "b850bc30-45f8-0041-a00a-83df46d8555d", + defaultAgentId: + process.env.ELIZA_APP_DEFAULT_AGENT_ID || + "b850bc30-45f8-0041-a00a-83df46d8555d", // Model preferences for webhook channels (Telegram, iMessage) modelPreferences: { @@ -72,9 +76,11 @@ export const elizaAppConfig = { process.env.ELIZA_APP_ACTION_PLANNER_MODEL || process.env.ELIZA_APP_MEDIUM_MODEL || ELIZA_APP_SMALL_MODEL, - responseModel: process.env.ELIZA_APP_RESPONSE_MODEL || ELIZA_APP_LARGE_MODEL, + responseModel: + process.env.ELIZA_APP_RESPONSE_MODEL || ELIZA_APP_LARGE_MODEL, mediaDescriptionModel: - process.env.ELIZA_APP_MEDIA_DESCRIPTION_MODEL || "google/gemini-2.5-flash-lite", + process.env.ELIZA_APP_MEDIA_DESCRIPTION_MODEL || + "google/gemini-2.5-flash-lite", }, // Prompt preset for eliza-app channels (engaging, conversation-continuing behavior) @@ -93,7 +99,10 @@ export const elizaAppConfig = { return { apiKey: optionalRuntimeEnv("ELIZA_APP_BLOOIO_API_KEY"), webhookSecret: process.env.ELIZA_APP_BLOOIO_WEBHOOK_SECRET || "", - phoneNumber: optionalRuntimeEnv("ELIZA_APP_BLOOIO_PHONE_NUMBER", "+14245074963"), + phoneNumber: optionalRuntimeEnv( + "ELIZA_APP_BLOOIO_PHONE_NUMBER", + "+14245074963", + ), }; }, @@ -129,7 +138,9 @@ export const elizaAppConfig = { export function validateElizaAppConfig() { // JWT is required for the core app to function if (!process.env.ELIZA_APP_JWT_SECRET) { - throw new Error("Required env var ELIZA_APP_JWT_SECRET is not set in production"); + throw new Error( + "Required env var ELIZA_APP_JWT_SECRET is not set in production", + ); } // Validate channel-specific required vars if they're enabled @@ -141,8 +152,13 @@ export function validateElizaAppConfig() { "Telegram is enabled but ELIZA_APP_TELEGRAM_BOT_TOKEN is not set in production", ); } - if (process.env.ELIZA_APP_BLOOIO_ENABLED === "true" && !process.env.ELIZA_APP_BLOOIO_API_KEY) { - throw new Error("Blooio is enabled but ELIZA_APP_BLOOIO_API_KEY is not set in production"); + if ( + process.env.ELIZA_APP_BLOOIO_ENABLED === "true" && + !process.env.ELIZA_APP_BLOOIO_API_KEY + ) { + throw new Error( + "Blooio is enabled but ELIZA_APP_BLOOIO_API_KEY is not set in production", + ); } if ( process.env.ELIZA_APP_DISCORD_ENABLED === "true" && @@ -150,7 +166,9 @@ export function validateElizaAppConfig() { !process.env.ELIZA_APP_DISCORD_APPLICATION_ID || !process.env.ELIZA_APP_DISCORD_CLIENT_SECRET) ) { - throw new Error("Discord is enabled but required Discord env vars are not set in production"); + throw new Error( + "Discord is enabled but required Discord env vars are not set in production", + ); } const whatsappEnabled = @@ -171,6 +189,8 @@ export function validateElizaAppConfig() { !process.env.ELIZA_APP_WHATSAPP_VERIFY_TOKEN || !process.env.ELIZA_APP_WHATSAPP_PHONE_NUMBER) ) { - throw new Error("WhatsApp is enabled but required WhatsApp env vars are not set in production"); + throw new Error( + "WhatsApp is enabled but required WhatsApp env vars are not set in production", + ); } } diff --git a/packages/lib/services/eliza-app/connection-enforcement.ts b/packages/lib/services/eliza-app/connection-enforcement.ts index 434064f9c..090129ffd 100644 --- a/packages/lib/services/eliza-app/connection-enforcement.ts +++ b/packages/lib/services/eliza-app/connection-enforcement.ts @@ -53,7 +53,13 @@ const DEFAULT_ELIZA_CHARACTER: ElizaCharacter = { "genuinely interested in the texture of someone's day", "quietly direct when something matters", ], - adjectives: ["perceptive", "present", "warm but restrained", "curious", "ungeneric"], + adjectives: [ + "perceptive", + "present", + "warm but restrained", + "curious", + "ungeneric", + ], style: { all: [ "say less. mean more.", @@ -126,7 +132,10 @@ function sample(items: T[], count: number): T[] { const shuffled = [...items]; for (let index = shuffled.length - 1; index > 0; index -= 1) { const nextIndex = Math.floor(Math.random() * (index + 1)); - [shuffled[index], shuffled[nextIndex]] = [shuffled[nextIndex], shuffled[index]]; + [shuffled[index], shuffled[nextIndex]] = [ + shuffled[nextIndex], + shuffled[index], + ]; } return shuffled.slice(0, Math.min(count, shuffled.length)); } @@ -135,7 +144,10 @@ function formatConversationHistory(messages: ConversationMessage[]): string { if (messages.length === 0) return ""; return `\n\nRecent conversation:\n${messages - .map((message) => `${message.role === "user" ? "User" : "Eliza"}: ${message.content}`) + .map( + (message) => + `${message.role === "user" ? "User" : "Eliza"}: ${message.content}`, + ) .join("\n")}\n`; } @@ -143,7 +155,10 @@ function getConversationKey(organizationId: string, userId: string): string { return `connection-enforcement:conversation:${organizationId}:${userId}`; } -function getConnectionStatusKey(organizationId: string, userId: string): string { +function getConnectionStatusKey( + organizationId: string, + userId: string, +): string { return `connection-enforcement:required-connection:${organizationId}:${userId}`; } @@ -153,7 +168,9 @@ async function loadConversationState( ): Promise { try { return ( - (await cache.get(getConversationKey(organizationId, userId))) ?? { + (await cache.get( + getConversationKey(organizationId, userId), + )) ?? { messageCount: 0, messages: [], } @@ -179,11 +196,14 @@ async function saveConversationState( CONVERSATION_TTL_SECONDS, ); } catch (error) { - logger.warn("[ConnectionEnforcement] Failed to persist conversation state", { - organizationId, - userId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[ConnectionEnforcement] Failed to persist conversation state", + { + organizationId, + userId, + error: error instanceof Error ? error.message : String(error), + }, + ); } } @@ -212,11 +232,16 @@ function formatLinks( ): string { if (messagingPlatform === "telegram") { return links - .map((link) => `[Connect ${PLATFORM_DISPLAY_NAMES[link.platform]}](${link.url})`) + .map( + (link) => + `[Connect ${PLATFORM_DISPLAY_NAMES[link.platform]}](${link.url})`, + ) .join("\n"); } - return links.map((link) => `${PLATFORM_DISPLAY_NAMES[link.platform]}: ${link.url}`).join("\n"); + return links + .map((link) => `${PLATFORM_DISPLAY_NAMES[link.platform]}: ${link.url}`) + .join("\n"); } function buildNudgePrompt( @@ -225,9 +250,14 @@ function buildNudgePrompt( isFirstInteraction: boolean, ): string { const bioSample = sample(DEFAULT_ELIZA_CHARACTER.bio, 4).join(" "); - const adjectiveSample = sample(DEFAULT_ELIZA_CHARACTER.adjectives, 4).join(", "); + const adjectiveSample = sample(DEFAULT_ELIZA_CHARACTER.adjectives, 4).join( + ", ", + ); const styleSample = sample( - [...DEFAULT_ELIZA_CHARACTER.style.all, ...DEFAULT_ELIZA_CHARACTER.style.chat], + [ + ...DEFAULT_ELIZA_CHARACTER.style.all, + ...DEFAULT_ELIZA_CHARACTER.style.chat, + ], 5, ).join("; "); @@ -252,11 +282,19 @@ RESPONSE RULES: 6. If they say they already connected something and you still do not see it, say you still do not see the connection yet and suggest trying again.${conversationHistory}`; } -function buildChatPrompt(platform: MessagingPlatform, conversationHistory: string): string { +function buildChatPrompt( + platform: MessagingPlatform, + conversationHistory: string, +): string { const bioSample = sample(DEFAULT_ELIZA_CHARACTER.bio, 4).join(" "); - const adjectiveSample = sample(DEFAULT_ELIZA_CHARACTER.adjectives, 4).join(", "); + const adjectiveSample = sample(DEFAULT_ELIZA_CHARACTER.adjectives, 4).join( + ", ", + ); const styleSample = sample( - [...DEFAULT_ELIZA_CHARACTER.style.all, ...DEFAULT_ELIZA_CHARACTER.style.chat], + [ + ...DEFAULT_ELIZA_CHARACTER.style.all, + ...DEFAULT_ELIZA_CHARACTER.style.chat, + ], 5, ).join("; "); @@ -290,7 +328,9 @@ function getFallbackResponse(message: string): string { return "fair. i just can't do much without context. if you were going to pick one, which would it be: google, microsoft, or x?"; } - return FALLBACK_RESPONSES[Math.floor(Math.random() * FALLBACK_RESPONSES.length)]; + return FALLBACK_RESPONSES[ + Math.floor(Math.random() * FALLBACK_RESPONSES.length) + ]; } async function generateOAuthLinks( @@ -300,7 +340,9 @@ async function generateOAuthLinks( specificProvider?: RequiredPlatform | null, ): Promise<{ platform: RequiredPlatform; url: string }[]> { const redirectUrl = `${getBaseUrl()}/api/eliza-app/auth/connection-success?platform=${platform}`; - const providers = specificProvider ? [specificProvider] : [...REQUIRED_PLATFORMS]; + const providers = specificProvider + ? [specificProvider] + : [...REQUIRED_PLATFORMS]; const results = await Promise.allSettled( providers.map(async (provider) => { @@ -311,7 +353,9 @@ async function generateOAuthLinks( redirectUrl, }); - return result.authUrl ? { platform: provider, url: result.authUrl } : null; + return result.authUrl + ? { platform: provider, url: result.authUrl } + : null; }), ); @@ -323,7 +367,10 @@ async function generateOAuthLinks( if (result.status === "rejected") { logger.warn("[ConnectionEnforcement] Failed to generate OAuth link", { organizationId, - error: result.reason instanceof Error ? result.reason.message : String(result.reason), + error: + result.reason instanceof Error + ? result.reason.message + : String(result.reason), }); } @@ -344,7 +391,10 @@ function detectProviderFromMessage(message: string): RequiredPlatform | null { } class ConnectionEnforcementService { - async hasRequiredConnection(organizationId: string, userId: string): Promise { + async hasRequiredConnection( + organizationId: string, + userId: string, + ): Promise { try { const cacheKey = getConnectionStatusKey(organizationId, userId); const cached = await cache.get(cacheKey); @@ -352,7 +402,10 @@ class ConnectionEnforcementService { return cached; } - const connectedPlatforms = await oauthService.getConnectedPlatforms(organizationId, userId); + const connectedPlatforms = await oauthService.getConnectedPlatforms( + organizationId, + userId, + ); const hasRequired = connectedPlatforms.some((platform) => (REQUIRED_PLATFORMS as readonly string[]).includes(platform), ); @@ -369,20 +422,28 @@ class ConnectionEnforcementService { } } - async invalidateRequiredConnectionCache(organizationId: string, userId?: string): Promise { + async invalidateRequiredConnectionCache( + organizationId: string, + userId?: string, + ): Promise { try { if (userId) { await cache.del(getConnectionStatusKey(organizationId, userId)); return; } - await cache.delPattern(`connection-enforcement:required-connection:${organizationId}:*`); + await cache.delPattern( + `connection-enforcement:required-connection:${organizationId}:*`, + ); } catch (error) { - logger.warn("[ConnectionEnforcement] Failed to invalidate connection cache", { - organizationId, - userId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[ConnectionEnforcement] Failed to invalidate connection cache", + { + organizationId, + userId, + error: error instanceof Error ? error.message : String(error), + }, + ); } } @@ -408,9 +469,16 @@ class ConnectionEnforcementService { isFirstInteraction, ); responseForHistory = llmResponse; - const links = await generateOAuthLinks(organizationId, userId, platform, detectedProvider); + const links = await generateOAuthLinks( + organizationId, + userId, + platform, + detectedProvider, + ); response = - links.length > 0 ? `${llmResponse}\n\n${formatLinks(links, platform)}` : llmResponse; + links.length > 0 + ? `${llmResponse}\n\n${formatLinks(links, platform)}` + : llmResponse; } else { response = await this.generateLLMResponse( userMessage, diff --git a/packages/lib/services/eliza-app/discord-auth.ts b/packages/lib/services/eliza-app/discord-auth.ts index 3082adb38..9b8c24976 100644 --- a/packages/lib/services/eliza-app/discord-auth.ts +++ b/packages/lib/services/eliza-app/discord-auth.ts @@ -53,11 +53,16 @@ class DiscordAuthService { * @param redirectUri - The redirect_uri used in the original authorization request * @returns Discord user data, or null if verification fails */ - async verifyOAuthCode(code: string, redirectUri: string): Promise { + async verifyOAuthCode( + code: string, + redirectUri: string, + ): Promise { const { applicationId, clientSecret } = elizaAppConfig.discord; if (!applicationId || !clientSecret) { - logger.error("[DiscordAuth] Application ID or client secret not configured"); + logger.error( + "[DiscordAuth] Application ID or client secret not configured", + ); return null; } @@ -89,7 +94,9 @@ class DiscordAuthService { const rawToken = await tokenResponse.json(); if (!rawToken.access_token) { - logger.error("[DiscordAuth] Invalid token response - missing access_token"); + logger.error( + "[DiscordAuth] Invalid token response - missing access_token", + ); return null; } tokenData = rawToken as DiscordTokenResponse; diff --git a/packages/lib/services/eliza-app/session-service.ts b/packages/lib/services/eliza-app/session-service.ts index fd0da67e9..730286ef6 100644 --- a/packages/lib/services/eliza-app/session-service.ts +++ b/packages/lib/services/eliza-app/session-service.ts @@ -109,7 +109,9 @@ class ElizaAppSessionService { } } - async validateAuthHeader(authHeader: string): Promise { + async validateAuthHeader( + authHeader: string, + ): Promise { if (!authHeader.startsWith("Bearer ")) { return null; } diff --git a/packages/lib/services/eliza-app/telegram-auth.ts b/packages/lib/services/eliza-app/telegram-auth.ts index 58e39530e..b304c3834 100644 --- a/packages/lib/services/eliza-app/telegram-auth.ts +++ b/packages/lib/services/eliza-app/telegram-auth.ts @@ -110,7 +110,9 @@ class TelegramAuthService { * Fields are sorted alphabetically and joined with newlines. * The hash field is excluded from the check string. */ - private generateCheckString(data: Omit & { hash?: string }): string { + private generateCheckString( + data: Omit & { hash?: string }, + ): string { const entries: [string, string | number][] = []; // Add all fields except hash, only if they have values diff --git a/packages/lib/services/eliza-app/user-service.ts b/packages/lib/services/eliza-app/user-service.ts index eba622837..66dbe336f 100644 --- a/packages/lib/services/eliza-app/user-service.ts +++ b/packages/lib/services/eliza-app/user-service.ts @@ -11,13 +11,19 @@ */ import { organizationsRepository } from "@/db/repositories/organizations"; -import { type UserWithOrganization, usersRepository } from "@/db/repositories/users"; +import { + type UserWithOrganization, + usersRepository, +} from "@/db/repositories/users"; import type { Organization } from "@/db/schemas/organizations"; import type { NewUser, User } from "@/db/schemas/users"; import { apiKeysService } from "@/lib/services/api-keys"; import { creditsService } from "@/lib/services/credits"; import { redeemSignupCode } from "@/lib/services/signup-code"; -import { isValidEmail, maskEmailForLogging } from "@/lib/utils/email-validation"; +import { + isValidEmail, + maskEmailForLogging, +} from "@/lib/utils/email-validation"; import { logger } from "@/lib/utils/logger"; import { normalizePhoneNumber } from "@/lib/utils/phone-normalization"; import type { TelegramAuthData } from "./telegram-auth"; @@ -42,8 +48,13 @@ export interface FindOrCreateResult { isNew: boolean; } -function generateSlugFromTelegram(username?: string, telegramId?: string): string { - const base = username ? username.toLowerCase().replace(/[^a-z0-9]/g, "-") : `tg-${telegramId}`; +function generateSlugFromTelegram( + username?: string, + telegramId?: string, +): string { + const base = username + ? username.toLowerCase().replace(/[^a-z0-9]/g, "-") + : `tg-${telegramId}`; const random = Math.random().toString(36).substring(2, 8); const timestamp = Date.now().toString(36).slice(-4); return `${base}-${timestamp}${random}`; @@ -67,8 +78,13 @@ function generateSlugFromEmail(email: string): string { return `email-${prefix}-${timestamp}${random}`; } -function generateSlugFromDiscord(username?: string, discordId?: string): string { - const base = username ? username.toLowerCase().replace(/[^a-z0-9]/g, "-") : discordId; +function generateSlugFromDiscord( + username?: string, + discordId?: string, +): string { + const base = username + ? username.toLowerCase().replace(/[^a-z0-9]/g, "-") + : discordId; const random = Math.random().toString(36).substring(2, 8); const timestamp = Date.now().toString(36).slice(-4); return `discord-${base}-${timestamp}${random}`; @@ -81,7 +97,10 @@ function generateSlugFromWhatsApp(whatsappId: string): string { return `wa-${lastFour}-${timestamp}${random}`; } -async function ensureUniqueSlug(generateFn: () => string, maxAttempts = 10): Promise { +async function ensureUniqueSlug( + generateFn: () => string, + maxAttempts = 10, +): Promise { let slug = generateFn(); let attempts = 0; @@ -132,10 +151,13 @@ async function createUserWithOrganization(params: { try { await redeemSignupCode(organization.id, signupCode); } catch (error) { - logger.warn("[ElizaAppUserService] Signup code redemption failed for new org", { - organizationId: organization.id, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[ElizaAppUserService] Signup code redemption failed for new org", + { + organizationId: organization.id, + error: error instanceof Error ? error.message : String(error), + }, + ); } } @@ -176,38 +198,50 @@ class ElizaAppUserService { const normalizedPhone = normalizePhoneNumber(phoneNumber); // Scenario 1: Check if user exists by telegram_id (returning Telegram user) - const existingTelegramUser = await usersRepository.findByTelegramIdWithOrganization(telegramId); + const existingTelegramUser = + await usersRepository.findByTelegramIdWithOrganization(telegramId); if (existingTelegramUser && existingTelegramUser.organization) { // Update Telegram profile data and ensure phone is set const updates: Partial = { - telegram_username: telegramData.username || existingTelegramUser.telegram_username, + telegram_username: + telegramData.username || existingTelegramUser.telegram_username, telegram_first_name: telegramData.first_name, - telegram_photo_url: telegramData.photo_url || existingTelegramUser.telegram_photo_url, + telegram_photo_url: + telegramData.photo_url || existingTelegramUser.telegram_photo_url, updated_at: new Date(), }; // Set phone number if not already set - but first check it's not taken if (!existingTelegramUser.phone_number) { - const phoneOwner = await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + const phoneOwner = + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); if (phoneOwner && phoneOwner.id !== existingTelegramUser.id) { // Phone is owned by a different user - this is a conflict - logger.warn("[ElizaAppUserService] Phone already owned by another user", { - telegramUserId: existingTelegramUser.id, - phoneOwnerId: phoneOwner.id, - phone: `***${normalizedPhone.slice(-4)}`, - }); + logger.warn( + "[ElizaAppUserService] Phone already owned by another user", + { + telegramUserId: existingTelegramUser.id, + phoneOwnerId: phoneOwner.id, + phone: `***${normalizedPhone.slice(-4)}`, + }, + ); throw new Error("PHONE_ALREADY_LINKED"); } updates.phone_number = normalizedPhone; updates.phone_verified = true; } else if (existingTelegramUser.phone_number !== normalizedPhone) { // User already has a different phone linked - reject the mismatch - logger.warn("[ElizaAppUserService] Telegram user has different phone linked", { - telegramId, - existingPhone: `***${existingTelegramUser.phone_number.slice(-4)}`, - requestedPhone: `***${normalizedPhone.slice(-4)}`, - }); + logger.warn( + "[ElizaAppUserService] Telegram user has different phone linked", + { + telegramId, + existingPhone: `***${existingTelegramUser.phone_number.slice(-4)}`, + requestedPhone: `***${normalizedPhone.slice(-4)}`, + }, + ); throw new Error("PHONE_MISMATCH"); } @@ -225,14 +259,18 @@ class ElizaAppUserService { throw error; } - logger.info("[ElizaAppUserService] Found existing Telegram user, updated", { - userId: existingTelegramUser.id, - telegramId, - phoneAdded: !existingTelegramUser.phone_number, - }); + logger.info( + "[ElizaAppUserService] Found existing Telegram user, updated", + { + userId: existingTelegramUser.id, + telegramId, + phoneAdded: !existingTelegramUser.phone_number, + }, + ); // Refetch to get updated data - const updatedUser = await usersRepository.findByTelegramIdWithOrganization(telegramId); + const updatedUser = + await usersRepository.findByTelegramIdWithOrganization(telegramId); return { user: updatedUser!, organization: updatedUser!.organization!, @@ -247,7 +285,10 @@ class ElizaAppUserService { if (existingPhoneUser && existingPhoneUser.organization) { // Re-check telegram_id to prevent race condition (TOCTOU) // Another request may have linked a different Telegram account between auth check and now - if (existingPhoneUser.telegram_id && existingPhoneUser.telegram_id !== telegramId) { + if ( + existingPhoneUser.telegram_id && + existingPhoneUser.telegram_id !== telegramId + ) { logger.warn( "[ElizaAppUserService] Phone user already linked to different Telegram (race)", { @@ -286,15 +327,21 @@ class ElizaAppUserService { throw error; } - logger.info("[ElizaAppUserService] Linked Telegram to existing phone user (iMessage-first)", { - userId: existingPhoneUser.id, - telegramId, - username: telegramData.username, - phone: `***${normalizedPhone.slice(-4)}`, - }); + logger.info( + "[ElizaAppUserService] Linked Telegram to existing phone user (iMessage-first)", + { + userId: existingPhoneUser.id, + telegramId, + username: telegramData.username, + phone: `***${normalizedPhone.slice(-4)}`, + }, + ); // Refetch to get updated data - const updatedUser = await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + const updatedUser = + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); return { user: updatedUser!, organization: updatedUser!.organization!, @@ -324,29 +371,43 @@ class ElizaAppUserService { is_anonymous: false, }, organizationName, - slugGenerator: () => generateSlugFromTelegram(telegramData.username, telegramId), + slugGenerator: () => + generateSlugFromTelegram(telegramData.username, telegramId), signupCode, }); } catch (error) { // Handle race condition: another request created the user first if (isUniqueConstraintError(error)) { // Try to find the user that was created by the other request (by telegram_id) - const userByTelegram = await usersRepository.findByTelegramIdWithOrganization(telegramId); + const userByTelegram = + await usersRepository.findByTelegramIdWithOrganization(telegramId); if (userByTelegram && userByTelegram.organization) { - logger.info("[ElizaAppUserService] Recovered from race condition (telegram)", { - telegramId, - }); - return { user: userByTelegram, organization: userByTelegram.organization, isNew: false }; + logger.info( + "[ElizaAppUserService] Recovered from race condition (telegram)", + { + telegramId, + }, + ); + return { + user: userByTelegram, + organization: userByTelegram.organization, + isNew: false, + }; } // Constraint may have been on phone_number (same phone, different Telegram ID) const userByPhone = - await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); if (userByPhone && userByPhone.organization) { - logger.warn("[ElizaAppUserService] Phone already linked by race condition", { - telegramId, - phone: `***${normalizedPhone.slice(-4)}`, - }); + logger.warn( + "[ElizaAppUserService] Phone already linked by race condition", + { + telegramId, + phone: `***${normalizedPhone.slice(-4)}`, + }, + ); throw new Error("PHONE_ALREADY_LINKED"); } } @@ -356,7 +417,8 @@ class ElizaAppUserService { async findOrCreateByPhone(phoneNumber: string): Promise { const normalizedPhone = normalizePhoneNumber(phoneNumber); - const existingUser = await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + const existingUser = + await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); if (existingUser && existingUser.organization) { if (!existingUser.phone_verified) { @@ -365,11 +427,18 @@ class ElizaAppUserService { updated_at: new Date(), }); } - logger.info("[ElizaAppUserService] Linked phone to existing user (iMessage)", { - userId: existingUser.id, - phone: `***${normalizedPhone.slice(-4)}`, - }); - return { user: existingUser, organization: existingUser.organization, isNew: false }; + logger.info( + "[ElizaAppUserService] Linked phone to existing user (iMessage)", + { + userId: existingUser.id, + phone: `***${normalizedPhone.slice(-4)}`, + }, + ); + return { + user: existingUser, + organization: existingUser.organization, + isNew: false, + }; } const lastFour = normalizedPhone.slice(-4); @@ -390,7 +459,10 @@ class ElizaAppUserService { } catch (error) { // Handle race condition: another request created the user first if (isUniqueConstraintError(error)) { - const user = await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + const user = + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); if (user && user.organization) { logger.info("[ElizaAppUserService] Recovered from race condition", { phone: `***${normalizedPhone.slice(-2)}`, @@ -409,14 +481,22 @@ class ElizaAppUserService { */ async findOrCreateByEmail(email: string): Promise { const normalizedEmail = email.toLowerCase().trim(); - const existingUser = await usersRepository.findByEmailWithOrganization(normalizedEmail); + const existingUser = + await usersRepository.findByEmailWithOrganization(normalizedEmail); if (existingUser && existingUser.organization) { - logger.info("[ElizaAppUserService] Linked email to existing user (iMessage)", { - userId: existingUser.id, - email: maskEmailForLogging(normalizedEmail), - }); - return { user: existingUser, organization: existingUser.organization, isNew: false }; + logger.info( + "[ElizaAppUserService] Linked email to existing user (iMessage)", + { + userId: existingUser.id, + email: maskEmailForLogging(normalizedEmail), + }, + ); + return { + user: existingUser, + organization: existingUser.organization, + isNew: false, + }; } // Create display name from email (mask middle part) @@ -442,11 +522,15 @@ class ElizaAppUserService { } catch (error) { // Handle race condition: another request created the user first if (isUniqueConstraintError(error)) { - const user = await usersRepository.findByEmailWithOrganization(normalizedEmail); + const user = + await usersRepository.findByEmailWithOrganization(normalizedEmail); if (user && user.organization) { - logger.info("[ElizaAppUserService] Recovered from race condition (email)", { - email: maskEmailForLogging(normalizedEmail), - }); + logger.info( + "[ElizaAppUserService] Recovered from race condition (email)", + { + email: maskEmailForLogging(normalizedEmail), + }, + ); return { user, organization: user.organization, isNew: false }; } } @@ -458,19 +542,29 @@ class ElizaAppUserService { return usersRepository.findWithOrganization(userId); } - async getByTelegramId(telegramId: string): Promise { + async getByTelegramId( + telegramId: string, + ): Promise { return usersRepository.findByTelegramIdWithOrganization(telegramId); } - async getByPhoneNumber(phoneNumber: string): Promise { - return usersRepository.findByPhoneNumberWithOrganization(normalizePhoneNumber(phoneNumber)); + async getByPhoneNumber( + phoneNumber: string, + ): Promise { + return usersRepository.findByPhoneNumberWithOrganization( + normalizePhoneNumber(phoneNumber), + ); } async getByEmail(email: string): Promise { - return usersRepository.findByEmailWithOrganization(email.toLowerCase().trim()); + return usersRepository.findByEmailWithOrganization( + email.toLowerCase().trim(), + ); } - async getByDiscordId(discordId: string): Promise { + async getByDiscordId( + discordId: string, + ): Promise { return usersRepository.findByDiscordIdWithOrganization(discordId); } @@ -503,17 +597,23 @@ class ElizaAppUserService { throw new Error("Discord username is required"); } - const normalizedPhone = phoneNumber ? normalizePhoneNumber(phoneNumber) : undefined; + const normalizedPhone = phoneNumber + ? normalizePhoneNumber(phoneNumber) + : undefined; // Scenario 1: Check if user exists by discord_id (returning Discord user) - const existingUser = await usersRepository.findByDiscordIdWithOrganization(discordId); + const existingUser = + await usersRepository.findByDiscordIdWithOrganization(discordId); if (existingUser && existingUser.organization) { // Update Discord profile data if changed (non-critical - graceful degradation) const updates: Partial = {}; let needsUpdate = false; - if (discordData.username && discordData.username !== existingUser.discord_username) { + if ( + discordData.username && + discordData.username !== existingUser.discord_username + ) { updates.discord_username = discordData.username; needsUpdate = true; } @@ -534,13 +634,19 @@ class ElizaAppUserService { // Also set phone number if provided and not already set if (normalizedPhone && !existingUser.phone_number) { - const phoneOwner = await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + const phoneOwner = + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); if (phoneOwner && phoneOwner.id !== existingUser.id) { - logger.warn("[ElizaAppUserService] Phone already owned by another user", { - discordUserId: existingUser.id, - phoneOwnerId: phoneOwner.id, - phone: `***${normalizedPhone.slice(-4)}`, - }); + logger.warn( + "[ElizaAppUserService] Phone already owned by another user", + { + discordUserId: existingUser.id, + phoneOwnerId: phoneOwner.id, + phone: `***${normalizedPhone.slice(-4)}`, + }, + ); throw new Error("PHONE_ALREADY_LINKED"); } updates.phone_number = normalizedPhone; @@ -578,23 +684,37 @@ class ElizaAppUserService { // Refetch if we updated phone if (normalizedPhone && !existingUser.phone_number) { - const refetched = await usersRepository.findByDiscordIdWithOrganization(discordId); + const refetched = + await usersRepository.findByDiscordIdWithOrganization(discordId); if (refetched && refetched.organization) { - return { user: refetched, organization: refetched.organization, isNew: false }; + return { + user: refetched, + organization: refetched.organization, + isNew: false, + }; } } - return { user: existingUser, organization: existingUser.organization, isNew: false }; + return { + user: existingUser, + organization: existingUser.organization, + isNew: false, + }; } // Scenario 2: Check if user exists by phone_number (Telegram/iMessage-first user linking Discord) if (normalizedPhone) { const existingPhoneUser = - await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); if (existingPhoneUser && existingPhoneUser.organization) { // Re-check discord_id to prevent race condition (TOCTOU) - if (existingPhoneUser.discord_id && existingPhoneUser.discord_id !== discordId) { + if ( + existingPhoneUser.discord_id && + existingPhoneUser.discord_id !== discordId + ) { logger.warn( "[ElizaAppUserService] Phone user already linked to different Discord (race)", { @@ -617,10 +737,13 @@ class ElizaAppUserService { }); } catch (error) { if (isUniqueConstraintError(error)) { - logger.warn("[ElizaAppUserService] Race condition on discord link", { - discordId, - phoneUserId: existingPhoneUser.id, - }); + logger.warn( + "[ElizaAppUserService] Race condition on discord link", + { + discordId, + phoneUserId: existingPhoneUser.id, + }, + ); throw new Error("DISCORD_ALREADY_LINKED"); } throw error; @@ -638,7 +761,9 @@ class ElizaAppUserService { // Refetch to get updated data const updatedUser = - await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); return { user: updatedUser!, organization: updatedUser!.organization!, @@ -658,34 +783,47 @@ class ElizaAppUserService { discord_username: discordData.username, discord_global_name: discordData.globalName || undefined, discord_avatar_url: discordData.avatarUrl || undefined, - ...(normalizedPhone && { phone_number: normalizedPhone, phone_verified: true }), + ...(normalizedPhone && { + phone_number: normalizedPhone, + phone_verified: true, + }), name: displayName, is_anonymous: false, }, organizationName, - slugGenerator: () => generateSlugFromDiscord(discordData.username, discordId), + slugGenerator: () => + generateSlugFromDiscord(discordData.username, discordId), signupCode, }); } catch (error) { // Handle race condition: another request created the user first if (isUniqueConstraintError(error)) { - const user = await usersRepository.findByDiscordIdWithOrganization(discordId); + const user = + await usersRepository.findByDiscordIdWithOrganization(discordId); if (user && user.organization) { - logger.info("[ElizaAppUserService] Recovered from race condition (discord)", { - discordId, - }); + logger.info( + "[ElizaAppUserService] Recovered from race condition (discord)", + { + discordId, + }, + ); return { user, organization: user.organization, isNew: false }; } // Constraint may have been on phone_number if (normalizedPhone) { const userByPhone = - await usersRepository.findByPhoneNumberWithOrganization(normalizedPhone); + await usersRepository.findByPhoneNumberWithOrganization( + normalizedPhone, + ); if (userByPhone && userByPhone.organization) { - logger.warn("[ElizaAppUserService] Phone already linked by race condition", { - discordId, - phone: `***${normalizedPhone.slice(-4)}`, - }); + logger.warn( + "[ElizaAppUserService] Phone already linked by race condition", + { + discordId, + phone: `***${normalizedPhone.slice(-4)}`, + }, + ); throw new Error("PHONE_ALREADY_LINKED"); } } @@ -726,7 +864,9 @@ class ElizaAppUserService { * Detects which type of identifier was provided based on format. * Used by Blooio webhook since iMessage can identify users by either phone or Apple ID email. */ - async getByPhoneOrEmail(identifier: string): Promise { + async getByPhoneOrEmail( + identifier: string, + ): Promise { const trimmed = identifier.trim(); // If it contains @, treat as email @@ -738,7 +878,10 @@ class ElizaAppUserService { return this.getByPhoneNumber(trimmed); } - async updateUser(userId: string, data: Partial): Promise { + async updateUser( + userId: string, + data: Partial, + ): Promise { return usersRepository.update(userId, { ...data, updated_at: new Date(), @@ -757,11 +900,14 @@ class ElizaAppUserService { if (existingPhoneUser.id === userId) { return { success: true }; } - logger.warn("[ElizaAppUserService] Phone already linked to another user", { - userId, - existingUserId: existingPhoneUser.id, - phone: `***${normalizedPhone.slice(-2)}`, - }); + logger.warn( + "[ElizaAppUserService] Phone already linked to another user", + { + userId, + existingUserId: existingPhoneUser.id, + phone: `***${normalizedPhone.slice(-2)}`, + }, + ); return { success: false, error: "This phone number is already linked to another account", @@ -812,17 +958,21 @@ class ElizaAppUserService { return { success: false, error: "Invalid email format" }; } - const existingEmailUser = await usersRepository.findByEmailWithOrganization(normalizedEmail); + const existingEmailUser = + await usersRepository.findByEmailWithOrganization(normalizedEmail); if (existingEmailUser) { if (existingEmailUser.id === userId) { return { success: true }; } - logger.warn("[ElizaAppUserService] Email already linked to another user", { - userId, - existingUserId: existingEmailUser.id, - email: maskEmailForLogging(normalizedEmail), // Mask for logs - }); + logger.warn( + "[ElizaAppUserService] Email already linked to another user", + { + userId, + existingUserId: existingEmailUser.id, + email: maskEmailForLogging(normalizedEmail), // Mask for logs + }, + ); return { success: false, error: "This email is already linked to another account", @@ -863,14 +1013,18 @@ class ElizaAppUserService { telegramData: TelegramAuthData, ): Promise<{ success: boolean; error?: string }> { const telegramId = String(telegramData.id); - const existingTelegramUser = await usersRepository.findByTelegramIdWithOrganization(telegramId); + const existingTelegramUser = + await usersRepository.findByTelegramIdWithOrganization(telegramId); if (existingTelegramUser && existingTelegramUser.id !== userId) { - logger.warn("[ElizaAppUserService] Telegram already linked to another user", { - userId, - existingUserId: existingTelegramUser.id, - telegramId, - }); + logger.warn( + "[ElizaAppUserService] Telegram already linked to another user", + { + userId, + existingUserId: existingTelegramUser.id, + telegramId, + }, + ); return { success: false, error: "This Telegram account is already linked to another account", @@ -926,14 +1080,18 @@ class ElizaAppUserService { const { discordId, username, globalName, avatarUrl } = discordData; // Check if this Discord ID is already linked to a different user - const existingDiscordUser = await usersRepository.findByDiscordIdWithOrganization(discordId); + const existingDiscordUser = + await usersRepository.findByDiscordIdWithOrganization(discordId); if (existingDiscordUser && existingDiscordUser.id !== userId) { - logger.warn("[ElizaAppUserService] Discord already linked to another user", { - userId, - existingUserId: existingDiscordUser.id, - discordId, - }); + logger.warn( + "[ElizaAppUserService] Discord already linked to another user", + { + userId, + existingUserId: existingDiscordUser.id, + discordId, + }, + ); return { success: false, error: "This Discord account is already linked to another account", @@ -1001,7 +1159,8 @@ class ElizaAppUserService { const derivedPhone = `+${whatsappId.replace(/\D/g, "")}`; // Scenario 1: Check if user exists by whatsapp_id (returning WhatsApp user) - const existingWhatsAppUser = await usersRepository.findByWhatsAppIdWithOrganization(whatsappId); + const existingWhatsAppUser = + await usersRepository.findByWhatsAppIdWithOrganization(whatsappId); if (existingWhatsAppUser && existingWhatsAppUser.organization) { // Update WhatsApp profile name if changed @@ -1033,11 +1192,15 @@ class ElizaAppUserService { } // Scenario 2: Check if user exists by phone_number (Telegram/iMessage-first user) - const existingPhoneUser = await usersRepository.findByPhoneNumberWithOrganization(derivedPhone); + const existingPhoneUser = + await usersRepository.findByPhoneNumberWithOrganization(derivedPhone); if (existingPhoneUser && existingPhoneUser.organization) { // Re-check whatsapp_id to prevent race condition (TOCTOU) - if (existingPhoneUser.whatsapp_id && existingPhoneUser.whatsapp_id !== whatsappId) { + if ( + existingPhoneUser.whatsapp_id && + existingPhoneUser.whatsapp_id !== whatsappId + ) { logger.warn( "[ElizaAppUserService] Phone user already linked to different WhatsApp (race)", { @@ -1067,14 +1230,18 @@ class ElizaAppUserService { throw error; } - logger.info("[ElizaAppUserService] Linked WhatsApp to existing phone user (cross-platform)", { - userId: existingPhoneUser.id, - whatsappId, - phone: `***${derivedPhone.slice(-4)}`, - }); + logger.info( + "[ElizaAppUserService] Linked WhatsApp to existing phone user (cross-platform)", + { + userId: existingPhoneUser.id, + whatsappId, + phone: `***${derivedPhone.slice(-4)}`, + }, + ); // Refetch to get updated data - const updatedUser = await usersRepository.findByPhoneNumberWithOrganization(derivedPhone); + const updatedUser = + await usersRepository.findByPhoneNumberWithOrganization(derivedPhone); return { user: updatedUser!, organization: updatedUser!.organization!, @@ -1103,21 +1270,33 @@ class ElizaAppUserService { // Handle race condition: another request created the user first if (isUniqueConstraintError(error)) { // Try to find the user that was created by the other request (by whatsapp_id) - const userByWhatsApp = await usersRepository.findByWhatsAppIdWithOrganization(whatsappId); + const userByWhatsApp = + await usersRepository.findByWhatsAppIdWithOrganization(whatsappId); if (userByWhatsApp && userByWhatsApp.organization) { - logger.info("[ElizaAppUserService] Recovered from race condition (whatsapp)", { - whatsappId, - }); - return { user: userByWhatsApp, organization: userByWhatsApp.organization, isNew: false }; + logger.info( + "[ElizaAppUserService] Recovered from race condition (whatsapp)", + { + whatsappId, + }, + ); + return { + user: userByWhatsApp, + organization: userByWhatsApp.organization, + isNew: false, + }; } // Constraint may have been on phone_number (same phone, different WhatsApp ID) - const userByPhone = await usersRepository.findByPhoneNumberWithOrganization(derivedPhone); + const userByPhone = + await usersRepository.findByPhoneNumberWithOrganization(derivedPhone); if (userByPhone && userByPhone.organization) { - logger.warn("[ElizaAppUserService] Phone already linked by race condition (whatsapp)", { - whatsappId, - phone: `***${derivedPhone.slice(-4)}`, - }); + logger.warn( + "[ElizaAppUserService] Phone already linked by race condition (whatsapp)", + { + whatsappId, + phone: `***${derivedPhone.slice(-4)}`, + }, + ); throw new Error("PHONE_ALREADY_LINKED"); } } @@ -1125,7 +1304,9 @@ class ElizaAppUserService { } } - async getByWhatsAppId(whatsappId: string): Promise { + async getByWhatsAppId( + whatsappId: string, + ): Promise { return usersRepository.findByWhatsAppIdWithOrganization(whatsappId); } @@ -1143,14 +1324,18 @@ class ElizaAppUserService { const { whatsappId, name } = whatsappData; // Check if this WhatsApp ID is already linked to a different user - const existingWhatsAppUser = await usersRepository.findByWhatsAppIdWithOrganization(whatsappId); + const existingWhatsAppUser = + await usersRepository.findByWhatsAppIdWithOrganization(whatsappId); if (existingWhatsAppUser && existingWhatsAppUser.id !== userId) { - logger.warn("[ElizaAppUserService] WhatsApp already linked to another user", { - userId, - existingUserId: existingWhatsAppUser.id, - whatsappId, - }); + logger.warn( + "[ElizaAppUserService] WhatsApp already linked to another user", + { + userId, + existingUserId: existingWhatsAppUser.id, + whatsappId, + }, + ); return { success: false, error: "This WhatsApp account is already linked to another account", diff --git a/packages/lib/services/eliza-app/whatsapp-auth.ts b/packages/lib/services/eliza-app/whatsapp-auth.ts index 5ed570c7f..e0250fedd 100644 --- a/packages/lib/services/eliza-app/whatsapp-auth.ts +++ b/packages/lib/services/eliza-app/whatsapp-auth.ts @@ -25,7 +25,11 @@ class WhatsAppAuthService { return false; } - const isValid = verifyWhatsAppSignature(appSecret, signatureHeader, rawBody); + const isValid = verifyWhatsAppSignature( + appSecret, + signatureHeader, + rawBody, + ); if (!isValid) { logger.warn("[WhatsAppAuth] Webhook signature verification failed"); diff --git a/packages/lib/services/eliza-token-price.ts b/packages/lib/services/eliza-token-price.ts index cdcd0cf6b..f150b1047 100644 --- a/packages/lib/services/eliza-token-price.ts +++ b/packages/lib/services/eliza-token-price.ts @@ -83,7 +83,9 @@ export class ElizaTokenPriceService { this.coinGeckoApiKey = process.env.COINGECKO_API_KEY; if (!this.coinGeckoApiKey) { - logger.warn("[ElizaPrice] COINGECKO_API_KEY not set - using free tier with rate limits"); + logger.warn( + "[ElizaPrice] COINGECKO_API_KEY not set - using free tier with rate limits", + ); } } @@ -95,7 +97,9 @@ export class ElizaTokenPriceService { // Check cache first const cachedPrice = await this.getCachedPrice(network); if (cachedPrice) { - logger.debug(`[ElizaPrice] Using cached price for ${network}: $${cachedPrice.priceUsd}`); + logger.debug( + `[ElizaPrice] Using cached price for ${network}: $${cachedPrice.priceUsd}`, + ); return cachedPrice; } @@ -150,11 +154,15 @@ export class ElizaTokenPriceService { /** * Fetch price from CoinGecko. */ - private async fetchFromCoinGecko(network: SupportedNetwork): Promise { + private async fetchFromCoinGecko( + network: SupportedNetwork, + ): Promise { const tokenAddress = ELIZA_TOKEN_ADDRESSES[network]; const isEvm = network !== "solana"; - const baseUrl = isEvm ? PRICE_SOURCES.coingecko.evm : PRICE_SOURCES.coingecko.solana; + const baseUrl = isEvm + ? PRICE_SOURCES.coingecko.evm + : PRICE_SOURCES.coingecko.solana; const platform = network === "solana" @@ -209,7 +217,9 @@ export class ElizaTokenPriceService { /** * Fetch price from DexScreener (for EVM chains). */ - private async fetchFromDexScreener(network: SupportedNetwork): Promise { + private async fetchFromDexScreener( + network: SupportedNetwork, + ): Promise { if (network === "solana") { return { success: false, @@ -256,7 +266,9 @@ export class ElizaTokenPriceService { /** * Fetch price from Jupiter (for Solana). */ - private async fetchFromJupiter(network: SupportedNetwork): Promise { + private async fetchFromJupiter( + network: SupportedNetwork, + ): Promise { if (network !== "solana") { return { success: false, @@ -304,7 +316,9 @@ export class ElizaTokenPriceService { /** * Fetch from multiple sources in parallel. */ - private async fetchFromMultipleSources(network: SupportedNetwork): Promise { + private async fetchFromMultipleSources( + network: SupportedNetwork, + ): Promise { const fetchers: Promise[] = []; // CoinGecko (all networks) @@ -349,8 +363,13 @@ export class ElizaTokenPriceService { * Validate prices from multiple sources. * Throws if prices deviate too much or all sources fail. */ - private validatePrices(prices: PriceFetchResult[], network: SupportedNetwork): PriceQuote { - const successfulPrices = prices.filter((p) => p.success && p.priceUsd !== undefined); + private validatePrices( + prices: PriceFetchResult[], + network: SupportedNetwork, + ): PriceQuote { + const successfulPrices = prices.filter( + (p) => p.success && p.priceUsd !== undefined, + ); if (successfulPrices.length === 0) { const errors = prices.map((p) => `${p.source}: ${p.error}`).join("; "); @@ -363,7 +382,8 @@ export class ElizaTokenPriceService { // If we have multiple prices, check for deviation if (successfulPrices.length > 1) { const priceValues = successfulPrices.map((p) => p.priceUsd!); - const avgPrice = priceValues.reduce((a, b) => a + b, 0) / priceValues.length; + const avgPrice = + priceValues.reduce((a, b) => a + b, 0) / priceValues.length; for (const price of priceValues) { const deviation = Math.abs(price - avgPrice) / avgPrice; @@ -405,7 +425,9 @@ export class ElizaTokenPriceService { /** * Get cached price if still valid. */ - private async getCachedPrice(network: SupportedNetwork): Promise { + private async getCachedPrice( + network: SupportedNetwork, + ): Promise { const now = new Date(); const minExpiresAt = new Date(now.getTime() - PRICE_CACHE_TTL_MS); @@ -433,7 +455,10 @@ export class ElizaTokenPriceService { /** * Cache a validated price. */ - private async cachePrice(network: SupportedNetwork, quote: PriceQuote): Promise { + private async cachePrice( + network: SupportedNetwork, + quote: PriceQuote, + ): Promise { await dbWrite.insert(elizaTokenPrices).values({ network, price_usd: String(quote.priceUsd), diff --git a/packages/lib/services/email.ts b/packages/lib/services/email.ts index a0bca0727..ab762d257 100644 --- a/packages/lib/services/email.ts +++ b/packages/lib/services/email.ts @@ -24,16 +24,23 @@ import { logger } from "@/lib/utils/logger"; class EmailService { private initialized = false; private fromEmail: string | null = null; - private smtpTransporter: Transporter | null = null; + private smtpTransporter: Transporter | null = + null; private useSmtp = false; private initialize(): void { if (this.initialized) return; this.fromEmail = - process.env.SENDGRID_FROM_EMAIL || process.env.SMTP_FROM || "noreply@elizacloud.ai"; + process.env.SENDGRID_FROM_EMAIL || + process.env.SMTP_FROM || + "noreply@elizacloud.ai"; - if (process.env.SMTP_HOST && process.env.SMTP_PORT && process.env.SMTP_PASSWORD) { + if ( + process.env.SMTP_HOST && + process.env.SMTP_PORT && + process.env.SMTP_PASSWORD + ) { logger.info("[EmailService] Using SMTP configuration"); this.smtpTransporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -127,7 +134,9 @@ class EmailService { * @returns True if sent successfully. */ async sendWelcomeEmail(data: WelcomeEmailData): Promise { - const { renderWelcomeTemplate } = await import("@/lib/email/utils/template-renderer"); + const { renderWelcomeTemplate } = await import( + "@/lib/email/utils/template-renderer" + ); const { html, text } = renderWelcomeTemplate(data); return this.send({ @@ -145,7 +154,9 @@ class EmailService { * @returns True if sent successfully. */ async sendLowCreditsEmail(data: LowCreditsEmailData): Promise { - const { renderLowCreditsTemplate } = await import("@/lib/email/utils/template-renderer"); + const { renderLowCreditsTemplate } = await import( + "@/lib/email/utils/template-renderer" + ); const { html, text } = renderLowCreditsTemplate(data); return this.send({ @@ -163,7 +174,9 @@ class EmailService { * @returns True if sent successfully. */ async sendInviteEmail(data: InviteEmailData): Promise { - const { renderInviteTemplate } = await import("@/lib/email/utils/template-renderer"); + const { renderInviteTemplate } = await import( + "@/lib/email/utils/template-renderer" + ); const { html, text } = renderInviteTemplate(data); return this.send({ @@ -180,8 +193,12 @@ class EmailService { * @param data - Auto top-up success email data. * @returns True if sent successfully. */ - async sendAutoTopUpSuccessEmail(data: AutoTopUpSuccessEmailData): Promise { - const { renderAutoTopUpSuccessTemplate } = await import("@/lib/email/utils/template-renderer"); + async sendAutoTopUpSuccessEmail( + data: AutoTopUpSuccessEmailData, + ): Promise { + const { renderAutoTopUpSuccessTemplate } = await import( + "@/lib/email/utils/template-renderer" + ); const { html, text } = renderAutoTopUpSuccessTemplate(data); return this.send({ @@ -198,8 +215,12 @@ class EmailService { * @param data - Auto top-up disabled email data. * @returns True if sent successfully. */ - async sendAutoTopUpDisabledEmail(data: AutoTopUpDisabledEmailData): Promise { - const { renderAutoTopUpDisabledTemplate } = await import("@/lib/email/utils/template-renderer"); + async sendAutoTopUpDisabledEmail( + data: AutoTopUpDisabledEmailData, + ): Promise { + const { renderAutoTopUpDisabledTemplate } = await import( + "@/lib/email/utils/template-renderer" + ); const { html, text } = renderAutoTopUpDisabledTemplate(data); return this.send({ @@ -216,7 +237,9 @@ class EmailService { * @param data - Purchase confirmation email data. * @returns True if sent successfully. */ - async sendPurchaseConfirmationEmail(data: PurchaseConfirmationEmailData): Promise { + async sendPurchaseConfirmationEmail( + data: PurchaseConfirmationEmailData, + ): Promise { const { renderPurchaseConfirmationTemplate } = await import( "@/lib/email/utils/template-renderer" ); diff --git a/packages/lib/services/entity-settings/cache.ts b/packages/lib/services/entity-settings/cache.ts index f6cc5fd53..bcacc065b 100644 --- a/packages/lib/services/entity-settings/cache.ts +++ b/packages/lib/services/entity-settings/cache.ts @@ -50,7 +50,10 @@ interface SettingsCacheValue { sources: Record; } -const inMemorySettingsCache = new InMemoryLRUCache(200, 60_000); +const inMemorySettingsCache = new InMemoryLRUCache( + 200, + 60_000, +); /** * Build cache key for entity settings @@ -107,7 +110,10 @@ export class EntitySettingsCache { const inMemory = inMemorySettingsCache.get(key); if (inMemory) { logger.debug(`[EntitySettingsCache] In-memory cache HIT: ${key}`); - return { settings: new Map(inMemory.settings), sources: { ...inMemory.sources } }; + return { + settings: new Map(inMemory.settings), + sources: { ...inMemory.sources }, + }; } // Fall back to Redis @@ -157,7 +163,9 @@ export class EntitySettingsCache { await cache.set(key, cached, this.ttlSeconds); - logger.debug(`[EntitySettingsCache] Cached ${settings.size} settings for ${key}`); + logger.debug( + `[EntitySettingsCache] Cached ${settings.size} settings for ${key}`, + ); } /** @@ -194,7 +202,9 @@ export class EntitySettingsCache { const pattern = buildUserPattern(userId); await cache.delPattern(pattern); - logger.info(`[EntitySettingsCache] Invalidated all settings for user ${userId}`); + logger.info( + `[EntitySettingsCache] Invalidated all settings for user ${userId}`, + ); } } diff --git a/packages/lib/services/entity-settings/service.ts b/packages/lib/services/entity-settings/service.ts index 98fe2a0e1..da1e043df 100644 --- a/packages/lib/services/entity-settings/service.ts +++ b/packages/lib/services/entity-settings/service.ts @@ -54,7 +54,11 @@ export class EntitySettingsService { * @param organizationId - The organization ID (for OAuth lookups) * @returns Prefetched settings and their sources */ - async prefetch(userId: string, agentId: string, organizationId: string): Promise { + async prefetch( + userId: string, + agentId: string, + organizationId: string, + ): Promise { // Check cache first const cached = await entitySettingsCache.get(userId, agentId); if (cached) { @@ -85,7 +89,11 @@ export class EntitySettingsService { globalEntitySettingsResult.status === "fulfilled" ? globalEntitySettingsResult.value : (logger.warn( - { userId, error: (globalEntitySettingsResult as PromiseRejectedResult).reason }, + { + userId, + error: (globalEntitySettingsResult as PromiseRejectedResult) + .reason, + }, "[EntitySettingsService] Failed to fetch global entity settings", ), new Map()); @@ -97,7 +105,8 @@ export class EntitySettingsService { { userId, agentId, - error: (agentSpecificSettingsResult as PromiseRejectedResult).reason, + error: (agentSpecificSettingsResult as PromiseRejectedResult) + .reason, }, "[EntitySettingsService] Failed to fetch agent-specific settings", ), @@ -107,7 +116,11 @@ export class EntitySettingsService { oauthTokensResult.status === "fulfilled" ? oauthTokensResult.value : (logger.warn( - { userId, organizationId, error: (oauthTokensResult as PromiseRejectedResult).reason }, + { + userId, + organizationId, + error: (oauthTokensResult as PromiseRejectedResult).reason, + }, "[EntitySettingsService] Failed to fetch OAuth tokens", ), new Map()); @@ -116,7 +129,11 @@ export class EntitySettingsService { userApiKeyResult.status === "fulfilled" ? userApiKeyResult.value : (logger.warn( - { userId, organizationId, error: (userApiKeyResult as PromiseRejectedResult).reason }, + { + userId, + organizationId, + error: (userApiKeyResult as PromiseRejectedResult).reason, + }, "[EntitySettingsService] Failed to fetch user API key", ), null); @@ -178,8 +195,14 @@ export class EntitySettingsService { agentId: string | null, ): Promise> { const condition = agentId - ? and(eq(entitySettings.user_id, userId), eq(entitySettings.agent_id, agentId)) - : and(eq(entitySettings.user_id, userId), isNull(entitySettings.agent_id)); + ? and( + eq(entitySettings.user_id, userId), + eq(entitySettings.agent_id, agentId), + ) + : and( + eq(entitySettings.user_id, userId), + isNull(entitySettings.agent_id), + ); const rows = await dbRead.select().from(entitySettings).where(condition); @@ -228,7 +251,8 @@ export class EntitySettingsService { for (const session of sessions) { // Map provider to setting key - const settingKey = OAUTH_PROVIDER_TO_SETTING_KEY[session.provider.toLowerCase()]; + const settingKey = + OAUTH_PROVIDER_TO_SETTING_KEY[session.provider.toLowerCase()]; if (!settingKey) { continue; } @@ -249,7 +273,10 @@ export class EntitySettingsService { /** * Fetch user's elizaOS API key */ - private async fetchUserApiKey(userId: string, organizationId: string): Promise { + private async fetchUserApiKey( + userId: string, + organizationId: string, + ): Promise { // Skip query if userId or organizationId are not valid UUIDs // (e.g., "public", "system", "anonymous" fallback values) if (!isValidUUID(userId) || !isValidUUID(organizationId)) { @@ -287,7 +314,8 @@ export class EntitySettingsService { const encryption = getEncryptionService(); // Encrypt the value - const { encryptedValue, encryptedDek, nonce, authTag, keyId } = await encryption.encrypt(value); + const { encryptedValue, encryptedDek, nonce, authTag, keyId } = + await encryption.encrypt(value); const encryptedFields = { encrypted_value: encryptedValue, @@ -308,7 +336,11 @@ export class EntitySettingsService { ...encryptedFields, }) .onConflictDoUpdate({ - target: [entitySettings.user_id, entitySettings.agent_id, entitySettings.key], + target: [ + entitySettings.user_id, + entitySettings.agent_id, + entitySettings.key, + ], set: { ...encryptedFields, updated_at: new Date(), @@ -398,12 +430,21 @@ export class EntitySettingsService { * * Returns metadata only, not the actual values (for security) */ - async list(userId: string, agentId?: string | null): Promise { + async list( + userId: string, + agentId?: string | null, + ): Promise { let condition; if (agentId !== undefined) { condition = agentId - ? and(eq(entitySettings.user_id, userId), eq(entitySettings.agent_id, agentId)) - : and(eq(entitySettings.user_id, userId), isNull(entitySettings.agent_id)); + ? and( + eq(entitySettings.user_id, userId), + eq(entitySettings.agent_id, agentId), + ) + : and( + eq(entitySettings.user_id, userId), + isNull(entitySettings.agent_id), + ); } else { condition = eq(entitySettings.user_id, userId); } @@ -436,7 +477,8 @@ export class EntitySettingsService { }); // Only show last 3 characters as preview - const valuePreview = decrypted.length > 3 ? "..." + decrypted.slice(-3) : "***"; + const valuePreview = + decrypted.length > 3 ? "..." + decrypted.slice(-3) : "***"; result.push({ id: row.id, diff --git a/packages/lib/services/field-encryption.ts b/packages/lib/services/field-encryption.ts index d290594bf..e001c3aea 100644 --- a/packages/lib/services/field-encryption.ts +++ b/packages/lib/services/field-encryption.ts @@ -109,7 +109,10 @@ export class FieldEncryptionService { // Encrypt with AES-256-GCM const cipher = crypto.createCipheriv(ALGORITHM, dek, nonce); - const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + const ciphertext = Buffer.concat([ + cipher.update(plaintext, "utf8"), + cipher.final(), + ]); const authTag = cipher.getAuthTag(); // Format: enc:v1:::: @@ -147,7 +150,10 @@ export class FieldEncryptionService { const decipher = crypto.createDecipheriv(ALGORITHM, dek, parsed.nonce); decipher.setAuthTag(parsed.authTag); - const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]); + const plaintext = Buffer.concat([ + decipher.update(parsed.ciphertext), + decipher.final(), + ]); return plaintext.toString("utf8"); } @@ -176,7 +182,9 @@ export class FieldEncryptionService { * @param value - The value to potentially decrypt * @returns Decrypted value or original value if not encrypted */ - async decryptIfNeeded(value: string | null | undefined): Promise { + async decryptIfNeeded( + value: string | null | undefined, + ): Promise { if (!value) return null; if (!this.isEncrypted(value)) { logger.warn("Found unencrypted value where encrypted was expected"); @@ -228,10 +236,13 @@ export class FieldEncryptionService { private parseEncryptedValue(value: string): ParsedEncryptedValue { const parts = value.split(":"); if (parts.length !== 6) { - throw new Error(`Invalid encrypted value format: expected 6 parts, got ${parts.length}`); + throw new Error( + `Invalid encrypted value format: expected 6 parts, got ${parts.length}`, + ); } - const [prefix, version, orgKeyId, nonceB64, authTagB64, ciphertextB64] = parts; + const [prefix, version, orgKeyId, nonceB64, authTagB64, ciphertextB64] = + parts; if (prefix !== ENCRYPTION_PREFIX || version !== FORMAT_VERSION) { throw new Error(`Unsupported encryption format: ${prefix}:${version}`); @@ -249,7 +260,9 @@ export class FieldEncryptionService { /** * Get or create an encryption key for an organization. */ - private async getOrCreateOrgKey(organizationId: string): Promise { + private async getOrCreateOrgKey( + organizationId: string, + ): Promise { // Try to get existing key const existingKey = await this.getOrgKeyByOrgId(organizationId); if (existingKey) { @@ -271,17 +284,22 @@ export class FieldEncryptionService { // Handle race condition - another request may have created it if (created) { - logger.info("Created encryption key for organization", { organizationId }); + logger.info("Created encryption key for organization", { + organizationId, + }); return created; } // Race condition: another request created the key, fetch it // Use dbWrite (primary) instead of dbRead (replica) to avoid replication lag - const raceCreatedKey = await dbWrite.query.organizationEncryptionKeys.findFirst({ - where: eq(organizationEncryptionKeys.organization_id, organizationId), - }); + const raceCreatedKey = + await dbWrite.query.organizationEncryptionKeys.findFirst({ + where: eq(organizationEncryptionKeys.organization_id, organizationId), + }); if (!raceCreatedKey) { - throw new Error(`Failed to create/get encryption key for org: ${organizationId}`); + throw new Error( + `Failed to create/get encryption key for org: ${organizationId}`, + ); } return raceCreatedKey; diff --git a/packages/lib/services/gateway-discord/__tests__/constants.test.ts b/packages/lib/services/gateway-discord/__tests__/constants.test.ts index a6c6c3dc0..142c24415 100644 --- a/packages/lib/services/gateway-discord/__tests__/constants.test.ts +++ b/packages/lib/services/gateway-discord/__tests__/constants.test.ts @@ -18,7 +18,9 @@ describe("DEAD_POD_THRESHOLD_MS", () => { test("is at least 3x typical heartbeat interval (15s)", () => { const HEARTBEAT_INTERVAL_MS = 15_000; - expect(DEAD_POD_THRESHOLD_MS).toBeGreaterThanOrEqual(HEARTBEAT_INTERVAL_MS * 3); + expect(DEAD_POD_THRESHOLD_MS).toBeGreaterThanOrEqual( + HEARTBEAT_INTERVAL_MS * 3, + ); }); test("is less than pod state TTL (300s / 5 minutes)", () => { diff --git a/packages/lib/services/gateway-discord/__tests__/event-router.test.ts b/packages/lib/services/gateway-discord/__tests__/event-router.test.ts index c0f0153c5..1e49345c4 100644 --- a/packages/lib/services/gateway-discord/__tests__/event-router.test.ts +++ b/packages/lib/services/gateway-discord/__tests__/event-router.test.ts @@ -25,7 +25,8 @@ describe("sanitizeError logic", () => { /[A-Za-z0-9_-]{18,30}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/g; // Non-global version for single matching tests - const DISCORD_TOKEN_PATTERN = /[A-Za-z0-9_-]{18,30}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/; + const DISCORD_TOKEN_PATTERN = + /[A-Za-z0-9_-]{18,30}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/; const sanitizeError = (error: unknown): string => { const message = error instanceof Error ? error.message : String(error); @@ -64,7 +65,9 @@ describe("sanitizeError logic", () => { const token = SAMPLE_TOKENS[0]; const message = `Failed to authenticate with token: ${token}`; const sanitized = sanitizeError(message); - expect(sanitized).toBe("Failed to authenticate with token: [REDACTED_TOKEN]"); + expect(sanitized).toBe( + "Failed to authenticate with token: [REDACTED_TOKEN]", + ); expect(sanitized).not.toContain(token); }); @@ -80,7 +83,9 @@ describe("sanitizeError logic", () => { const sanitized = sanitizeError(msg); expect(sanitized).not.toContain(SAMPLE_TOKENS[0]); expect(sanitized).not.toContain(SAMPLE_TOKENS[1]); - expect(sanitized).toBe("Token1: [REDACTED_TOKEN], Token2: [REDACTED_TOKEN]"); + expect(sanitized).toBe( + "Token1: [REDACTED_TOKEN], Token2: [REDACTED_TOKEN]", + ); }); test("handles non-Error objects", () => { @@ -165,7 +170,8 @@ describe("channel filtering logic", () => { const channelId = "channel-123"; // If empty, should allow (no filtering) - const shouldProcess = enabledChannels.length === 0 || enabledChannels.includes(channelId); + const shouldProcess = + enabledChannels.length === 0 || enabledChannels.includes(channelId); expect(shouldProcess).toBe(true); }); @@ -173,7 +179,8 @@ describe("channel filtering logic", () => { const enabledChannels = ["channel-123", "channel-456"]; const channelId = "channel-123"; - const shouldProcess = enabledChannels.length === 0 || enabledChannels.includes(channelId); + const shouldProcess = + enabledChannels.length === 0 || enabledChannels.includes(channelId); expect(shouldProcess).toBe(true); }); @@ -181,7 +188,8 @@ describe("channel filtering logic", () => { const enabledChannels = ["channel-123", "channel-456"]; const channelId = "channel-789"; - const shouldProcess = enabledChannels.length === 0 || enabledChannels.includes(channelId); + const shouldProcess = + enabledChannels.length === 0 || enabledChannels.includes(channelId); expect(shouldProcess).toBe(false); }); @@ -226,7 +234,9 @@ describe("keyword matching logic", () => { test("does not match partial word", () => { // "or" should not match "organization" or "more" - expect(matchesKeyword("Tell me more about organizations", ["or"])).toBe(false); + expect(matchesKeyword("Tell me more about organizations", ["or"])).toBe( + false, + ); }); test("matches word at boundaries", () => { @@ -261,7 +271,9 @@ describe("mention mode logic", () => { test("detects when bot is not mentioned", () => { const botUserId = "bot-user-111"; - const mentions = [{ id: "other-user-222", username: "OtherUser", bot: false }]; + const mentions = [ + { id: "other-user-222", username: "OtherUser", bot: false }, + ]; const botMentioned = mentions.some((m) => m.id === botUserId); expect(botMentioned).toBe(false); diff --git a/packages/lib/services/gateway-discord/__tests__/schemas.test.ts b/packages/lib/services/gateway-discord/__tests__/schemas.test.ts index 5d2989c43..a3f820183 100644 --- a/packages/lib/services/gateway-discord/__tests__/schemas.test.ts +++ b/packages/lib/services/gateway-discord/__tests__/schemas.test.ts @@ -377,13 +377,17 @@ describe("MessageCreateDataSchema", () => { describe("DiscordEventPayloadSchema", () => { test("validates connection_id as UUID", () => { const uuidSchema = DiscordEventPayloadSchema.shape.connection_id; - expect(uuidSchema.safeParse("550e8400-e29b-41d4-a716-446655440000").success).toBe(true); + expect( + uuidSchema.safeParse("550e8400-e29b-41d4-a716-446655440000").success, + ).toBe(true); expect(uuidSchema.safeParse("not-a-uuid").success).toBe(false); }); test("validates organization_id as UUID", () => { const uuidSchema = DiscordEventPayloadSchema.shape.organization_id; - expect(uuidSchema.safeParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8").success).toBe(true); + expect( + uuidSchema.safeParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8").success, + ).toBe(true); expect(uuidSchema.safeParse("not-a-uuid").success).toBe(false); }); @@ -395,7 +399,9 @@ describe("DiscordEventPayloadSchema", () => { test("validates timestamp as datetime", () => { const timestampSchema = DiscordEventPayloadSchema.shape.timestamp; - expect(timestampSchema.safeParse("2024-01-15T12:00:00.000Z").success).toBe(true); + expect(timestampSchema.safeParse("2024-01-15T12:00:00.000Z").success).toBe( + true, + ); expect(timestampSchema.safeParse("not-a-date").success).toBe(false); }); diff --git a/packages/lib/services/gateway-discord/event-router.ts b/packages/lib/services/gateway-discord/event-router.ts index c2d3253af..c6108a910 100644 --- a/packages/lib/services/gateway-discord/event-router.ts +++ b/packages/lib/services/gateway-discord/event-router.ts @@ -20,7 +20,10 @@ import { } from "@elizaos/core"; import { createHash } from "crypto"; import { v4 as uuidv4 } from "uuid"; -import { discordConnectionsRepository, userCharactersRepository } from "@/db/repositories"; +import { + discordConnectionsRepository, + userCharactersRepository, +} from "@/db/repositories"; import { AgentMode } from "@/lib/eliza/agent-mode-types"; import { runtimeFactory } from "@/lib/eliza/runtime-factory"; import { userContextService } from "@/lib/eliza/user-context"; @@ -50,7 +53,8 @@ const MAX_DISCORD_MESSAGE_LENGTH = 2000; * - Part 2 (timestamp): 6 characters * - Part 3 (HMAC): 27-40 characters */ -const DISCORD_TOKEN_PATTERN = /[A-Za-z0-9_-]{18,30}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/g; +const DISCORD_TOKEN_PATTERN = + /[A-Za-z0-9_-]{18,30}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/g; /** * Sanitize error messages to prevent accidental token exposure in logs. @@ -137,7 +141,10 @@ class DiscordRateLimiter { (elapsed / DISCORD_RATE_LIMIT_WINDOW_MS) * DISCORD_RATE_LIMIT_REQUESTS, ); if (tokensToAdd > 0) { - state.tokens = Math.min(DISCORD_RATE_LIMIT_REQUESTS, state.tokens + tokensToAdd); + state.tokens = Math.min( + DISCORD_RATE_LIMIT_REQUESTS, + state.tokens + tokensToAdd, + ); state.lastRefill = now; } } @@ -190,7 +197,9 @@ class DiscordRateLimiter { state.queue.push({ resolve, reject }); // Schedule token refill and queue processing - const waitTime = Math.ceil(DISCORD_RATE_LIMIT_WINDOW_MS / DISCORD_RATE_LIMIT_REQUESTS); + const waitTime = Math.ceil( + DISCORD_RATE_LIMIT_WINDOW_MS / DISCORD_RATE_LIMIT_REQUESTS, + ); setTimeout(() => { this.refillTokens(state); this.processQueue(state); @@ -329,7 +338,9 @@ async function handleMessageCreate( } // Get connection to find the associated app - const connection = await discordConnectionsRepository.findById(payload.connection_id); + const connection = await discordConnectionsRepository.findById( + payload.connection_id, + ); if (!connection) { logger.error("[DiscordRouter] Connection not found", { connectionId: payload.connection_id, @@ -341,7 +352,10 @@ async function handleMessageCreate( const metadata = connection.metadata; if (metadata) { // Check channel filtering - if (metadata.enabledChannels?.length && !metadata.enabledChannels.includes(data.channel_id)) { + if ( + metadata.enabledChannels?.length && + !metadata.enabledChannels.includes(data.channel_id) + ) { return { processed: true }; // Skip - channel not enabled } if (metadata.disabledChannels?.includes(data.channel_id)) { @@ -354,9 +368,12 @@ async function handleMessageCreate( // Note: bot_user_id is the actual Discord user ID, different from application_id const botUserId = connection.bot_user_id; if (!botUserId) { - logger.warn("[Discord Event Router] Bot user ID not set, skipping mention check", { - connectionId: connection.id, - }); + logger.warn( + "[Discord Event Router] Bot user ID not set, skipping mention check", + { + connectionId: connection.id, + }, + ); return { processed: true }; } const botMentioned = data.mentions?.some((m) => m.id === botUserId); @@ -388,7 +405,9 @@ async function handleMessageCreate( return { processed: false }; } - const character = await userCharactersRepository.findById(connection.character_id); + const character = await userCharactersRepository.findById( + connection.character_id, + ); if (!character) { logger.warn("[DiscordRouter] Character not found", { characterId: connection.character_id, @@ -456,9 +475,14 @@ async function handleMessageCreate( /** * Process Discord message data into a format for the runtime. */ -function processMessage(data: MessageCreateData, payload: DiscordEventPayload): ProcessedMessage { +function processMessage( + data: MessageCreateData, + payload: DiscordEventPayload, +): ProcessedMessage { // Create a room ID based on channel - const roomId = stringToUuid(`discord-${payload.organization_id}-${data.channel_id}`) as string; + const roomId = stringToUuid( + `discord-${payload.organization_id}-${data.channel_id}`, + ) as string; // Create entity ID for the Discord user const entityId = stringToUuid(`discord-user-${data.author.id}`) as string; @@ -557,7 +581,8 @@ async function sendToRuntime( // Ensure user entity exists const displayName = - message.metadata.discordAuthor.global_name || message.metadata.discordAuthor.username; + message.metadata.discordAuthor.global_name || + message.metadata.discordAuthor.username; try { await runtime.createEntity({ @@ -602,7 +627,9 @@ async function sendToRuntime( content: { text: message.text, source: "discord", - ...(message.attachments?.length ? { attachments: message.attachments } : {}), + ...(message.attachments?.length + ? { attachments: message.attachments } + : {}), }, metadata: { type: MemoryType.MESSAGE, @@ -696,7 +723,10 @@ async function sendDiscordResponse( const makeRequest = async (): Promise => { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), DISCORD_API_TIMEOUT_MS); + const timeoutId = setTimeout( + () => controller.abort(), + DISCORD_API_TIMEOUT_MS, + ); try { return await fetch(`${DISCORD_API_BASE}/channels/${channelId}/messages`, { @@ -715,9 +745,14 @@ async function sendDiscordResponse( // Handle rate limit with single retry if (response.status === 429) { const retryAfterHeader = response.headers.get("Retry-After"); - const retryAfterSeconds = retryAfterHeader ? parseFloat(retryAfterHeader) : undefined; + const retryAfterSeconds = retryAfterHeader + ? parseFloat(retryAfterHeader) + : undefined; - const retryMs = discordRateLimiter.handleRateLimit(botToken, retryAfterSeconds); + const retryMs = discordRateLimiter.handleRateLimit( + botToken, + retryAfterSeconds, + ); // Wait and retry once await new Promise((resolve) => setTimeout(resolve, retryMs)); diff --git a/packages/lib/services/gateway-discord/schemas.ts b/packages/lib/services/gateway-discord/schemas.ts index 866238997..13ec1a857 100644 --- a/packages/lib/services/gateway-discord/schemas.ts +++ b/packages/lib/services/gateway-discord/schemas.ts @@ -125,6 +125,8 @@ export const FailoverRequestSchema = z.object({ // Type exports export type DiscordEventPayload = z.infer; -export type ConnectionStatusUpdate = z.infer; +export type ConnectionStatusUpdate = z.infer< + typeof ConnectionStatusUpdateSchema +>; export type FailoverRequest = z.infer; export type MessageCreateData = z.infer; diff --git a/packages/lib/services/generations.ts b/packages/lib/services/generations.ts index 9ee2ee8a7..e1c2d67ca 100644 --- a/packages/lib/services/generations.ts +++ b/packages/lib/services/generations.ts @@ -2,7 +2,11 @@ * Service for managing AI generation records (images, videos, etc.). */ -import { type Generation, generationsRepository, type NewGeneration } from "@/db/repositories"; +import { + type Generation, + generationsRepository, + type NewGeneration, +} from "@/db/repositories"; /** * Service for tracking and managing AI generation jobs. @@ -16,8 +20,14 @@ export class GenerationsService { return await generationsRepository.findByJobId(jobId); } - async listByOrganization(organizationId: string, limit?: number): Promise { - return await generationsRepository.listByOrganization(organizationId, limit); + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { + return await generationsRepository.listByOrganization( + organizationId, + limit, + ); } async listByOrganizationAndType( @@ -25,7 +35,11 @@ export class GenerationsService { type: string, limit?: number, ): Promise { - return await generationsRepository.listByOrganizationAndType(organizationId, type, limit); + return await generationsRepository.listByOrganizationAndType( + organizationId, + type, + limit, + ); } async listByOrganizationAndStatus( @@ -38,18 +52,29 @@ export class GenerationsService { offset?: number; }, ): Promise { - return await generationsRepository.listByOrganizationAndStatus(organizationId, status, options); + return await generationsRepository.listByOrganizationAndStatus( + organizationId, + status, + options, + ); } async create(data: NewGeneration): Promise { return await generationsRepository.create(data); } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { return await generationsRepository.update(id, data); } - async updateStatus(id: string, status: string, error?: string): Promise { + async updateStatus( + id: string, + status: string, + error?: string, + ): Promise { const updateData: Partial = { status, error, @@ -86,7 +111,11 @@ export class GenerationsService { totalCredits: number; }>; }> { - return await generationsRepository.getStats(organizationId, startDate, endDate); + return await generationsRepository.getStats( + organizationId, + startDate, + endDate, + ); } } diff --git a/packages/lib/services/git-sync.ts b/packages/lib/services/git-sync.ts index c6796679e..8401e0d5f 100644 --- a/packages/lib/services/git-sync.ts +++ b/packages/lib/services/git-sync.ts @@ -92,14 +92,19 @@ export class GitSyncService { const sandbox = this.getSandbox(sandboxId); if (!sandbox) return false; try { - const result = await this.runGitCommand(sandbox, ["rev-parse", "--is-inside-work-tree"]); + const result = await this.runGitCommand(sandbox, [ + "rev-parse", + "--is-inside-work-tree", + ]); return result.exitCode === 0 && result.stdout.trim() === "true"; } catch { return false; } } - async configureGit(config: GitSyncConfig): Promise<{ success: boolean; error?: string }> { + async configureGit( + config: GitSyncConfig, + ): Promise<{ success: boolean; error?: string }> { const { sandboxId, repoFullName, branch = "main" } = config; const sandbox = this.getSandbox(sandboxId); if (!sandbox) return { success: false, error: "Sandbox not found" }; @@ -112,12 +117,16 @@ export class GitSyncService { try { const gitAvailable = await this.isGitAvailable(sandboxId); - if (!gitAvailable) return { success: false, error: "Git is not available in sandbox" }; + if (!gitAvailable) + return { success: false, error: "Git is not available in sandbox" }; - const repoName = repoFullName.includes("/") ? repoFullName.split("/").pop()! : repoFullName; + const repoName = repoFullName.includes("/") + ? repoFullName.split("/").pop()! + : repoFullName; let authenticatedUrl: string; try { - authenticatedUrl = githubReposService.getAuthenticatedCloneUrl(repoName); + authenticatedUrl = + githubReposService.getAuthenticatedCloneUrl(repoName); } catch (e) { return { success: false, @@ -136,7 +145,10 @@ export class GitSyncService { await this.runGitCommand(sandbox, ["checkout", "-b", branch]); } else { // Repo exists (cloned from template) - ensure we're on the right branch - const currentBranch = await this.runGitCommand(sandbox, ["branch", "--show-current"]); + const currentBranch = await this.runGitCommand(sandbox, [ + "branch", + "--show-current", + ]); const currentBranchName = currentBranch.stdout.trim(); logger.info("Current git branch", { sandboxId, @@ -147,7 +159,11 @@ export class GitSyncService { // If not on the target branch, try to switch or create it if (currentBranchName !== branch) { // Try to checkout existing branch or create new one - const checkoutResult = await this.runGitCommand(sandbox, ["checkout", "-B", branch]); + const checkoutResult = await this.runGitCommand(sandbox, [ + "checkout", + "-B", + branch, + ]); if (checkoutResult.exitCode !== 0) { logger.warn("Failed to switch to target branch", { sandboxId, @@ -158,20 +174,42 @@ export class GitSyncService { } } - await this.runGitCommand(sandbox, ["config", "user.name", DEFAULT_AUTHOR.name]); - await this.runGitCommand(sandbox, ["config", "user.email", DEFAULT_AUTHOR.email]); + await this.runGitCommand(sandbox, [ + "config", + "user.name", + DEFAULT_AUTHOR.name, + ]); + await this.runGitCommand(sandbox, [ + "config", + "user.email", + DEFAULT_AUTHOR.email, + ]); const remoteResult = await this.runGitCommand(sandbox, ["remote", "-v"]); const hasOrigin = remoteResult.stdout.includes("origin"); if (hasOrigin) { - await this.runGitCommand(sandbox, ["remote", "set-url", "origin", authenticatedUrl]); + await this.runGitCommand(sandbox, [ + "remote", + "set-url", + "origin", + authenticatedUrl, + ]); } else { - await this.runGitCommand(sandbox, ["remote", "add", "origin", authenticatedUrl]); + await this.runGitCommand(sandbox, [ + "remote", + "add", + "origin", + authenticatedUrl, + ]); } // Fetch from origin (may fail if remote branch doesn't exist yet, that's ok) - const fetchResult = await this.runGitCommand(sandbox, ["fetch", "origin", branch]); + const fetchResult = await this.runGitCommand(sandbox, [ + "fetch", + "origin", + branch, + ]); if (fetchResult.exitCode !== 0) { logger.info("Fetch from origin failed (branch may not exist yet)", { sandboxId, @@ -188,7 +226,8 @@ export class GitSyncService { }); return { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to configure git", { sandboxId, repoFullName, @@ -203,7 +242,10 @@ export class GitSyncService { if (!sandbox) return null; try { - const result = await this.runGitCommand(sandbox, ["status", "--porcelain"]); + const result = await this.runGitCommand(sandbox, [ + "status", + "--porcelain", + ]); if (result.exitCode !== 0) return null; const lines = result.stdout.split("\n").filter((l) => l.trim()); @@ -273,7 +315,8 @@ export class GitSyncService { stderr: result.stderr, }); - if (result.exitCode !== 0) return { success: false, error: result.stderr }; + if (result.exitCode !== 0) + return { success: false, error: result.stderr }; return { success: true }; } catch (error) { return { @@ -283,9 +326,13 @@ export class GitSyncService { } } - async commit(sandboxId: string, options: CommitOptions): Promise { + async commit( + sandboxId: string, + options: CommitOptions, + ): Promise { const sandbox = this.getSandbox(sandboxId); - if (!sandbox) return { success: false, filesCommitted: 0, error: "Sandbox not found" }; + if (!sandbox) + return { success: false, filesCommitted: 0, error: "Sandbox not found" }; const { message, author = DEFAULT_AUTHOR, files } = options; @@ -304,7 +351,10 @@ export class GitSyncService { } // Get raw git status for debugging - const rawStatus = await this.runGitCommand(sandbox, ["status", "--porcelain"]); + const rawStatus = await this.runGitCommand(sandbox, [ + "status", + "--porcelain", + ]); logger.info("Git status after staging", { sandboxId, exitCode: rawStatus.exitCode, @@ -313,7 +363,11 @@ export class GitSyncService { }); // Also check diff --cached to see what's actually staged - const diffCached = await this.runGitCommand(sandbox, ["diff", "--cached", "--name-only"]); + const diffCached = await this.runGitCommand(sandbox, [ + "diff", + "--cached", + "--name-only", + ]); const stagedFiles = diffCached.stdout .trim() .split("\n") @@ -331,7 +385,11 @@ export class GitSyncService { return { success: true, filesCommitted: 0 }; } - const commitResult = await this.runGitCommand(sandbox, ["commit", "-m", message]); + const commitResult = await this.runGitCommand(sandbox, [ + "commit", + "-m", + message, + ]); logger.info("Commit result", { sandboxId, @@ -354,7 +412,10 @@ export class GitSyncService { }; } - const shaResult = await this.runGitCommand(sandbox, ["rev-parse", "HEAD"]); + const shaResult = await this.runGitCommand(sandbox, [ + "rev-parse", + "HEAD", + ]); const commitSha = shaResult.stdout.trim(); logger.info("Commit created", { @@ -364,7 +425,8 @@ export class GitSyncService { }); return { success: true, commitSha, filesCommitted: stagedFiles.length }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to create commit", { sandboxId, error: errorMessage, @@ -406,7 +468,10 @@ export class GitSyncService { } } - async commitAndPush(config: GitSyncConfig, options: CommitOptions): Promise { + async commitAndPush( + config: GitSyncConfig, + options: CommitOptions, + ): Promise { const { sandboxId, repoFullName, branch = "main" } = config; logger.info("Starting commit and push", { @@ -461,7 +526,9 @@ export class GitSyncService { async getLocalCommits( sandboxId: string, limit: number = 10, - ): Promise> { + ): Promise< + Array<{ sha: string; message: string; author: string; date: string }> + > { const sandbox = this.getSandbox(sandboxId); if (!sandbox) return []; try { diff --git a/packages/lib/services/github-repos.ts b/packages/lib/services/github-repos.ts index 151df8500..fac63075f 100644 --- a/packages/lib/services/github-repos.ts +++ b/packages/lib/services/github-repos.ts @@ -12,7 +12,8 @@ import { logger } from "@/lib/utils/logger"; */ const GITHUB_ORG = process.env.GITHUB_ORG_NAME || "eliza-cloud-apps"; -const TEMPLATE_REPO = process.env.GITHUB_TEMPLATE_REPO || "eliza-cloud-apps/cloud-apps-template"; +const TEMPLATE_REPO = + process.env.GITHUB_TEMPLATE_REPO || "eliza-cloud-apps/cloud-apps-template"; // Retry configuration const MAX_RETRIES = 3; @@ -37,7 +38,10 @@ function getOctokit(): Octokit { /** * Retry wrapper with exponential backoff for rate limits */ -async function withRetry(operation: () => Promise, context: string): Promise { +async function withRetry( + operation: () => Promise, + context: string, +): Promise { let lastError: Error | null = null; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { @@ -98,7 +102,9 @@ export interface CommitInfo { /** * Create a new repository from the template */ -export async function createAppRepo(options: CreateRepoOptions): Promise { +export async function createAppRepo( + options: CreateRepoOptions, +): Promise { const octokit = getOctokit(); const { name, description, isPrivate = true } = options; diff --git a/packages/lib/services/headscale-client.ts b/packages/lib/services/headscale-client.ts index f6ec62b77..b6aa4eca3 100644 --- a/packages/lib/services/headscale-client.ts +++ b/packages/lib/services/headscale-client.ts @@ -9,7 +9,8 @@ import { logger } from "@/lib/utils/logger"; -const HEADSCALE_API_URL = process.env.HEADSCALE_API_URL || "http://localhost:8081"; +const HEADSCALE_API_URL = + process.env.HEADSCALE_API_URL || "http://localhost:8081"; const HEADSCALE_API_KEY = process.env.HEADSCALE_API_KEY || ""; const HEADSCALE_USER = process.env.HEADSCALE_USER || "milady"; @@ -99,7 +100,10 @@ export class HeadscaleClient { /** List all registered nodes. */ async listNodes(): Promise { try { - const data = await this.request<{ nodes?: HeadscaleNode[] }>("GET", "/api/v1/node"); + const data = await this.request<{ nodes?: HeadscaleNode[] }>( + "GET", + "/api/v1/node", + ); return data.nodes ?? []; } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); @@ -125,7 +129,10 @@ export class HeadscaleClient { async deleteNode(nodeId: string): Promise { try { logger.info(`[headscale] deleting node ${nodeId}`); - await this.request>("DELETE", `/api/v1/node/${nodeId}`); + await this.request>( + "DELETE", + `/api/v1/node/${nodeId}`, + ); logger.info(`[headscale] deleted node ${nodeId}`); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); @@ -142,7 +149,11 @@ export class HeadscaleClient { /** Set ACL tags on a node (PUT /api/v1/node/{nodeId}/tags). */ async setNodeTags(nodeId: string, tags: string[]): Promise { try { - await this.request>("POST", `/api/v1/node/${nodeId}/tags`, { tags }); + await this.request>( + "POST", + `/api/v1/node/${nodeId}/tags`, + { tags }, + ); logger.info(`[headscale] set tags on node ${nodeId}: ${tags.join(", ")}`); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); @@ -169,11 +180,17 @@ export class HeadscaleClient { expiration?: string; aclTags?: string[]; }): Promise { - const { reusable = false, ephemeral = false, expiration, aclTags = ["tag:agent"] } = opts ?? {}; + const { + reusable = false, + ephemeral = false, + expiration, + aclTags = ["tag:agent"], + } = opts ?? {}; // 10-minute window is sufficient for container boot + VPN enrollment. // Previous 24h was unnecessarily large for single-use ephemeral keys. - const expirationTime = expiration ?? new Date(Date.now() + 10 * 60 * 1000).toISOString(); + const expirationTime = + expiration ?? new Date(Date.now() + 10 * 60 * 1000).toISOString(); const userId = await this.resolveUserId(); @@ -217,13 +234,18 @@ export class HeadscaleClient { /** List all advertised routes across nodes. */ async listRoutes(): Promise { try { - const data = await this.request<{ routes?: HeadscaleRoute[] }>("GET", "/api/v1/routes"); + const data = await this.request<{ routes?: HeadscaleRoute[] }>( + "GET", + "/api/v1/routes", + ); return data.routes ?? []; } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); // Some older Headscale versions may not support /routes if (msg.includes("404")) { - logger.warn("[headscale] routes endpoint not supported; returning empty list"); + logger.warn( + "[headscale] routes endpoint not supported; returning empty list", + ); return []; } logger.error("[headscale] error listing routes:", msg); @@ -234,7 +256,10 @@ export class HeadscaleClient { /** Enable an advertised route by its ID. */ async enableRoute(routeId: string): Promise { try { - await this.request>("POST", `/api/v1/routes/${routeId}/enable`); + await this.request>( + "POST", + `/api/v1/routes/${routeId}/enable`, + ); logger.info(`[headscale] enabled route ${routeId}`); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); @@ -251,7 +276,11 @@ export class HeadscaleClient { * Generic HTTP request helper for the Headscale REST API. * All requests include the Bearer token and an abort timeout. */ - private async request(method: string, path: string, body?: unknown): Promise { + private async request( + method: string, + path: string, + body?: unknown, + ): Promise { const url = `${this.baseUrl}${path}`; const headers: Record = { @@ -274,8 +303,12 @@ export class HeadscaleClient { if (!resp.ok) { const text = await resp.text().catch(() => ""); // Log raw body at debug level only — don't leak it into error messages - logger.debug(`[headscale] API error body for ${method} ${path}:`, { body: text }); - throw new Error(`Headscale API ${method} ${path} failed: ${resp.status} ${resp.statusText}`); + logger.debug(`[headscale] API error body for ${method} ${path}:`, { + body: text, + }); + throw new Error( + `Headscale API ${method} ${path} failed: ${resp.status} ${resp.statusText}`, + ); } // Some endpoints (DELETE) may not return a body @@ -303,7 +336,9 @@ export class HeadscaleClient { }>("GET", "/api/v1/user"); const users = data.users ?? []; - const match = users.find((u) => u.name === this.user || String(u.id) === this.user); + const match = users.find( + (u) => u.name === this.user || String(u.id) === this.user, + ); if (!match?.id || !/^\d+$/.test(String(match.id))) { throw new Error(`[headscale] user not found or invalid: ${this.user}`); diff --git a/packages/lib/services/headscale-integration.ts b/packages/lib/services/headscale-integration.ts index db0c3b7cb..34aea4eb1 100644 --- a/packages/lib/services/headscale-integration.ts +++ b/packages/lib/services/headscale-integration.ts @@ -78,7 +78,10 @@ export class HeadscaleIntegration { return { preAuthKey: preAuthKeyObj.key, envVars }; } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); - logger.error(`[headscale-integration] failed to prepare VPN for ${agentId}:`, msg); + logger.error( + `[headscale-integration] failed to prepare VPN for ${agentId}:`, + msg, + ); throw error; } } @@ -111,7 +114,9 @@ export class HeadscaleIntegration { if (node && node.ipAddresses.length > 0) { const ip = node.ipAddresses[0]; - logger.info(`[headscale-integration] VPN registered for ${agentId}: ${ip}`); + logger.info( + `[headscale-integration] VPN registered for ${agentId}: ${ip}`, + ); return ip; } } catch (err) { @@ -124,7 +129,9 @@ export class HeadscaleIntegration { return null; // bail early, retrying won't help } // Transient errors (network, timeout) — keep polling - logger.debug(`[headscale-integration] Poll error for ${agentId}: ${msg}`); + logger.debug( + `[headscale-integration] Poll error for ${agentId}: ${msg}`, + ); } // Exponential backoff with jitter to avoid thundering-herd on @@ -136,7 +143,9 @@ export class HeadscaleIntegration { interval = Math.min(interval * 1.5, POLL_INTERVAL_MAX_MS); } - logger.warn(`[headscale-integration] VPN registration timeout for ${agentId}`); + logger.warn( + `[headscale-integration] VPN registration timeout for ${agentId}`, + ); return null; } @@ -163,7 +172,10 @@ export class HeadscaleIntegration { logger.info(`[headscale-integration] VPN node cleaned up for ${agentId}`); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); - logger.warn(`[headscale-integration] error cleaning up VPN for ${agentId}:`, msg); + logger.warn( + `[headscale-integration] error cleaning up VPN for ${agentId}:`, + msg, + ); // Don't rethrow — cleanup failures should not block container deletion } } @@ -178,7 +190,10 @@ export class HeadscaleIntegration { return await this.client.getNodeIP(agentId); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); - logger.warn(`[headscale-integration] error getting VPN IP for ${agentId}:`, msg); + logger.warn( + `[headscale-integration] error getting VPN IP for ${agentId}:`, + msg, + ); return null; } } diff --git a/packages/lib/services/health-monitor.ts b/packages/lib/services/health-monitor.ts index 164e5fb12..385a9e1e9 100644 --- a/packages/lib/services/health-monitor.ts +++ b/packages/lib/services/health-monitor.ts @@ -121,12 +121,18 @@ export async function updateContainerHealth( // If no rows were updated, container status has changed (not a race condition) if (!updatedContainer) { // Just update health check timestamp without changing status - await dbWrite.update(containers).set(baseUpdate).where(eq(containers.id, containerId)); - - logger.debug("Container health check failed, but status changed (not running anymore)", { - containerId, - healthy: false, - }); + await dbWrite + .update(containers) + .set(baseUpdate) + .where(eq(containers.id, containerId)); + + logger.debug( + "Container health check failed, but status changed (not running anymore)", + { + containerId, + healthy: false, + }, + ); return; } @@ -155,7 +161,10 @@ export async function updateContainerHealth( if (!updatedContainer) { // Just update health check timestamp for non-failed containers - await dbWrite.update(containers).set(baseUpdate).where(eq(containers.id, containerId)); + await dbWrite + .update(containers) + .set(baseUpdate) + .where(eq(containers.id, containerId)); logger.debug("Container health check passed, status unchanged", { containerId, diff --git a/packages/lib/services/invites.ts b/packages/lib/services/invites.ts index 66a2d9144..b1fbc2be6 100644 --- a/packages/lib/services/invites.ts +++ b/packages/lib/services/invites.ts @@ -7,7 +7,10 @@ import { type OrganizationInvite, organizationInvitesRepository, } from "@/db/repositories"; -import { generateInviteToken, hashInviteToken } from "@/lib/utils/invite-tokens"; +import { + generateInviteToken, + hashInviteToken, +} from "@/lib/utils/invite-tokens"; import { emailService } from "./email"; import { organizationsService } from "./organizations"; import { usersService } from "./users"; @@ -50,16 +53,28 @@ export class InvitesService { return await organizationInvitesRepository.findById(id); } - async listByOrganization(organizationId: string): Promise { - return await organizationInvitesRepository.listByOrganization(organizationId); + async listByOrganization( + organizationId: string, + ): Promise { + return await organizationInvitesRepository.listByOrganization( + organizationId, + ); } - async listPendingByOrganization(organizationId: string): Promise { - return await organizationInvitesRepository.listPendingByOrganization(organizationId); + async listPendingByOrganization( + organizationId: string, + ): Promise { + return await organizationInvitesRepository.listPendingByOrganization( + organizationId, + ); } - async findPendingInviteByEmail(email: string): Promise { - return await organizationInvitesRepository.findPendingInviteByEmail(email.toLowerCase()); + async findPendingInviteByEmail( + email: string, + ): Promise { + return await organizationInvitesRepository.findPendingInviteByEmail( + email.toLowerCase(), + ); } async createInvite(params: CreateInviteParams): Promise<{ @@ -74,13 +89,16 @@ export class InvitesService { throw new Error("Invalid role. Must be 'admin' or 'member'"); } - const existingUser = await usersService.getByEmailWithOrganization(normalizedEmail); + const existingUser = + await usersService.getByEmailWithOrganization(normalizedEmail); if (existingUser && existingUser.organization_id === organizationId) { throw new Error("User is already a member of this organization"); } const existingInvite = - await organizationInvitesRepository.findPendingInviteByEmail(normalizedEmail); + await organizationInvitesRepository.findPendingInviteByEmail( + normalizedEmail, + ); if (existingInvite && existingInvite.organization_id === organizationId) { throw new Error("An invite for this email is already pending"); } @@ -122,7 +140,8 @@ export class InvitesService { async validateToken(token: string): Promise { const tokenHash = hashInviteToken(token); - const invite = await organizationInvitesRepository.findByTokenHash(tokenHash); + const invite = + await organizationInvitesRepository.findByTokenHash(tokenHash); if (!invite) { return { valid: false, error: "Invalid invite" }; @@ -148,7 +167,10 @@ export class InvitesService { return { valid: true, invite: invite as InviteWithOrganization }; } - async acceptInvite(token: string, userId: string): Promise { + async acceptInvite( + token: string, + userId: string, + ): Promise { const validation = await this.validateToken(token); if (!validation.valid || !validation.invite) { throw new Error(validation.error || "Invalid invite"); @@ -162,7 +184,9 @@ export class InvitesService { } if (user.email?.toLowerCase() !== invite.invited_email) { - throw new Error(`Please sign in with ${invite.invited_email} to accept this invite`); + throw new Error( + `Please sign in with ${invite.invited_email} to accept this invite`, + ); } if (user.organization_id === invite.organization_id) { @@ -181,7 +205,10 @@ export class InvitesService { updated_at: new Date(), }); - const updatedInvite = await organizationInvitesRepository.markAsAccepted(invite.id, userId); + const updatedInvite = await organizationInvitesRepository.markAsAccepted( + invite.id, + userId, + ); if (!updatedInvite) { throw new Error("Failed to mark invite as accepted"); @@ -190,7 +217,10 @@ export class InvitesService { return updatedInvite; } - async revokeInvite(inviteId: string, organizationId: string): Promise { + async revokeInvite( + inviteId: string, + organizationId: string, + ): Promise { const invite = await organizationInvitesRepository.findById(inviteId); if (!invite) { diff --git a/packages/lib/services/invoices.ts b/packages/lib/services/invoices.ts index 43d57c556..6493ab297 100644 --- a/packages/lib/services/invoices.ts +++ b/packages/lib/services/invoices.ts @@ -30,7 +30,9 @@ class InvoicesService { return invoice; } - async getByStripeInvoiceId(stripeInvoiceId: string): Promise { + async getByStripeInvoiceId( + stripeInvoiceId: string, + ): Promise { const [invoice] = await dbRead .select() .from(invoices) @@ -70,7 +72,11 @@ class InvoicesService { } async getById(id: string): Promise { - const [invoice] = await dbRead.select().from(invoices).where(eq(invoices.id, id)).limit(1); + const [invoice] = await dbRead + .select() + .from(invoices) + .where(eq(invoices.id, id)) + .limit(1); return invoice; } diff --git a/packages/lib/services/llm-trajectory.ts b/packages/lib/services/llm-trajectory.ts index 25c641650..be6586ebc 100644 --- a/packages/lib/services/llm-trajectory.ts +++ b/packages/lib/services/llm-trajectory.ts @@ -12,7 +12,10 @@ import { and, desc, eq, gte, lte, sql } from "drizzle-orm"; import { db } from "@/db/client"; -import { llmTrajectories, type NewLlmTrajectory } from "@/db/schemas/llm-trajectories"; +import { + llmTrajectories, + type NewLlmTrajectory, +} from "@/db/schemas/llm-trajectories"; export interface LogCallParams { organizationId: string; @@ -95,7 +98,10 @@ class LlmTrajectoryService { /** * List trajectories for an organization. */ - async listByOrganization(organizationId: string, filters: TrajectoryFilters = {}) { + async listByOrganization( + organizationId: string, + filters: TrajectoryFilters = {}, + ) { const conditions = [eq(llmTrajectories.organization_id, organizationId)]; if (filters.model) { @@ -179,10 +185,12 @@ class LlmTrajectoryService { avgLatencyMs: Math.round(Number(result?.avgLatencyMs ?? 0)), successCount: Number(result?.successCount ?? 0), failureCount: Number(result?.failureCount ?? 0), - byPurpose: byPurpose.map((r: { purpose: string | null; count: unknown }) => ({ - purpose: r.purpose, - count: Number(r.count), - })), + byPurpose: byPurpose.map( + (r: { purpose: string | null; count: unknown }) => ({ + purpose: r.purpose, + count: Number(r.count), + }), + ), byModel: byModel.map((r: { model: string; count: unknown }) => ({ model: r.model, count: Number(r.count), diff --git a/packages/lib/services/managed-milady-config.ts b/packages/lib/services/managed-milady-config.ts index ff4b110b5..03f4cd903 100644 --- a/packages/lib/services/managed-milady-config.ts +++ b/packages/lib/services/managed-milady-config.ts @@ -35,13 +35,17 @@ export function normalizeBaseUrl(raw: string): string { export function resolveMiladyAppUrl(): string { return normalizeBaseUrl( - process.env.NEXT_PUBLIC_MILADY_APP_URL || process.env.MILADY_APP_URL || DEFAULT_MILADY_APP_URL, + process.env.NEXT_PUBLIC_MILADY_APP_URL || + process.env.MILADY_APP_URL || + DEFAULT_MILADY_APP_URL, ); } export function resolveCloudPublicUrl(): string { return normalizeBaseUrl( - process.env.NEXT_PUBLIC_APP_URL || process.env.ELIZA_CLOUD_URL || DEFAULT_CLOUD_PUBLIC_URL, + process.env.NEXT_PUBLIC_APP_URL || + process.env.ELIZA_CLOUD_URL || + DEFAULT_CLOUD_PUBLIC_URL, ); } @@ -117,9 +121,14 @@ function isActiveApiKeyForUser( return !key.expires_at || new Date(key.expires_at).getTime() > Date.now(); } -async function getOrCreateUserApiKey(userId: string, organizationId: string): Promise { +async function getOrCreateUserApiKey( + userId: string, + organizationId: string, +): Promise { const existingKeys = await apiKeysService.listByOrganization(organizationId); - const existingKey = existingKeys.find((key) => isActiveApiKeyForUser(key, userId)); + const existingKey = existingKeys.find((key) => + isActiveApiKeyForUser(key, userId), + ); if (existingKey) { return existingKey.key; } @@ -144,9 +153,13 @@ export async function prepareManagedMiladyBaseEnvironment(params: { userId: string; }): Promise { const existingEnv = { ...(params.existingEnv ?? {}) }; - const userApiKey = await getOrCreateUserApiKey(params.userId, params.organizationId); + const userApiKey = await getOrCreateUserApiKey( + params.userId, + params.organizationId, + ); const apiToken = - existingEnv.MILADY_API_TOKEN?.trim() || `milady_${crypto.randomUUID().replace(/-/g, "")}`; + existingEnv.MILADY_API_TOKEN?.trim() || + `milady_${crypto.randomUUID().replace(/-/g, "")}`; return { apiToken, @@ -155,7 +168,9 @@ export async function prepareManagedMiladyBaseEnvironment(params: { ...existingEnv, MILADY_API_TOKEN: apiToken, MILADY_ALLOW_WS_QUERY_TOKEN: "1", - MILADY_ALLOWED_ORIGINS: mergeManagedAllowedOrigins(existingEnv.MILADY_ALLOWED_ORIGINS), + MILADY_ALLOWED_ORIGINS: mergeManagedAllowedOrigins( + existingEnv.MILADY_ALLOWED_ORIGINS, + ), ELIZAOS_API_KEY: userApiKey, ELIZAOS_CLOUD_API_KEY: userApiKey, ELIZAOS_CLOUD_ENABLED: "true", diff --git a/packages/lib/services/managed-milady-env.ts b/packages/lib/services/managed-milady-env.ts index b4a17fe40..00cde1946 100644 --- a/packages/lib/services/managed-milady-env.ts +++ b/packages/lib/services/managed-milady-env.ts @@ -39,7 +39,8 @@ export async function prepareManagedMiladyEnvironment(params: { environmentVars.STEWARD_AGENT_ID = params.sandboxId; } - const changed = JSON.stringify(existingEnv) !== JSON.stringify(environmentVars); + const changed = + JSON.stringify(existingEnv) !== JSON.stringify(environmentVars); return { apiToken: environmentVars.MILADY_API_TOKEN, diff --git a/packages/lib/services/memory.ts b/packages/lib/services/memory.ts index 3f8c8c76a..2e3471c75 100644 --- a/packages/lib/services/memory.ts +++ b/packages/lib/services/memory.ts @@ -10,7 +10,11 @@ import type { ConversationMessage } from "@/db/repositories"; import { memoryTable, participantTable } from "@/db/schemas/eliza"; import { users } from "@/db/schemas/users"; import { CacheKeys, CacheTTL } from "@/lib/cache/keys"; -import { type MemoryRoomContext, memoryCache, type SearchResult } from "@/lib/cache/memory-cache"; +import { + type MemoryRoomContext, + memoryCache, + type SearchResult, +} from "@/lib/cache/memory-cache"; import { AgentMode } from "@/lib/eliza/agent-mode-types"; import { runtimeFactory } from "@/lib/eliza/runtime-factory"; import { userContextService } from "@/lib/eliza/user-context"; @@ -96,7 +100,9 @@ export interface SummarizeConversationResult { participants: string[]; } -const isSummarizeConversationResult = (value: unknown): value is SummarizeConversationResult => { +const isSummarizeConversationResult = ( + value: unknown, +): value is SummarizeConversationResult => { if (!value || typeof value !== "object") return false; const record = value as Record; return ( @@ -119,19 +125,31 @@ export class MemoryService { } // PERFORMANCE FIX: Add method to check single room ownership efficiently - private async checkRoomOwnership(organizationId: string, roomId: string): Promise { + private async checkRoomOwnership( + organizationId: string, + roomId: string, + ): Promise { const result = await dbRead .select({ exists: participantTable.roomId }) .from(participantTable) .innerJoin(users, eq(participantTable.entityId, users.id)) - .where(and(eq(participantTable.roomId, roomId), eq(users.organization_id, organizationId))) + .where( + and( + eq(participantTable.roomId, roomId), + eq(users.organization_id, organizationId), + ), + ) .limit(1); return result.length > 0; } - private async getRoomIdsForOrganization(organizationId: string): Promise> { - logger.info(`[Memory Service] getRoomIdsForOrganization called for org: ${organizationId}`); + private async getRoomIdsForOrganization( + organizationId: string, + ): Promise> { + logger.info( + `[Memory Service] getRoomIdsForOrganization called for org: ${organizationId}`, + ); const results = await dbRead .selectDistinct({ roomId: participantTable.roomId }) @@ -181,7 +199,9 @@ export class MemoryService { channelId: input.roomId, userName: input.entityId, }); - logger.debug(`[Memory Service] Ensured entity and participant: ${entityId} -> ${roomId}`); + logger.debug( + `[Memory Service] Ensured entity and participant: ${entityId} -> ${roomId}`, + ); const memory: Memory = { id: uuidv4() as UUID, @@ -200,13 +220,16 @@ export class MemoryService { const persistent = input.persistent !== false; if (persistent) { - logger.debug(`[Memory Service] Attempting to create memory in PostgreSQL:`, { - memoryId: memory.id, - roomId: memory.roomId, - entityId: memory.entityId, - agentId: memory.agentId, - contentLength: JSON.stringify(memory.content).length, - }); + logger.debug( + `[Memory Service] Attempting to create memory in PostgreSQL:`, + { + memoryId: memory.id, + roomId: memory.roomId, + entityId: memory.entityId, + agentId: memory.agentId, + contentLength: JSON.stringify(memory.content).length, + }, + ); await runtime.createMemory(memory, "memories", true); logger.info(`[Memory Service] Saved memory to PostgreSQL: ${memory.id}`); } @@ -225,7 +248,9 @@ export class MemoryService { }; } - async retrieveMemories(input: RetrieveMemoriesInput): Promise { + async retrieveMemories( + input: RetrieveMemoriesInput, + ): Promise { logger.info( `[Memory Service] retrieveMemories called with input:`, JSON.stringify( @@ -243,7 +268,9 @@ export class MemoryService { ); const runtime = await this.getSystemRuntime(); - logger.info(`[Memory Service] Runtime initialized, agentId: ${runtime.agentId}`); + logger.info( + `[Memory Service] Runtime initialized, agentId: ${runtime.agentId}`, + ); if (input.query) { const queryHash = this.hashQuery(input.query, input); @@ -261,9 +288,14 @@ export class MemoryService { // PERFORMANCE FIX: Use efficient single-room check when roomId is provided // instead of fetching all room IDs for the organization if (input.roomId) { - logger.info(`[Memory Service] Checking room ownership for roomId ${input.roomId}`); + logger.info( + `[Memory Service] Checking room ownership for roomId ${input.roomId}`, + ); - const hasAccess = await this.checkRoomOwnership(input.organizationId, input.roomId); + const hasAccess = await this.checkRoomOwnership( + input.organizationId, + input.roomId, + ); if (!hasAccess) { logger.warn( @@ -272,11 +304,15 @@ export class MemoryService { return []; } - logger.info(`[Memory Service] Room ${input.roomId} is allowed, fetching memories`); + logger.info( + `[Memory Service] Room ${input.roomId} is allowed, fetching memories`, + ); if (input.query) { const embedding = new Array(1536).fill(0); - logger.info(`[Memory Service] Calling searchMemories with roomId: ${input.roomId}`); + logger.info( + `[Memory Service] Calling searchMemories with roomId: ${input.roomId}`, + ); memories = await runtime.searchMemories({ embedding, tableName: "memories", @@ -285,13 +321,18 @@ export class MemoryService { match_threshold: 0.7, }); } else { - logger.info(`[Memory Service] Querying database directly for roomId: ${input.roomId}`); + logger.info( + `[Memory Service] Querying database directly for roomId: ${input.roomId}`, + ); const results = await dbRead .select() .from(memoryTable) .where( - and(eq(memoryTable.roomId, input.roomId), eq(memoryTable.agentId, runtime.agentId)), + and( + eq(memoryTable.roomId, input.roomId), + eq(memoryTable.agentId, runtime.agentId), + ), ) .orderBy(desc(memoryTable.createdAt)) .limit(input.limit || 10); @@ -312,17 +353,27 @@ export class MemoryService { ); } - logger.info(`[Memory Service] Database query returned ${memories.length} memories`); + logger.info( + `[Memory Service] Database query returned ${memories.length} memories`, + ); } else { - logger.info(`[Memory Service] No specific roomId, fetching from all allowed rooms`); + logger.info( + `[Memory Service] No specific roomId, fetching from all allowed rooms`, + ); // Fetch all room IDs for the organization when no specific room is requested - const allowedRoomIds = await this.getRoomIdsForOrganization(input.organizationId); + const allowedRoomIds = await this.getRoomIdsForOrganization( + input.organizationId, + ); - logger.info(`[Memory Service] Allowed room IDs size: ${allowedRoomIds.size}`); + logger.info( + `[Memory Service] Allowed room IDs size: ${allowedRoomIds.size}`, + ); if (allowedRoomIds.size === 0) { - logger.warn(`[Memory Service] No rooms found for organization ${input.organizationId}`); + logger.warn( + `[Memory Service] No rooms found for organization ${input.organizationId}`, + ); return []; } @@ -335,7 +386,10 @@ export class MemoryService { .select() .from(memoryTable) .where( - and(inArray(memoryTable.roomId, roomIdArray), eq(memoryTable.agentId, runtime.agentId)), + and( + inArray(memoryTable.roomId, roomIdArray), + eq(memoryTable.agentId, runtime.agentId), + ), ) .orderBy(desc(memoryTable.createdAt)) .limit(limit); @@ -370,7 +424,11 @@ export class MemoryService { if (input.query) { const queryHash = this.hashQuery(input.query, input); - await memoryCache.cacheSearchResults(queryHash, results, CacheTTL.memory.search); + await memoryCache.cacheSearchResults( + queryHash, + results, + CacheTTL.memory.search, + ); } logger.info( @@ -439,7 +497,9 @@ export class MemoryService { CacheTTL.memory.roomContext, ); - logger.info(`[Memory Service] Retrieved room context: ${roomId} (${memories.length} messages)`); + logger.info( + `[Memory Service] Retrieved room context: ${roomId} (${memories.length} messages)`, + ); return context; } @@ -451,7 +511,9 @@ export class MemoryService { CacheKeys.memory.conversationSummary(input.organizationId, cacheKey), ); if (cached) { - logger.debug(`[Memory Service] Cache HIT for conversation summary: ${input.roomId}`); + logger.debug( + `[Memory Service] Cache HIT for conversation summary: ${input.roomId}`, + ); // Type guard to ensure cached content matches SummarizeConversationResult const cachedContent = cached.content; if (isSummarizeConversationResult(cachedContent)) { @@ -466,7 +528,10 @@ export class MemoryService { input.lastN || 50, ); - const summaryPrompt = this.buildSummaryPrompt(context, input.style || "brief"); + const summaryPrompt = this.buildSummaryPrompt( + context, + input.style || "brief", + ); const result = await streamText({ model: gateway.languageModel("gpt-4o-mini"), @@ -516,12 +581,20 @@ export class MemoryService { return summary; } - private hashQuery(query: string, filters: Partial): string { - const hash = createHash("md5").update(JSON.stringify({ query, filters })).digest("hex"); + private hashQuery( + query: string, + filters: Partial, + ): string { + const hash = createHash("md5") + .update(JSON.stringify({ query, filters })) + .digest("hex"); return hash.substring(0, 16); } - private buildSummaryPrompt(context: MemoryRoomContext, style: string): string { + private buildSummaryPrompt( + context: MemoryRoomContext, + style: string, + ): string { const messages = context.messages .slice(0, 50) .map((m) => `${m.entityId}: ${m.content.text}`) @@ -529,7 +602,8 @@ export class MemoryService { const styleInstructions = { brief: "Provide a concise 2-3 sentence summary.", - detailed: "Provide a comprehensive summary with key points and discussion flow.", + detailed: + "Provide a comprehensive summary with key points and discussion flow.", "bullet-points": "Provide a bulleted list of the main topics discussed.", }; @@ -598,7 +672,9 @@ Summary:`; relevanceScores.sort((a, b) => b.score - a.score); for (const scoreItem of relevanceScores) { - const msg = olderMessages.find((m) => m.id?.toString() === scoreItem.messageId); + const msg = olderMessages.find( + (m) => m.id?.toString() === scoreItem.messageId, + ); if (msg) { const msgTokens = await this.estimateTokenCount([msg]); if (currentTokens + msgTokens <= maxTokens) { @@ -642,7 +718,8 @@ Summary:`; size: number; format: string; }> { - const conversation = await conversationsService.getWithMessages(conversationId); + const conversation = + await conversationsService.getWithMessages(conversationId); if (!conversation) { throw new Error("Conversation not found"); @@ -705,7 +782,9 @@ Summary:`; break; } - logger.info(`[Memory Service] Exported conversation ${conversationId} as ${format}`); + logger.info( + `[Memory Service] Exported conversation ${conversationId} as ${format}`, + ); return { content, @@ -728,7 +807,8 @@ Summary:`; conversationId: string; clonedMessageCount: number; }> { - const sourceConversation = await conversationsService.getWithMessages(conversationId); + const sourceConversation = + await conversationsService.getWithMessages(conversationId); if (!sourceConversation) { throw new Error("Source conversation not found"); @@ -784,7 +864,9 @@ Summary:`; limit: 100, }); - const memoriesText = memories.map((m) => m.memory.content.text || "").join("\n"); + const memoriesText = memories + .map((m) => m.memory.content.text || "") + .join("\n"); let insights: string[] = []; let data: Record = {}; @@ -817,7 +899,9 @@ Summary:`; groupedByDay.set(day, (groupedByDay.get(day) || 0) + 1); } - chartData = Array.from(groupedByDay.entries()).map(([label, value]) => ({ label, value })); + chartData = Array.from(groupedByDay.entries()).map( + ([label, value]) => ({ label, value }), + ); insights = [ `Analyzed ${memories.length} memories over ${groupedByDay.size} days`, @@ -859,10 +943,12 @@ Summary:`; const lowerText = memoriesText.toLowerCase(); for (const word of positiveWords) { - positiveCount += (lowerText.match(new RegExp(word, "g")) || []).length; + positiveCount += (lowerText.match(new RegExp(word, "g")) || []) + .length; } for (const word of negativeWords) { - negativeCount += (lowerText.match(new RegExp(word, "g")) || []).length; + negativeCount += (lowerText.match(new RegExp(word, "g")) || []) + .length; } const neutralCount = memories.length - positiveCount - negativeCount; @@ -888,7 +974,9 @@ Summary:`; } } - logger.info(`[Memory Service] Analyzed ${memories.length} memories for ${analysisType}`); + logger.info( + `[Memory Service] Analyzed ${memories.length} memories for ${analysisType}`, + ); return { analysisType, diff --git a/packages/lib/services/message-router/index.ts b/packages/lib/services/message-router/index.ts index e4f69323e..479075aa2 100644 --- a/packages/lib/services/message-router/index.ts +++ b/packages/lib/services/message-router/index.ts @@ -36,7 +36,9 @@ const MAX_METADATA_SIZE = 10 * 1024; /** * Helper to validate and sanitize metadata before storage */ -function validateMetadata(metadata: unknown): Record | undefined { +function validateMetadata( + metadata: unknown, +): Record | undefined { if (!metadata) return undefined; try { @@ -99,7 +101,9 @@ class MessageRouterService { /** * Find the agent and phone number mapping for an incoming message */ - async routeIncomingMessage(message: IncomingMessage): Promise { + async routeIncomingMessage( + message: IncomingMessage, + ): Promise { try { logger.info("[MessageRouter] Routing incoming message", { from: message.from, @@ -113,7 +117,10 @@ class MessageRouterService { .from(agentPhoneNumbers) .where( and( - eq(agentPhoneNumbers.phone_number, normalizePhoneNumber(message.to)), + eq( + agentPhoneNumbers.phone_number, + normalizePhoneNumber(message.to), + ), eq(agentPhoneNumbers.is_active, true), ), ) @@ -198,12 +205,15 @@ class MessageRouterService { // Check if room exists, if not create it const existingRoom = await this.findExistingRoom(roomId); if (!existingRoom) { - logger.info("[MessageRouter] Creating new room for phone conversation", { - roomId, - agentId, - from: message.from, - to: message.to, - }); + logger.info( + "[MessageRouter] Creating new room for phone conversation", + { + roomId, + agentId, + from: message.from, + to: message.to, + }, + ); await roomsService.createRoom({ id: roomId, @@ -255,12 +265,16 @@ class MessageRouterService { } // Fallback if agent doesn't respond (e.g., agent returned null/empty) - logger.warn("[MessageRouter] Agent returned no response", { agentId, organizationId }); + logger.warn("[MessageRouter] Agent returned no response", { + agentId, + organizationId, + }); return { text: "Thanks for your message! I'm processing it but couldn't generate a response. Please try again.", }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); logger.error("[MessageRouter] Error processing with agent", { error: errorMessage, agentId, @@ -268,12 +282,18 @@ class MessageRouterService { }); // Return differentiated error messages based on error type - if (errorMessage.includes("not found") || errorMessage.includes("not configured")) { + if ( + errorMessage.includes("not found") || + errorMessage.includes("not configured") + ) { return { text: "Sorry, this assistant is currently not available. Please contact support if the issue persists.", }; } - if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) { + if ( + errorMessage.includes("timeout") || + errorMessage.includes("ETIMEDOUT") + ) { return { text: "Sorry, the response is taking longer than expected. Please try again in a moment.", }; @@ -378,9 +398,17 @@ class MessageRouterService { // Use secretsService.get() which looks up by (organizationId, secretName) // Note: getDecryptedValue() takes (secretId, organizationId) - different signature - const { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN } = await import("@/lib/constants/secrets"); - const accountSid = await secretsService.get(params.organizationId, TWILIO_ACCOUNT_SID); - const authToken = await secretsService.get(params.organizationId, TWILIO_AUTH_TOKEN); + const { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN } = await import( + "@/lib/constants/secrets" + ); + const accountSid = await secretsService.get( + params.organizationId, + TWILIO_ACCOUNT_SID, + ); + const authToken = await secretsService.get( + params.organizationId, + TWILIO_AUTH_TOKEN, + ); if (!accountSid || !authToken) { logger.error("[MessageRouter] Missing Twilio credentials"); @@ -428,7 +456,10 @@ class MessageRouterService { // Use secretsService.get() which looks up by (organizationId, secretName) const { BLOOIO_API_KEY } = await import("@/lib/constants/secrets"); - const apiKey = await secretsService.get(params.organizationId, BLOOIO_API_KEY); + const apiKey = await secretsService.get( + params.organizationId, + BLOOIO_API_KEY, + ); if (!apiKey) { logger.error("[MessageRouter] Missing Blooio API key"); @@ -471,12 +502,20 @@ class MessageRouterService { ); // Try org-specific credentials first (from secrets service) - let accessToken = await secretsService.get(params.organizationId, WHATSAPP_ACCESS_TOKEN); - let phoneNumberId = await secretsService.get(params.organizationId, WHATSAPP_PHONE_NUMBER_ID); + let accessToken = await secretsService.get( + params.organizationId, + WHATSAPP_ACCESS_TOKEN, + ); + let phoneNumberId = await secretsService.get( + params.organizationId, + WHATSAPP_PHONE_NUMBER_ID, + ); // Fall back to global config (for eliza-app public bot) if (!accessToken || !phoneNumberId) { - const { elizaAppConfig } = await import("@/lib/services/eliza-app/config"); + const { elizaAppConfig } = await import( + "@/lib/services/eliza-app/config" + ); accessToken = accessToken || elizaAppConfig.whatsapp.accessToken; phoneNumberId = phoneNumberId || elizaAppConfig.whatsapp.phoneNumberId; } @@ -488,7 +527,12 @@ class MessageRouterService { return false; } - await sendWhatsAppMessage(accessToken, phoneNumberId, params.to, params.body); + await sendWhatsAppMessage( + accessToken, + phoneNumberId, + params.to, + params.body, + ); logger.info("[MessageRouter] WhatsApp message sent successfully", { organizationId: params.organizationId, @@ -599,7 +643,8 @@ class MessageRouterService { canVoice?: boolean; }; }): Promise<{ id: string; webhookUrl: string }> { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const webhookUrl = `${baseUrl}/api/webhooks/${params.provider}/${params.organizationId}`; const [record] = await dbWrite diff --git a/packages/lib/services/milady-agent-config.ts b/packages/lib/services/milady-agent-config.ts index ff595ea86..df53b58da 100644 --- a/packages/lib/services/milady-agent-config.ts +++ b/packages/lib/services/milady-agent-config.ts @@ -2,7 +2,8 @@ export const MILADY_INTERNAL_CONFIG_PREFIX = "__milady"; export const MILADY_CHARACTER_OWNERSHIP_KEY = "__miladyCharacterOwnership"; export const MILADY_REUSE_EXISTING_CHARACTER = "reuse-existing"; export const MILADY_MANAGED_DISCORD_KEY = "__miladyManagedDiscord"; -export const MILADY_MANAGED_DISCORD_GATEWAY_KEY = "__miladyManagedDiscordGateway"; +export const MILADY_MANAGED_DISCORD_GATEWAY_KEY = + "__miladyManagedDiscordGateway"; export const MILADY_MANAGED_GITHUB_KEY = "__miladyManagedGithub"; export interface ManagedMiladyDiscordBinding { @@ -25,7 +26,9 @@ function asRecord(value: unknown): Record | null { : null; } -function cloneAgentConfig(agentConfig?: Record | null): Record { +function cloneAgentConfig( + agentConfig?: Record | null, +): Record { return asRecord(agentConfig) ? { ...agentConfig } : {}; } @@ -55,7 +58,10 @@ export function withReusedMiladyCharacterOwnership( export function reusesExistingMiladyCharacter( agentConfig?: Record | null, ): boolean { - return agentConfig?.[MILADY_CHARACTER_OWNERSHIP_KEY] === MILADY_REUSE_EXISTING_CHARACTER; + return ( + agentConfig?.[MILADY_CHARACTER_OWNERSHIP_KEY] === + MILADY_REUSE_EXISTING_CHARACTER + ); } export function readManagedMiladyDiscordBinding( @@ -66,17 +72,32 @@ export function readManagedMiladyDiscordBinding( return null; } - const guildId = typeof binding.guildId === "string" ? binding.guildId.trim() : ""; - const guildName = typeof binding.guildName === "string" ? binding.guildName.trim() : ""; + const guildId = + typeof binding.guildId === "string" ? binding.guildId.trim() : ""; + const guildName = + typeof binding.guildName === "string" ? binding.guildName.trim() : ""; const adminDiscordUserId = - typeof binding.adminDiscordUserId === "string" ? binding.adminDiscordUserId.trim() : ""; + typeof binding.adminDiscordUserId === "string" + ? binding.adminDiscordUserId.trim() + : ""; const adminDiscordUsername = - typeof binding.adminDiscordUsername === "string" ? binding.adminDiscordUsername.trim() : ""; + typeof binding.adminDiscordUsername === "string" + ? binding.adminDiscordUsername.trim() + : ""; const adminElizaUserId = - typeof binding.adminElizaUserId === "string" ? binding.adminElizaUserId.trim() : ""; - const connectedAt = typeof binding.connectedAt === "string" ? binding.connectedAt.trim() : ""; + typeof binding.adminElizaUserId === "string" + ? binding.adminElizaUserId.trim() + : ""; + const connectedAt = + typeof binding.connectedAt === "string" ? binding.connectedAt.trim() : ""; - if (!guildId || !guildName || !adminDiscordUserId || !adminDiscordUsername || !adminElizaUserId) { + if ( + !guildId || + !guildName || + !adminDiscordUserId || + !adminDiscordUsername || + !adminElizaUserId + ) { return null; } @@ -88,14 +109,16 @@ export function readManagedMiladyDiscordBinding( adminDiscordUsername, adminElizaUserId, connectedAt: connectedAt || new Date(0).toISOString(), - ...(typeof binding.applicationId === "string" && binding.applicationId.trim() + ...(typeof binding.applicationId === "string" && + binding.applicationId.trim() ? { applicationId: binding.applicationId.trim() } : {}), ...(typeof binding.adminDiscordDisplayName === "string" && binding.adminDiscordDisplayName.trim() ? { adminDiscordDisplayName: binding.adminDiscordDisplayName.trim() } : {}), - ...(typeof binding.adminDiscordAvatarUrl === "string" && binding.adminDiscordAvatarUrl.trim() + ...(typeof binding.adminDiscordAvatarUrl === "string" && + binding.adminDiscordAvatarUrl.trim() ? { adminDiscordAvatarUrl: binding.adminDiscordAvatarUrl.trim() } : {}), ...(typeof binding.botNickname === "string" && binding.botNickname.trim() @@ -155,7 +178,8 @@ export function readManagedMiladyDiscordGateway( return null; } - const createdAt = typeof gateway.createdAt === "string" ? gateway.createdAt.trim() : ""; + const createdAt = + typeof gateway.createdAt === "string" ? gateway.createdAt.trim() : ""; return { mode: "shared-gateway", @@ -205,13 +229,20 @@ export function readManagedMiladyGithubBinding( return null; } - const connectionId = typeof binding.connectionId === "string" ? binding.connectionId.trim() : ""; - const githubUserId = typeof binding.githubUserId === "string" ? binding.githubUserId.trim() : ""; + const connectionId = + typeof binding.connectionId === "string" ? binding.connectionId.trim() : ""; + const githubUserId = + typeof binding.githubUserId === "string" ? binding.githubUserId.trim() : ""; const githubUsername = - typeof binding.githubUsername === "string" ? binding.githubUsername.trim() : ""; + typeof binding.githubUsername === "string" + ? binding.githubUsername.trim() + : ""; const adminElizaUserId = - typeof binding.adminElizaUserId === "string" ? binding.adminElizaUserId.trim() : ""; - const connectedAt = typeof binding.connectedAt === "string" ? binding.connectedAt.trim() : ""; + typeof binding.adminElizaUserId === "string" + ? binding.adminElizaUserId.trim() + : ""; + const connectedAt = + typeof binding.connectedAt === "string" ? binding.connectedAt.trim() : ""; const mode = binding.mode === "shared-owner" || binding.mode === "cloud-managed" ? binding.mode @@ -239,10 +270,12 @@ export function readManagedMiladyGithubBinding( scopes: Array.isArray(binding.scopes) ? binding.scopes : [], ...(connectionRole ? { connectionRole } : {}), ...(source ? { source } : {}), - ...(typeof binding.githubDisplayName === "string" && binding.githubDisplayName.trim() + ...(typeof binding.githubDisplayName === "string" && + binding.githubDisplayName.trim() ? { githubDisplayName: binding.githubDisplayName.trim() } : {}), - ...(typeof binding.githubAvatarUrl === "string" && binding.githubAvatarUrl.trim() + ...(typeof binding.githubAvatarUrl === "string" && + binding.githubAvatarUrl.trim() ? { githubAvatarUrl: binding.githubAvatarUrl.trim() } : {}), ...(typeof binding.githubEmail === "string" && binding.githubEmail.trim() @@ -264,10 +297,16 @@ export function withManagedMiladyGithubBinding( adminElizaUserId: binding.adminElizaUserId, connectedAt: binding.connectedAt, scopes: binding.scopes, - ...(binding.connectionRole ? { connectionRole: binding.connectionRole } : {}), + ...(binding.connectionRole + ? { connectionRole: binding.connectionRole } + : {}), ...(binding.source ? { source: binding.source } : {}), - ...(binding.githubDisplayName ? { githubDisplayName: binding.githubDisplayName } : {}), - ...(binding.githubAvatarUrl ? { githubAvatarUrl: binding.githubAvatarUrl } : {}), + ...(binding.githubDisplayName + ? { githubDisplayName: binding.githubDisplayName } + : {}), + ...(binding.githubAvatarUrl + ? { githubAvatarUrl: binding.githubAvatarUrl } + : {}), ...(binding.githubEmail ? { githubEmail: binding.githubEmail } : {}), }; return next; diff --git a/packages/lib/services/milady-billing-gate.ts b/packages/lib/services/milady-billing-gate.ts index 4a99d25d4..9ad8db606 100644 --- a/packages/lib/services/milady-billing-gate.ts +++ b/packages/lib/services/milady-billing-gate.ts @@ -21,7 +21,9 @@ export interface CreditGateResult { * Returns `{ allowed: true }` if `credit_balance >= MINIMUM_DEPOSIT`, * otherwise returns a user-facing error message directing them to add funds. */ -export async function checkMiladyCreditGate(organizationId: string): Promise { +export async function checkMiladyCreditGate( + organizationId: string, +): Promise { try { const org = await organizationsRepository.findById(organizationId); if (!org) { diff --git a/packages/lib/services/milady-gateway-relay.ts b/packages/lib/services/milady-gateway-relay.ts index 6a20e4bb4..09a38b0df 100644 --- a/packages/lib/services/milady-gateway-relay.ts +++ b/packages/lib/services/milady-gateway-relay.ts @@ -1,6 +1,9 @@ import { Redis } from "@upstash/redis"; import { randomUUID } from "crypto"; -import type { BridgeRequest, BridgeResponse } from "@/lib/services/milady-sandbox"; +import type { + BridgeRequest, + BridgeResponse, +} from "@/lib/services/milady-sandbox"; import { logger } from "@/lib/utils/logger"; import { assertPersistentCloudStateConfigured } from "@/lib/utils/persistence-guard"; @@ -42,8 +45,13 @@ interface RelaySessionStore { addOwnerSession(ownerKey: string, sessionId: string): Promise; removeOwnerSession(ownerKey: string, sessionId: string): Promise; listOwnerSessionIds(ownerKey: string): Promise; - enqueueRequest(sessionId: string, request: MiladyGatewayRelayRequestEnvelope): Promise; - dequeueRequest(sessionId: string): Promise; + enqueueRequest( + sessionId: string, + request: MiladyGatewayRelayRequestEnvelope, + ): Promise; + dequeueRequest( + sessionId: string, + ): Promise; setResponse(requestId: string, response: BridgeResponse): Promise; getResponse(requestId: string): Promise; deleteResponse(requestId: string): Promise; @@ -70,7 +78,10 @@ function buildResponseKey(requestId: string): string { } function isSessionExpired(session: MiladyGatewayRelaySession): boolean { - return Date.now() - new Date(session.lastSeenAt).getTime() > SESSION_TTL_SECONDS * 1000; + return ( + Date.now() - new Date(session.lastSeenAt).getTime() > + SESSION_TTL_SECONDS * 1000 + ); } function parseJson(value: unknown): T | null { @@ -96,18 +107,26 @@ function getRedisClient(): Redis | null { } const redisUrl = process.env.REDIS_URL || process.env.KV_URL; - const restUrl = process.env.KV_REST_API_URL || process.env.UPSTASH_REDIS_REST_URL; - const restToken = process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN; + const restUrl = + process.env.KV_REST_API_URL || process.env.UPSTASH_REDIS_REST_URL; + const restToken = + process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN; if (redisUrl) { redisClient = Redis.fromEnv(); - logger.info?.("[milady-gateway-relay] Redis relay store initialized (native protocol)"); + logger.info?.( + "[milady-gateway-relay] Redis relay store initialized (native protocol)", + ); } else if (restUrl && restToken) { redisClient = new Redis({ url: restUrl, token: restToken }); - logger.info?.("[milady-gateway-relay] Redis relay store initialized (REST API)"); + logger.info?.( + "[milady-gateway-relay] Redis relay store initialized (REST API)", + ); } else { assertPersistentCloudStateConfigured("milady-gateway-relay", false); - logger.warn?.("[milady-gateway-relay] Redis unavailable, using in-memory relay store"); + logger.warn?.( + "[milady-gateway-relay] Redis unavailable, using in-memory relay store", + ); redisClient = null; } @@ -118,7 +137,10 @@ function getRedisClient(): Redis | null { class InMemoryRelayStore implements RelaySessionStore { private sessions = new Map(); private ownerIndex = new Map>(); - private requestQueues = new Map(); + private requestQueues = new Map< + string, + MiladyGatewayRelayRequestEnvelope[] + >(); private responses = new Map(); private pruneExpiredSessions(): void { @@ -133,7 +155,9 @@ class InMemoryRelayStore implements RelaySessionStore { } } - async getSession(sessionId: string): Promise { + async getSession( + sessionId: string, + ): Promise { this.pruneExpiredSessions(); return this.sessions.get(sessionId) ?? null; } @@ -147,7 +171,9 @@ class InMemoryRelayStore implements RelaySessionStore { this.sessions.delete(sessionId); this.requestQueues.delete(sessionId); if (session) { - this.ownerIndex.get(buildOwnerKey(session.organizationId, session.userId))?.delete(sessionId); + this.ownerIndex + .get(buildOwnerKey(session.organizationId, session.userId)) + ?.delete(sessionId); } } @@ -175,7 +201,9 @@ class InMemoryRelayStore implements RelaySessionStore { this.requestQueues.set(sessionId, queue); } - async dequeueRequest(sessionId: string): Promise { + async dequeueRequest( + sessionId: string, + ): Promise { const queue = this.requestQueues.get(sessionId); if (!queue?.length) { return null; @@ -183,7 +211,10 @@ class InMemoryRelayStore implements RelaySessionStore { return queue.shift() ?? null; } - async setResponse(requestId: string, response: BridgeResponse): Promise { + async setResponse( + requestId: string, + response: BridgeResponse, + ): Promise { this.responses.set(requestId, response); } @@ -199,8 +230,12 @@ class InMemoryRelayStore implements RelaySessionStore { class RedisRelayStore implements RelaySessionStore { constructor(private readonly redis: Redis) {} - async getSession(sessionId: string): Promise { - return parseJson(await this.redis.get(buildSessionKey(sessionId))); + async getSession( + sessionId: string, + ): Promise { + return parseJson( + await this.redis.get(buildSessionKey(sessionId)), + ); } async setSession(session: MiladyGatewayRelaySession): Promise { @@ -238,13 +273,18 @@ class RedisRelayStore implements RelaySessionStore { await this.redis.expire(buildQueueKey(sessionId), REQUEST_TTL_SECONDS); } - async dequeueRequest(sessionId: string): Promise { + async dequeueRequest( + sessionId: string, + ): Promise { return parseJson( await this.redis.lpop(buildQueueKey(sessionId)), ); } - async setResponse(requestId: string, response: BridgeResponse): Promise { + async setResponse( + requestId: string, + response: BridgeResponse, + ): Promise { await this.redis.setex( buildResponseKey(requestId), REQUEST_TTL_SECONDS, @@ -253,7 +293,9 @@ class RedisRelayStore implements RelaySessionStore { } async getResponse(requestId: string): Promise { - return parseJson(await this.redis.get(buildResponseKey(requestId))); + return parseJson( + await this.redis.get(buildResponseKey(requestId)), + ); } async deleteResponse(requestId: string): Promise { @@ -281,7 +323,9 @@ class MiladyGatewayRelayService { return this.store; } - resetForTests(store: RelaySessionStore | null = new InMemoryRelayStore()): void { + resetForTests( + store: RelaySessionStore | null = new InMemoryRelayStore(), + ): void { this.store = store; } @@ -305,7 +349,10 @@ class MiladyGatewayRelayService { }; await store.setSession(session); - await store.addOwnerSession(buildOwnerKey(params.organizationId, params.userId), session.id); + await store.addOwnerSession( + buildOwnerKey(params.organizationId, params.userId), + session.id, + ); logger.info?.("[milady-gateway-relay] Registered local runtime session", { sessionId: session.id, @@ -317,7 +364,9 @@ class MiladyGatewayRelayService { return session; } - async refreshSession(sessionId: string): Promise { + async refreshSession( + sessionId: string, + ): Promise { const store = this.getStore(); const existing = await store.getSession(sessionId); if (!existing) { @@ -348,7 +397,9 @@ class MiladyGatewayRelayService { } } - async getSession(sessionId: string): Promise { + async getSession( + sessionId: string, + ): Promise { const store = this.getStore(); const session = await store.getSession(sessionId); if (!session || isSessionExpired(session)) { @@ -454,7 +505,10 @@ class MiladyGatewayRelayService { return { jsonrpc: "2.0", id: rpc.id, - error: { code: -32000, message: "Local runtime session disconnected" }, + error: { + code: -32000, + message: "Local runtime session disconnected", + }, }; } diff --git a/packages/lib/services/milady-gateway-router.ts b/packages/lib/services/milady-gateway-router.ts index 17e3f3be5..f4f186046 100644 --- a/packages/lib/services/milady-gateway-router.ts +++ b/packages/lib/services/milady-gateway-router.ts @@ -1,5 +1,8 @@ import { createHash, randomUUID } from "crypto"; -import { type MiladySandbox, miladySandboxesRepository } from "@/db/repositories/milady-sandboxes"; +import { + type MiladySandbox, + miladySandboxesRepository, +} from "@/db/repositories/milady-sandboxes"; import { usersRepository } from "@/db/repositories/users"; import { readManagedMiladyDiscordBinding, @@ -9,7 +12,10 @@ import { type MiladyGatewayRelaySession, miladyGatewayRelayService, } from "@/lib/services/milady-gateway-relay"; -import type { BridgeRequest, BridgeResponse } from "@/lib/services/milady-sandbox"; +import type { + BridgeRequest, + BridgeResponse, +} from "@/lib/services/milady-sandbox"; import { miladySandboxService } from "@/lib/services/milady-sandbox"; import { logger } from "@/lib/utils/logger"; import { normalizePhoneNumber } from "@/lib/utils/phone-normalization"; @@ -95,7 +101,9 @@ function hashToUuid(input: string): string { const hex = createHash("sha256").update(input).digest("hex").slice(0, 32); const chars = hex.split(""); chars[12] = "4"; - chars[16] = ((Number.parseInt(chars[16] ?? "0", 16) & 0x3) | 0x8).toString(16); + chars[16] = ((Number.parseInt(chars[16] ?? "0", 16) & 0x3) | 0x8).toString( + 16, + ); return [ chars.slice(0, 8).join(""), chars.slice(8, 12).join(""), @@ -111,7 +119,9 @@ function buildDirectConversationRoomId( a: string, b: string, ): string { - const normalized = [normalizePhoneNumber(a), normalizePhoneNumber(b)].sort().join("-"); + const normalized = [normalizePhoneNumber(a), normalizePhoneNumber(b)] + .sort() + .join("-"); return hashToUuid(`room:${agentId}:${platform}:${normalized}`); } @@ -140,7 +150,10 @@ function extractReplyText(response: BridgeResponse): string | null { } export class MiladyGatewayRouterService { - private async listOwnedSandboxes(orgId: string, userId: string): Promise { + private async listOwnedSandboxes( + orgId: string, + userId: string, + ): Promise { const sandboxes = await miladySandboxesRepository.listByOrganization(orgId); return sandboxes.filter((sandbox) => sandbox.user_id === userId); } @@ -154,7 +167,10 @@ export class MiladyGatewayRouterService { reason?: MiladyGatewayRouteReason; agentId?: string; }> { - const localSessions = await miladyGatewayRelayService.listOwnerSessions(organizationId, userId); + const localSessions = await miladyGatewayRelayService.listOwnerSessions( + organizationId, + userId, + ); if (localSessions.length >= 1) { return { target: { @@ -165,7 +181,8 @@ export class MiladyGatewayRouterService { }; } - const ownedSandboxes = sandboxes ?? (await this.listOwnedSandboxes(organizationId, userId)); + const ownedSandboxes = + sandboxes ?? (await this.listOwnedSandboxes(organizationId, userId)); return chooseSingleSandboxTarget(ownedSandboxes); } @@ -180,28 +197,40 @@ export class MiladyGatewayRouterService { const senderDiscordUserId = args.senderDiscordUserId.trim(); if (args.guildId?.trim()) { - const linkedSandboxes = await miladySandboxesRepository.findByManagedDiscordGuildId( - args.guildId.trim(), - ); + const linkedSandboxes = + await miladySandboxesRepository.findByManagedDiscordGuildId( + args.guildId.trim(), + ); const ownedLinkedSandboxes = linkedSandboxes.filter((sandbox) => { - const binding = readManagedMiladyDiscordBinding(asConfigRecord(sandbox.agent_config)); + const binding = readManagedMiladyDiscordBinding( + asConfigRecord(sandbox.agent_config), + ); return binding?.adminDiscordUserId === senderDiscordUserId; }); if (ownedLinkedSandboxes.length === 0) { return { - reason: linkedSandboxes.length > 0 ? "sender_not_guild_owner" : "not_linked", + reason: + linkedSandboxes.length > 0 + ? "sender_not_guild_owner" + : "not_linked", }; } const directlyBoundSandboxes = ownedLinkedSandboxes.filter( - (sandbox) => !readManagedMiladyDiscordGateway(asConfigRecord(sandbox.agent_config)), + (sandbox) => + !readManagedMiladyDiscordGateway( + asConfigRecord(sandbox.agent_config), + ), ); if (directlyBoundSandboxes.length > 0) { return chooseSingleSandboxTarget(directlyBoundSandboxes); } - const owner = await usersRepository.findByDiscordIdWithOrganization(senderDiscordUserId); + const owner = + await usersRepository.findByDiscordIdWithOrganization( + senderDiscordUserId, + ); if (!owner?.organization_id) { return { reason: "unknown_owner", @@ -211,7 +240,10 @@ export class MiladyGatewayRouterService { return this.resolveOwnedRuntimeTarget(owner.organization_id, owner.id); } - const owner = await usersRepository.findByDiscordIdWithOrganization(senderDiscordUserId); + const owner = + await usersRepository.findByDiscordIdWithOrganization( + senderDiscordUserId, + ); if (!owner) { return { reason: "unknown_owner", @@ -224,17 +256,30 @@ export class MiladyGatewayRouterService { }; } - const sandboxes = await this.listOwnedSandboxes(owner.organization_id, owner.id); + const sandboxes = await this.listOwnedSandboxes( + owner.organization_id, + owner.id, + ); const exactBoundMatches = sandboxes.filter((sandbox) => { - const binding = readManagedMiladyDiscordBinding(asConfigRecord(sandbox.agent_config)); + const binding = readManagedMiladyDiscordBinding( + asConfigRecord(sandbox.agent_config), + ); return binding?.adminDiscordUserId === senderDiscordUserId; }); - const preferred = exactBoundMatches.length > 0 ? exactBoundMatches : sandboxes; - return this.resolveOwnedRuntimeTarget(owner.organization_id, owner.id, preferred); + const preferred = + exactBoundMatches.length > 0 ? exactBoundMatches : sandboxes; + return this.resolveOwnedRuntimeTarget( + owner.organization_id, + owner.id, + preferred, + ); } - private async resolvePhoneTarget(args: { organizationId: string; senderId: string }): Promise<{ + private async resolvePhoneTarget(args: { + organizationId: string; + senderId: string; + }): Promise<{ target?: ResolvedMiladyTarget; reason?: MiladyGatewayRouteReason; agentId?: string; @@ -247,8 +292,12 @@ export class MiladyGatewayRouterService { } const owner = senderId.includes("@") - ? await usersRepository.findByEmailWithOrganization(senderId.toLowerCase()) - : await usersRepository.findByPhoneNumberWithOrganization(normalizePhoneNumber(senderId)); + ? await usersRepository.findByEmailWithOrganization( + senderId.toLowerCase(), + ) + : await usersRepository.findByPhoneNumberWithOrganization( + normalizePhoneNumber(senderId), + ); if (!owner) { return { @@ -256,7 +305,10 @@ export class MiladyGatewayRouterService { }; } - if (!owner.organization_id || owner.organization_id !== args.organizationId) { + if ( + !owner.organization_id || + owner.organization_id !== args.organizationId + ) { return { reason: "owner_org_mismatch", }; @@ -274,7 +326,10 @@ export class MiladyGatewayRouterService { const responses = await Promise.all( sessions.map(async (session) => ({ session, - response: await miladyGatewayRelayService.routeToSession(session, rpc), + response: await miladyGatewayRelayService.routeToSession( + session, + rpc, + ), })), ); @@ -301,7 +356,8 @@ export class MiladyGatewayRouterService { } const primary = - successful.find((entry) => extractReplyText(entry.response) !== null) ?? successful[0]!; + successful.find((entry) => extractReplyText(entry.response) !== null) ?? + successful[0]!; return { handled: true, @@ -381,12 +437,16 @@ export class MiladyGatewayRouterService { sender: { id: args.sender.id, username: args.sender.username, - ...(args.sender.displayName ? { displayName: args.sender.displayName } : {}), + ...(args.sender.displayName + ? { displayName: args.sender.displayName } + : {}), metadata: { discord: { userId: args.sender.id, username: args.sender.username, - ...(args.sender.displayName ? { globalName: args.sender.displayName } : {}), + ...(args.sender.displayName + ? { globalName: args.sender.displayName } + : {}), ...(args.sender.avatar ? { avatar: args.sender.avatar } : {}), }, }, @@ -463,7 +523,9 @@ export class MiladyGatewayRouterService { provider: args.provider, from: normalizedFrom, to: normalizedTo, - ...(args.providerMessageId ? { providerMessageId: args.providerMessageId } : {}), + ...(args.providerMessageId + ? { providerMessageId: args.providerMessageId } + : {}), ...(args.metadata ? args.metadata : {}), }, }, diff --git a/packages/lib/services/milady-github-return.ts b/packages/lib/services/milady-github-return.ts index 7e2c25a05..1c09b2d9b 100644 --- a/packages/lib/services/milady-github-return.ts +++ b/packages/lib/services/milady-github-return.ts @@ -1,7 +1,8 @@ import { NextResponse } from "next/server"; import type { ManagedMiladyGithubMode } from "./milady-agent-config"; -export const LIFEOPS_GITHUB_POST_MESSAGE_TYPE = "milady-lifeops-github-complete"; +export const LIFEOPS_GITHUB_POST_MESSAGE_TYPE = + "milady-lifeops-github-complete"; export interface LifeOpsGithubReturnDetail { target: "owner" | "agent"; @@ -40,7 +41,9 @@ function readMiladyDeepLinkPath(value: string): string | null { } } -export function resolveMiladyReturnUrl(value: string | null | undefined): string | null { +export function resolveMiladyReturnUrl( + value: string | null | undefined, +): string | null { if (!value) { return null; } diff --git a/packages/lib/services/milady-google-connector.ts b/packages/lib/services/milady-google-connector.ts index 9df2da21d..e894200cc 100644 --- a/packages/lib/services/milady-google-connector.ts +++ b/packages/lib/services/milady-google-connector.ts @@ -3,7 +3,10 @@ import { dbRead } from "@/db/client"; import { platformCredentials } from "@/db/schemas/platform-credentials"; import { oauthService } from "@/lib/services/oauth"; import { getPreferredActiveConnection } from "@/lib/services/oauth/oauth-service"; -import { getProvider, isProviderConfigured } from "@/lib/services/oauth/provider-registry"; +import { + getProvider, + isProviderConfigured, +} from "@/lib/services/oauth/provider-registry"; import type { OAuthConnectionRole } from "@/lib/services/oauth/types"; import { applyTimeZone, @@ -12,8 +15,10 @@ import { sanitizeHeaderValue, } from "@/lib/utils/google-mcp-shared"; -const GOOGLE_CALENDAR_EVENTS_ENDPOINT = "https://www.googleapis.com/calendar/v3/calendars"; -const GOOGLE_GMAIL_MESSAGES_ENDPOINT = "https://gmail.googleapis.com/gmail/v1/users/me/messages"; +const GOOGLE_CALENDAR_EVENTS_ENDPOINT = + "https://www.googleapis.com/calendar/v3/calendars"; +const GOOGLE_GMAIL_MESSAGES_ENDPOINT = + "https://gmail.googleapis.com/gmail/v1/users/me/messages"; const GOOGLE_GMAIL_SEND_ENDPOINT = `${GOOGLE_GMAIL_MESSAGES_ENDPOINT}/send`; const DEFAULT_GOOGLE_CONNECTOR_CAPABILITIES = [ "google.basic_identity", @@ -48,7 +53,12 @@ export interface ManagedGoogleConnectorStatus { mode: "cloud_managed"; configured: boolean; connected: boolean; - reason: "connected" | "disconnected" | "config_missing" | "token_missing" | "needs_reauth"; + reason: + | "connected" + | "disconnected" + | "config_missing" + | "token_missing" + | "needs_reauth"; identity: Record | null; grantedCapabilities: MiladyGoogleCapability[]; grantedScopes: string[]; @@ -227,7 +237,9 @@ function normalizeCapabilities( : ["google.basic_identity", ...normalized]; } -function capabilitiesToScopes(capabilities: readonly MiladyGoogleCapability[]): string[] { +function capabilitiesToScopes( + capabilities: readonly MiladyGoogleCapability[], +): string[] { const scopes = new Set([ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", @@ -251,7 +263,9 @@ function capabilitiesToScopes(capabilities: readonly MiladyGoogleCapability[]): return [...scopes]; } -function scopesToCapabilities(scopes: readonly string[]): MiladyGoogleCapability[] { +function scopesToCapabilities( + scopes: readonly string[], +): MiladyGoogleCapability[] { const granted = new Set(scopes); const capabilities: MiladyGoogleCapability[] = []; const hasIdentity = @@ -326,7 +340,11 @@ async function getActiveGoogleConnectionRecord(args: { side: OAuthConnectionRole; }) { const connections = await getScopedGoogleConnections(args); - const activeConnection = getPreferredActiveConnection(connections, args.userId, args.side); + const activeConnection = getPreferredActiveConnection( + connections, + args.userId, + args.side, + ); const latestConnection = connections[0] ?? null; const activeRow = activeConnection ? await getConnectionRow(args.organizationId, activeConnection.id) @@ -430,7 +448,8 @@ function getTimeZoneOffsetMinutes(date: Date, timeZone: string): number { second: "2-digit", hour12: false, }).formatToParts(date); - const token = parts.find((part) => part.type === "timeZoneName")?.value?.trim() ?? "GMT"; + const token = + parts.find((part) => part.type === "timeZoneName")?.value?.trim() ?? "GMT"; if (token === "GMT" || token === "UTC") { return 0; } @@ -450,7 +469,14 @@ function localPartsToEpochMs(parts: { minute: number; second: number; }): number { - return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second); + return Date.UTC( + parts.year, + parts.month - 1, + parts.day, + parts.hour, + parts.minute, + parts.second, + ); } function buildUtcDateFromLocalParts( @@ -537,7 +563,11 @@ function readConferenceLink(event: GoogleCalendarApiEvent): string | null { if (event.hangoutLink?.trim()) { return event.hangoutLink.trim(); } - return event.conferenceData?.entryPoints?.find((entry) => entry.uri?.trim())?.uri?.trim() || null; + return ( + event.conferenceData?.entryPoints + ?.find((entry) => entry.uri?.trim()) + ?.uri?.trim() || null + ); } function normalizeGoogleCalendarEvent( @@ -547,7 +577,10 @@ function normalizeGoogleCalendarEvent( ): ManagedGoogleCalendarEvent | null { const externalId = event.id?.trim(); const start = readGoogleEventInstant(event.start, fallbackTimeZone); - const end = readGoogleEventInstant(event.end, start?.timeZone ?? fallbackTimeZone); + const end = readGoogleEventInstant( + event.end, + start?.timeZone ?? fallbackTimeZone, + ); if (!externalId || !start || !end) { return null; } @@ -636,7 +669,10 @@ function stripQuotedDisplayName(value: string): string { return trimmed; } -function parseMailbox(value: string): { display: string; email: string | null } { +function parseMailbox(value: string): { + display: string; + email: string | null; +} { const trimmed = value.trim(); const match = trimmed.match(/^(.*?)(?:<([^>]+)>)$/); if (match) { @@ -672,7 +708,9 @@ function readHeaderValue( name: string, ): string | undefined { const lowerName = name.toLowerCase(); - const header = headers?.find((candidate) => candidate.name?.trim().toLowerCase() === lowerName); + const header = headers?.find( + (candidate) => candidate.name?.trim().toLowerCase() === lowerName, + ); const value = header?.value?.trim(); return value && value.length > 0 ? value : undefined; } @@ -733,20 +771,28 @@ function classifyReplyNeed(args: { listId: string | undefined; autoSubmitted: string | undefined; }) { - const labels = new Set(args.labels.map((label) => label.trim().toUpperCase())); + const labels = new Set( + args.labels.map((label) => label.trim().toUpperCase()), + ); const isUnread = labels.has("UNREAD"); const explicitlyImportant = labels.has("IMPORTANT"); const selfEmail = args.selfEmail?.trim().toLowerCase() || null; const fromEmail = args.fromEmail?.trim().toLowerCase() || null; - const directRecipients = [...args.to, ...args.cc].map((entry) => entry.trim().toLowerCase()); - const directlyAddressed = selfEmail ? directRecipients.includes(selfEmail) : false; + const directRecipients = [...args.to, ...args.cc].map((entry) => + entry.trim().toLowerCase(), + ); + const directlyAddressed = selfEmail + ? directRecipients.includes(selfEmail) + : false; const fromSelf = Boolean(selfEmail && fromEmail && selfEmail === fromEmail); const precedence = args.precedence?.trim().toLowerCase(); const autoSubmitted = args.autoSubmitted?.trim().toLowerCase(); const automated = Boolean( fromEmail && - /(?:^|\b)(?:no-?reply|donotreply|notifications?|mailer-daemon)(?:\b|@)/i.test(fromEmail), + /(?:^|\b)(?:no-?reply|donotreply|notifications?|mailer-daemon)(?:\b|@)/i.test( + fromEmail, + ), ) || Boolean(args.listId) || precedence === "bulk" || @@ -784,7 +830,9 @@ function classifyReplyNeed(args: { return { likelyReplyNeeded: !automated && !fromSelf && isUnread && directlyAddressed, - isImportant: explicitlyImportant || (!automated && !fromSelf && isUnread && directlyAddressed), + isImportant: + explicitlyImportant || + (!automated && !fromSelf && isUnread && directlyAddressed), triageScore: Math.max(0, triageScore), triageReason: reasons.join(", ") || "recent inbox message", }; @@ -812,7 +860,9 @@ function normalizeGoogleGmailMessage( const cc = parseMailboxList(readHeaderValue(headers, "Cc")).map( (entry) => entry.email || entry.display, ); - const labels = (message.labelIds ?? []).map((label) => label.trim()).filter(Boolean); + const labels = (message.labelIds ?? []) + .map((label) => label.trim()) + .filter(Boolean); const receivedAtMs = Number(message.internalDate); const receivedAt = Number.isFinite(receivedAtMs) ? new Date(receivedAtMs).toISOString() @@ -851,7 +901,8 @@ function normalizeGoogleGmailMessage( htmlLink: deriveHtmlLink(threadId), metadata: { historyId: message.historyId?.trim() || null, - sizeEstimate: typeof message.sizeEstimate === "number" ? message.sizeEstimate : null, + sizeEstimate: + typeof message.sizeEstimate === "number" ? message.sizeEstimate : null, dateHeader: readHeaderValue(headers, "Date") || null, messageIdHeader: readHeaderValue(headers, "Message-Id") || null, referencesHeader: readHeaderValue(headers, "References") || null, @@ -872,7 +923,9 @@ function normalizeReplySubject(subject: string): string { function shapeConnectedStatus( side: OAuthConnectionRole, - connection: NonNullable>[number]>, + connection: NonNullable< + Awaited>[number] + >, row: GoogleConnectionRow | null, ): ManagedGoogleConnectorStatus { const connected = connection.status === "active"; @@ -905,7 +958,10 @@ function shapeConnectedStatus( }; } -function emptyStatus(side: OAuthConnectionRole, configured: boolean): ManagedGoogleConnectorStatus { +function emptyStatus( + side: OAuthConnectionRole, + configured: boolean, +): ManagedGoogleConnectorStatus { return { provider: "google", side, @@ -960,7 +1016,9 @@ export async function listManagedGoogleConnectorAccounts(args: { return []; } - const sides: OAuthConnectionRole[] = args.side ? [args.side] : ["owner", "agent"]; + const sides: OAuthConnectionRole[] = args.side + ? [args.side] + : ["owner", "agent"]; const results: ManagedGoogleConnectorStatus[] = []; for (const side of sides) { @@ -1035,7 +1093,11 @@ export async function fetchManagedGoogleCalendarFeed(args: { timeMin: string; timeMax: string; timeZone: string; -}): Promise<{ calendarId: string; events: ManagedGoogleCalendarEvent[]; syncedAt: string }> { +}): Promise<{ + calendarId: string; + events: ManagedGoogleCalendarEvent[]; + syncedAt: string; +}> { const params = new URLSearchParams({ singleEvents: "true", orderBy: "startTime", @@ -1054,11 +1116,15 @@ export async function fetchManagedGoogleCalendarFeed(args: { side: args.side, url: `${GOOGLE_CALENDAR_EVENTS_ENDPOINT}/${encodeURIComponent(args.calendarId)}/events?${params.toString()}`, }); - const parsed = (await response.json()) as { items?: GoogleCalendarApiEvent[] }; + const parsed = (await response.json()) as { + items?: GoogleCalendarApiEvent[]; + }; return { calendarId: args.calendarId, events: (parsed.items ?? []) - .map((event) => normalizeGoogleCalendarEvent(args.calendarId, event, args.timeZone)) + .map((event) => + normalizeGoogleCalendarEvent(args.calendarId, event, args.timeZone), + ) .filter((event): event is ManagedGoogleCalendarEvent => event !== null), syncedAt: new Date().toISOString(), }; @@ -1100,7 +1166,11 @@ export async function createManagedGoogleCalendarEvent(args: { }, }); const parsed = (await response.json()) as GoogleCalendarApiEvent; - const event = normalizeGoogleCalendarEvent(args.calendarId, parsed, args.timeZone); + const event = normalizeGoogleCalendarEvent( + args.calendarId, + parsed, + args.timeZone, + ); if (!event) { fail(502, "Google Calendar returned an incomplete event payload."); } @@ -1175,7 +1245,11 @@ async function fetchManagedGoogleCalendarEvent(args: { url: `${GOOGLE_CALENDAR_EVENTS_ENDPOINT}/${encodeURIComponent(args.calendarId)}/events/${encodeURIComponent(args.eventId)}?${params.toString()}`, }); const parsed = (await response.json()) as GoogleCalendarApiEvent; - return normalizeGoogleCalendarEvent(args.calendarId, parsed, args.fallbackTimeZone); + return normalizeGoogleCalendarEvent( + args.calendarId, + parsed, + args.fallbackTimeZone, + ); } export async function updateManagedGoogleCalendarEvent(args: { @@ -1198,7 +1272,8 @@ export async function updateManagedGoogleCalendarEvent(args: { }): Promise<{ event: ManagedGoogleCalendarEvent }> { const ONE_HOUR_MS = 60 * 60 * 1000; const needsExistingEventContext = - Boolean(args.startAt || args.endAt) && (!args.timeZone || !args.startAt || !args.endAt); + Boolean(args.startAt || args.endAt) && + (!args.timeZone || !args.startAt || !args.endAt); const existingEvent = needsExistingEventContext ? await fetchManagedGoogleCalendarEvent({ organizationId: args.organizationId, @@ -1209,7 +1284,8 @@ export async function updateManagedGoogleCalendarEvent(args: { fallbackTimeZone: args.timeZone, }) : null; - const effectiveTimeZone = args.timeZone ?? existingEvent?.timezone ?? undefined; + const effectiveTimeZone = + args.timeZone ?? existingEvent?.timezone ?? undefined; let normalizedStartAt = normalizeManagedCalendarDateTimeInTimeZone( args.startAt, "startAt", @@ -1250,18 +1326,26 @@ export async function updateManagedGoogleCalendarEvent(args: { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...(args.title !== undefined ? { summary: args.title } : {}), - ...(args.description !== undefined ? { description: args.description } : {}), + ...(args.description !== undefined + ? { description: args.description } + : {}), ...(args.location !== undefined ? { location: args.location } : {}), ...(normalizedStartAt ? { start: applyTimeZone(normalizedStartAt, effectiveTimeZone) } : {}), - ...(normalizedEndAt ? { end: applyTimeZone(normalizedEndAt, effectiveTimeZone) } : {}), + ...(normalizedEndAt + ? { end: applyTimeZone(normalizedEndAt, effectiveTimeZone) } + : {}), ...(args.attendees !== undefined ? { attendees: args.attendees } : {}), }), }, }); const parsed = (await response.json()) as GoogleCalendarApiEvent; - const event = normalizeGoogleCalendarEvent(args.calendarId, parsed, effectiveTimeZone); + const event = normalizeGoogleCalendarEvent( + args.calendarId, + parsed, + effectiveTimeZone, + ); if (!event) { fail(502, "Google Calendar returned an incomplete event payload."); } @@ -1334,7 +1418,9 @@ async function fetchManagedGoogleGmailMessages(args: { }), ); - return messages.filter((message): message is ManagedGoogleGmailMessage => message !== null); + return messages.filter( + (message): message is ManagedGoogleGmailMessage => message !== null, + ); } export async function fetchManagedGoogleGmailTriage(args: { @@ -1350,7 +1436,8 @@ export async function fetchManagedGoogleGmailTriage(args: { side: args.side, }); const selfEmail = - connectorStatus.identity && typeof connectorStatus.identity.email === "string" + connectorStatus.identity && + typeof connectorStatus.identity.email === "string" ? connectorStatus.identity.email : null; const messages = await fetchManagedGoogleGmailMessages({ @@ -1406,7 +1493,8 @@ export async function fetchManagedGoogleGmailSearch(args: { ); } const selfEmail = - connectorStatus.identity && typeof connectorStatus.identity.email === "string" + connectorStatus.identity && + typeof connectorStatus.identity.email === "string" ? connectorStatus.identity.email : null; @@ -1441,7 +1529,8 @@ export async function readManagedGoogleGmailMessage(args: { ); } const selfEmail = - connectorStatus.identity && typeof connectorStatus.identity.email === "string" + connectorStatus.identity && + typeof connectorStatus.identity.email === "string" ? connectorStatus.identity.email : null; const response = await googleFetch({ @@ -1477,12 +1566,18 @@ export async function sendManagedGoogleReply(args: { }): Promise { const lines = [ `To: ${sanitizeHeaderValue(args.to.join(", "))}`, - ...(args.cc && args.cc.length > 0 ? [`Cc: ${sanitizeHeaderValue(args.cc.join(", "))}`] : []), + ...(args.cc && args.cc.length > 0 + ? [`Cc: ${sanitizeHeaderValue(args.cc.join(", "))}`] + : []), `Subject: ${sanitizeHeaderValue(normalizeReplySubject(args.subject))}`, "MIME-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", - ...(args.inReplyTo ? [`In-Reply-To: ${sanitizeHeaderValue(args.inReplyTo)}`] : []), - ...(args.references ? [`References: ${sanitizeHeaderValue(args.references)}`] : []), + ...(args.inReplyTo + ? [`In-Reply-To: ${sanitizeHeaderValue(args.inReplyTo)}`] + : []), + ...(args.references + ? [`References: ${sanitizeHeaderValue(args.references)}`] + : []), "", args.bodyText.replace(/\r?\n/g, "\r\n"), ]; @@ -1513,7 +1608,9 @@ export async function sendManagedGoogleMessage(args: { }): Promise { const lines = [ `To: ${sanitizeHeaderValue(args.to.join(", "))}`, - ...(args.cc && args.cc.length > 0 ? [`Cc: ${sanitizeHeaderValue(args.cc.join(", "))}`] : []), + ...(args.cc && args.cc.length > 0 + ? [`Cc: ${sanitizeHeaderValue(args.cc.join(", "))}`] + : []), ...(args.bcc && args.bcc.length > 0 ? [`Bcc: ${sanitizeHeaderValue(args.bcc.join(", "))}`] : []), diff --git a/packages/lib/services/milady-managed-discord.ts b/packages/lib/services/milady-managed-discord.ts index e807397ab..b43228771 100644 --- a/packages/lib/services/milady-managed-discord.ts +++ b/packages/lib/services/milady-managed-discord.ts @@ -11,7 +11,8 @@ import { } from "./milady-agent-config"; const DISCORD_OWNER_USER_IDS_ENV_KEY = "MILADY_DISCORD_OWNER_USER_IDS_JSON"; -export const DISCORD_DEVELOPER_PORTAL_URL = "https://discord.com/developers/applications"; +export const DISCORD_DEVELOPER_PORTAL_URL = + "https://discord.com/developers/applications"; export const MANAGED_DISCORD_GATEWAY_AGENT_NAME = "Milady Discord Gateway"; function asRecord(value: unknown): Record | null { @@ -20,7 +21,10 @@ function asRecord(value: unknown): Record | null { : null; } -function ensureRecord(parent: Record, key: string): Record { +function ensureRecord( + parent: Record, + key: string, +): Record { const existing = asRecord(parent[key]); if (existing) { return existing; @@ -131,11 +135,16 @@ function toStatus( } export class ManagedMiladyDiscordService { - async ensureGatewayAgent(params: { organizationId: string; userId: string }): Promise<{ + async ensureGatewayAgent(params: { + organizationId: string; + userId: string; + }): Promise<{ created: boolean; sandbox: Awaited>; }> { - const sandboxes = await miladySandboxesRepository.listByOrganization(params.organizationId); + const sandboxes = await miladySandboxesRepository.listByOrganization( + params.organizationId, + ); const existingGateway = sandboxes.find((sandbox) => readManagedMiladyDiscordGateway( (sandbox.agent_config as Record | null) ?? {}, @@ -196,10 +205,13 @@ export class ManagedMiladyDiscordService { organizationId: string; binding: ManagedMiladyDiscordBinding; }): Promise<{ restarted: boolean; status: ManagedMiladyDiscordStatus }> { - const conflictingGuildLinks = await miladySandboxesRepository.findByManagedDiscordGuildId( - params.binding.guildId, + const conflictingGuildLinks = + await miladySandboxesRepository.findByManagedDiscordGuildId( + params.binding.guildId, + ); + const conflict = conflictingGuildLinks.find( + (sandbox) => sandbox.id !== params.agentId, ); - const conflict = conflictingGuildLinks.find((sandbox) => sandbox.id !== params.agentId); if (conflict) { throw new Error("Discord server is already linked to another agent"); } @@ -216,8 +228,14 @@ export class ManagedMiladyDiscordService { (sandbox.agent_config as Record | null) ?? {}, params.binding, ); - nextConfig = withDiscordConnectorAdmin(nextConfig, params.binding.adminDiscordUserId); - nextConfig = withDiscordOwnerIdentity(nextConfig, params.binding.adminDiscordUserId); + nextConfig = withDiscordConnectorAdmin( + nextConfig, + params.binding.adminDiscordUserId, + ); + nextConfig = withDiscordOwnerIdentity( + nextConfig, + params.binding.adminDiscordUserId, + ); await miladySandboxesRepository.update(sandbox.id, { agent_config: nextConfig, @@ -225,12 +243,18 @@ export class ManagedMiladyDiscordService { let restarted = false; if (sandbox.status === "running") { - const shutdown = await miladySandboxService.shutdown(sandbox.id, params.organizationId); + const shutdown = await miladySandboxService.shutdown( + sandbox.id, + params.organizationId, + ); if (!shutdown.success) { throw new Error(shutdown.error || "Failed to restart agent"); } - const provision = await miladySandboxService.provision(sandbox.id, params.organizationId); + const provision = await miladySandboxService.provision( + sandbox.id, + params.organizationId, + ); if (!provision.success) { throw new Error(provision.error || "Failed to restart agent"); } @@ -277,23 +301,32 @@ export class ManagedMiladyDiscordService { let restarted = false; if (sandbox.status === "running") { - const shutdown = await miladySandboxService.shutdown(sandbox.id, params.organizationId); + const shutdown = await miladySandboxService.shutdown( + sandbox.id, + params.organizationId, + ); if (!shutdown.success) { throw new Error(shutdown.error || "Failed to restart agent"); } - const provision = await miladySandboxService.provision(sandbox.id, params.organizationId); + const provision = await miladySandboxService.provision( + sandbox.id, + params.organizationId, + ); if (!provision.success) { throw new Error(provision.error || "Failed to restart agent"); } restarted = true; } - logger.info("[managed-discord] Unlinked Discord from managed Milady agent", { - agentId: sandbox.id, - organizationId: params.organizationId, - restarted, - }); + logger.info( + "[managed-discord] Unlinked Discord from managed Milady agent", + { + agentId: sandbox.id, + organizationId: params.organizationId, + restarted, + }, + ); return { restarted, diff --git a/packages/lib/services/milady-managed-github.ts b/packages/lib/services/milady-managed-github.ts index ea57b3509..14c6ef161 100644 --- a/packages/lib/services/milady-managed-github.ts +++ b/packages/lib/services/milady-managed-github.ts @@ -52,7 +52,9 @@ function toStatus( } function isGithubOAuthConfigured(): boolean { - return Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); + return Boolean( + process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET, + ); } export class ManagedMiladyGithubService { @@ -98,12 +100,18 @@ export class ManagedMiladyGithubService { let restarted = false; if (sandbox.status === "running") { - const shutdown = await miladySandboxService.shutdown(sandbox.id, params.organizationId); + const shutdown = await miladySandboxService.shutdown( + sandbox.id, + params.organizationId, + ); if (!shutdown.success) { throw new Error(shutdown.error || "Failed to restart agent"); } - const provision = await miladySandboxService.provision(sandbox.id, params.organizationId); + const provision = await miladySandboxService.provision( + sandbox.id, + params.organizationId, + ); if (!provision.success) { throw new Error(provision.error || "Failed to restart agent"); } @@ -135,21 +143,28 @@ export class ManagedMiladyGithubService { throw new Error("Agent not found"); } - const currentConfig = (sandbox.agent_config as Record | null) ?? {}; + const currentConfig = + (sandbox.agent_config as Record | null) ?? {}; const currentBinding = readManagedMiladyGithubBinding(currentConfig); // Revoke the OAuth connection if it exists - if (currentBinding?.connectionId && currentBinding.mode !== "shared-owner") { + if ( + currentBinding?.connectionId && + currentBinding.mode !== "shared-owner" + ) { try { await oauthService.revokeConnection({ organizationId: params.organizationId, connectionId: currentBinding.connectionId, }); } catch (error) { - logger.warn("[managed-github] Failed to revoke OAuth connection during disconnect", { - connectionId: currentBinding.connectionId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[managed-github] Failed to revoke OAuth connection during disconnect", + { + connectionId: currentBinding.connectionId, + error: error instanceof Error ? error.message : String(error), + }, + ); } } @@ -161,12 +176,18 @@ export class ManagedMiladyGithubService { let restarted = false; if (sandbox.status === "running") { - const shutdown = await miladySandboxService.shutdown(sandbox.id, params.organizationId); + const shutdown = await miladySandboxService.shutdown( + sandbox.id, + params.organizationId, + ); if (!shutdown.success) { throw new Error(shutdown.error || "Failed to restart agent"); } - const provision = await miladySandboxService.provision(sandbox.id, params.organizationId); + const provision = await miladySandboxService.provision( + sandbox.id, + params.organizationId, + ); if (!provision.success) { throw new Error(provision.error || "Failed to restart agent"); } diff --git a/packages/lib/services/milady-managed-launch.ts b/packages/lib/services/milady-managed-launch.ts index 27f2c26e2..27a7a26c3 100644 --- a/packages/lib/services/milady-managed-launch.ts +++ b/packages/lib/services/milady-managed-launch.ts @@ -96,7 +96,11 @@ async function ensureManagedOnboarding( token: string, userApiKey: string, ): Promise { - const statusResponse = await requestManagedAgent(apiBase, token, "/api/onboarding/status"); + const statusResponse = await requestManagedAgent( + apiBase, + token, + "/api/onboarding/status", + ); if (!statusResponse.ok) { throw new ManagedMiladyLaunchError( @@ -105,7 +109,9 @@ async function ensureManagedOnboarding( ); } - const onboardingStatus = (await statusResponse.json()) as { complete?: boolean }; + const onboardingStatus = (await statusResponse.json()) as { + complete?: boolean; + }; if (onboardingStatus.complete) { return; } @@ -127,10 +133,15 @@ async function ensureManagedOnboarding( ], }; - const onboardingResponse = await requestManagedAgent(apiBase, token, "/api/onboarding", { - method: "POST", - body: JSON.stringify(onboardingBody), - }); + const onboardingResponse = await requestManagedAgent( + apiBase, + token, + "/api/onboarding", + { + method: "POST", + body: JSON.stringify(onboardingBody), + }, + ); if (!onboardingResponse.ok) { const text = await onboardingResponse.text().catch(() => ""); @@ -145,10 +156,13 @@ async function ensureManagedOnboarding( await requestManagedAgent(apiBase, token, "/api/agent/restart", { method: "POST", }).catch((error) => { - logger.warn("[milady-managed-launch] Agent restart after onboarding failed", { - agentId: sandbox.id, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[milady-managed-launch] Agent restart after onboarding failed", + { + agentId: sandbox.id, + error: error instanceof Error ? error.message : String(error), + }, + ); }); } @@ -173,7 +187,10 @@ export async function launchManagedMiladyAgent(params: { organizationId: string; userId: string; }): Promise { - let sandbox = await miladySandboxService.getAgent(params.agentId, params.organizationId); + let sandbox = await miladySandboxService.getAgent( + params.agentId, + params.organizationId, + ); if (!sandbox) { throw new ManagedMiladyLaunchError("Agent not found", 404); } @@ -194,19 +211,29 @@ export async function launchManagedMiladyAgent(params: { }; if (sandbox.status === "running") { - const shutdownResult = await miladySandboxService.shutdown(sandbox.id, params.organizationId); + const shutdownResult = await miladySandboxService.shutdown( + sandbox.id, + params.organizationId, + ); if (!shutdownResult.success) { throw new ManagedMiladyLaunchError( shutdownResult.error || "Failed to refresh sandbox environment", shutdownResult.error === "Agent not found" ? 404 : 409, ); } - sandbox = (await miladySandboxService.getAgent(sandbox.id, params.organizationId)) ?? sandbox; + sandbox = + (await miladySandboxService.getAgent( + sandbox.id, + params.organizationId, + )) ?? sandbox; } } if (sandbox.status !== "running" || !sandbox.health_url) { - const provisionResult = await miladySandboxService.provision(sandbox.id, params.organizationId); + const provisionResult = await miladySandboxService.provision( + sandbox.id, + params.organizationId, + ); if (!provisionResult.success) { throw new ManagedMiladyLaunchError( diff --git a/packages/lib/services/milady-provision-lock.ts b/packages/lib/services/milady-provision-lock.ts index e6802ab71..ed1d7e88c 100644 --- a/packages/lib/services/milady-provision-lock.ts +++ b/packages/lib/services/milady-provision-lock.ts @@ -6,6 +6,9 @@ import { sql } from "drizzle-orm"; * Use the two-key form instead of hashing a concatenated string into a single * int4 so the lock space is effectively 64 bits (two independent 32-bit keys). */ -export function miladyProvisionAdvisoryLockSql(organizationId: string, agentId: string) { +export function miladyProvisionAdvisoryLockSql( + organizationId: string, + agentId: string, +) { return sql`SELECT pg_advisory_xact_lock(hashtext(${organizationId}), hashtext(${agentId}))`; } diff --git a/packages/lib/services/milady-sandbox.ts b/packages/lib/services/milady-sandbox.ts index f508cd584..ffe0dde71 100644 --- a/packages/lib/services/milady-sandbox.ts +++ b/packages/lib/services/milady-sandbox.ts @@ -30,7 +30,10 @@ import { prepareManagedMiladyEnvironment } from "./managed-milady-env"; import { miladyProvisionAdvisoryLockSql } from "./milady-provision-lock"; import { getNeonClient, NeonClientError } from "./neon-client"; import { JOB_TYPES } from "./provisioning-jobs"; -import { createSandboxProvider, type SandboxProvider } from "./sandbox-provider"; +import { + createSandboxProvider, + type SandboxProvider, +} from "./sandbox-provider"; /** Shared Neon project used as branch parent for per-agent databases. */ const NEON_PARENT_PROJECT_ID: string = process.env.NEON_PARENT_PROJECT_ID ?? ""; @@ -151,14 +154,21 @@ export class MiladySandboxService { return miladySandboxesRepository.listByOrganization(orgId); } - async deleteAgent(agentId: string, orgId: string): Promise { + async deleteAgent( + agentId: string, + orgId: string, + ): Promise { return dbWrite.transaction(async (tx) => { await this.lockLifecycle(tx, agentId, orgId); const rec = await this.getAgentForLifecycleMutation(tx, agentId, orgId); if (!rec) return { success: false, error: "Agent not found" } as const; - const hasActiveProvisionJob = await this.hasActiveProvisionJobTx(tx, agentId, orgId); + const hasActiveProvisionJob = await this.hasActiveProvisionJobTx( + tx, + agentId, + orgId, + ); if (rec.status === "provisioning" || hasActiveProvisionJob) { return { success: false, @@ -189,11 +199,14 @@ export class MiladySandboxService { } as const; } - logger.info("[milady-sandbox] Sandbox already absent during delete cleanup", { - sandboxId: rec.sandbox_id, - status: rec.status, - error: errorMessage, - }); + logger.info( + "[milady-sandbox] Sandbox already absent during delete cleanup", + { + sandboxId: rec.sandbox_id, + status: rec.status, + error: errorMessage, + }, + ); } } if (rec.neon_project_id) { @@ -230,7 +243,8 @@ export class MiladySandboxService { async provision(agentId: string, orgId: string): Promise { let rec = await miladySandboxesRepository.findByIdAndOrg(agentId, orgId); - if (!rec) return { success: false, error: "Agent not found" } as ProvisionResult; + if (!rec) + return { success: false, error: "Agent not found" } as ProvisionResult; const lock = await miladySandboxesRepository.trySetProvisioning(rec.id); if (!lock) { @@ -262,7 +276,10 @@ export class MiladySandboxService { } dbUri = db.connectionUri!; // Neon provision updates DB but doesn't return the full record; re-fetch to avoid stale data - const refreshed = await miladySandboxesRepository.findByIdAndOrg(agentId, orgId); + const refreshed = await miladySandboxesRepository.findByIdAndOrg( + agentId, + orgId, + ); if (refreshed) { rec = refreshed; } @@ -331,12 +348,18 @@ export class MiladySandboxService { // 4. Restore from backup const backup = await miladySandboxesRepository.getLatestBackup(rec.id); if (backup) - await this.pushState(handle.bridgeUrl, backup.state_data as MiladyBackupStateData, { - trusted: true, - }); + await this.pushState( + handle.bridgeUrl, + backup.state_data as MiladyBackupStateData, + { + trusted: true, + }, + ); // 5. Mark running + persist provider-specific metadata - const updateData: Parameters[1] = { + const updateData: Parameters< + typeof miladySandboxesRepository.update + >[1] = { status: "running", sandbox_id: handle.sandboxId, bridge_url: handle.bridgeUrl, @@ -347,16 +370,25 @@ export class MiladySandboxService { // For docker provider, persist docker-specific fields from typed metadata if (handle.metadata?.provider === "docker") { - const dockerMeta = handle.metadata as unknown as DockerSandboxMetadata; + const dockerMeta = + handle.metadata as unknown as DockerSandboxMetadata; if (dockerMeta.nodeId) updateData.node_id = dockerMeta.nodeId; - if (dockerMeta.containerName) updateData.container_name = dockerMeta.containerName; - if (dockerMeta.bridgePort) updateData.bridge_port = dockerMeta.bridgePort; - if (dockerMeta.webUiPort) updateData.web_ui_port = dockerMeta.webUiPort; - if (dockerMeta.headscaleIp) updateData.headscale_ip = dockerMeta.headscaleIp; - if (dockerMeta.dockerImage) updateData.docker_image = dockerMeta.dockerImage; + if (dockerMeta.containerName) + updateData.container_name = dockerMeta.containerName; + if (dockerMeta.bridgePort) + updateData.bridge_port = dockerMeta.bridgePort; + if (dockerMeta.webUiPort) + updateData.web_ui_port = dockerMeta.webUiPort; + if (dockerMeta.headscaleIp) + updateData.headscale_ip = dockerMeta.headscaleIp; + if (dockerMeta.dockerImage) + updateData.docker_image = dockerMeta.dockerImage; } - const updated = await miladySandboxesRepository.update(rec.id, updateData); + const updated = await miladySandboxesRepository.update( + rec.id, + updateData, + ); logger.info("[milady-sandbox] Provisioned", { agentId: rec.id, @@ -375,19 +407,25 @@ export class MiladySandboxService { const msg = err instanceof Error ? err.message : String(err); lastError = msg; - logger.warn("[milady-sandbox] Post-create failure, cleaning up container", { - agentId: rec.id, - sandboxId: handle.sandboxId, - attempt, - error: msg, - }); - - await (await this.getProvider()).stop(handle.sandboxId).catch((stopErr) => { - logger.error("[milady-sandbox] Ghost container cleanup failed", { + logger.warn( + "[milady-sandbox] Post-create failure, cleaning up container", + { + agentId: rec.id, sandboxId: handle.sandboxId, - error: stopErr instanceof Error ? stopErr.message : String(stopErr), + attempt, + error: msg, + }, + ); + + await (await this.getProvider()) + .stop(handle.sandboxId) + .catch((stopErr) => { + logger.error("[milady-sandbox] Ghost container cleanup failed", { + sandboxId: handle.sandboxId, + error: + stopErr instanceof Error ? stopErr.message : String(stopErr), + }); }); - }); // Check if it's a unique constraint error (port collision) -> retry const isUniqueConstraintError = @@ -424,7 +462,11 @@ export class MiladySandboxService { sandboxOrBridgeUrl: | Pick< MiladySandbox, - "bridge_url" | "node_id" | "bridge_port" | "headscale_ip" | "sandbox_id" + | "bridge_url" + | "node_id" + | "bridge_port" + | "headscale_ip" + | "sandbox_id" > | string, path: string, @@ -435,14 +477,22 @@ export class MiladySandboxService { return new URL(path, sandboxOrBridgeUrl).toString(); } - return (await assertSafeOutboundUrl(new URL(path, sandboxOrBridgeUrl).toString())).toString(); + return ( + await assertSafeOutboundUrl( + new URL(path, sandboxOrBridgeUrl).toString(), + ) + ).toString(); } - const dockerBridgeBaseUrl = await this.getTrustedDockerBridgeBaseUrl(sandboxOrBridgeUrl); + const dockerBridgeBaseUrl = + await this.getTrustedDockerBridgeBaseUrl(sandboxOrBridgeUrl); if ( dockerBridgeBaseUrl && sandboxOrBridgeUrl.bridge_url && - this.matchesTrustedDockerBridge(sandboxOrBridgeUrl.bridge_url, dockerBridgeBaseUrl) + this.matchesTrustedDockerBridge( + sandboxOrBridgeUrl.bridge_url, + dockerBridgeBaseUrl, + ) ) { return new URL(path, dockerBridgeBaseUrl).toString(); } @@ -456,7 +506,9 @@ export class MiladySandboxService { } return ( - await assertSafeOutboundUrl(new URL(path, sandboxOrBridgeUrl.bridge_url).toString()) + await assertSafeOutboundUrl( + new URL(path, sandboxOrBridgeUrl.bridge_url).toString(), + ) ).toString(); } @@ -468,7 +520,8 @@ export class MiladySandboxService { } const host = - sandbox.headscale_ip || (await dockerNodesRepository.findByNodeId(sandbox.node_id))?.hostname; + sandbox.headscale_ip || + (await dockerNodesRepository.findByNodeId(sandbox.node_id))?.hostname; if (!host) { return null; } @@ -493,7 +546,10 @@ export class MiladySandboxService { return false; } - if (candidate.protocol !== "http:" || !this.isMiladyPrivateBridgeHost(candidate.hostname)) { + if ( + candidate.protocol !== "http:" || + !this.isMiladyPrivateBridgeHost(candidate.hostname) + ) { return false; } @@ -515,8 +571,12 @@ export class MiladySandboxService { ); } - private isLegacyDockerSandboxId(sandboxId: string | null | undefined): boolean { - return typeof sandboxId === "string" && /^milady-[0-9a-f-]{36}$/i.test(sandboxId); + private isLegacyDockerSandboxId( + sandboxId: string | null | undefined, + ): boolean { + return ( + typeof sandboxId === "string" && /^milady-[0-9a-f-]{36}$/i.test(sandboxId) + ); } private isMiladyPrivateBridgeHost(hostname: string): boolean { @@ -524,7 +584,9 @@ export class MiladySandboxService { return false; } - const [first, second] = hostname.split(".").map((part) => Number.parseInt(part, 10)); + const [first, second] = hostname + .split(".") + .map((part) => Number.parseInt(part, 10)); // CGNAT (100.64.0.0/10) if (first === 100 && second >= 64 && second <= 127) return true; // RFC1918: 10.0.0.0/8 @@ -551,8 +613,15 @@ export class MiladySandboxService { // Bridge - async bridge(agentId: string, orgId: string, rpc: BridgeRequest): Promise { - const rec = await miladySandboxesRepository.findRunningSandbox(agentId, orgId); + async bridge( + agentId: string, + orgId: string, + rpc: BridgeRequest, + ): Promise { + const rec = await miladySandboxesRepository.findRunningSandbox( + agentId, + orgId, + ); if (!rec?.bridge_url) { logger.warn("[milady-sandbox] Bridge call to non-running sandbox", { agentId, @@ -644,10 +713,13 @@ export class MiladySandboxService { agentId, walletPath, }); - return new Response(JSON.stringify({ error: "Invalid wallet endpoint" }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ error: "Invalid wallet endpoint" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } // Sanitize query parameters @@ -663,13 +735,19 @@ export class MiladySandboxService { sanitizedQuery = filtered.toString(); } - const rec = await miladySandboxesRepository.findRunningSandbox(agentId, orgId); + const rec = await miladySandboxesRepository.findRunningSandbox( + agentId, + orgId, + ); if (!rec) { - logger.warn("[milady-sandbox] Wallet proxy: sandbox not found or not running", { - agentId, - orgId, - walletPath, - }); + logger.warn( + "[milady-sandbox] Wallet proxy: sandbox not found or not running", + { + agentId, + orgId, + walletPath, + }, + ); return null; } if (!rec.bridge_url) { @@ -688,7 +766,9 @@ export class MiladySandboxService { const envVars = rec.environment_vars as Record | null; const apiToken = envVars?.MILADY_API_TOKEN; if (!apiToken) { - logger.warn("[milady-sandbox] No MILADY_API_TOKEN for wallet proxy", { agentId }); + logger.warn("[milady-sandbox] No MILADY_API_TOKEN for wallet proxy", { + agentId, + }); } // Determine the agent endpoint. Prefer the public domain (reachable from @@ -712,7 +792,9 @@ export class MiladySandboxService { endpoint: endpoint.replace(/Bearer.*/, "***"), }); - const headers: Record = { "Content-Type": "application/json" }; + const headers: Record = { + "Content-Type": "application/json", + }; if (apiToken) { headers.Authorization = `Bearer ${apiToken}`; } @@ -735,12 +817,22 @@ export class MiladySandboxService { } } - async bridgeStream(agentId: string, orgId: string, rpc: BridgeRequest): Promise { - const rec = await miladySandboxesRepository.findRunningSandbox(agentId, orgId); + async bridgeStream( + agentId: string, + orgId: string, + rpc: BridgeRequest, + ): Promise { + const rec = await miladySandboxesRepository.findRunningSandbox( + agentId, + orgId, + ); if (!rec?.bridge_url) return null; try { - const bridgeEndpoint = await this.getSafeBridgeEndpoint(rec, "/bridge/stream"); + const bridgeEndpoint = await this.getSafeBridgeEndpoint( + rec, + "/bridge/stream", + ); const res = await fetch(bridgeEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -765,8 +857,12 @@ export class MiladySandboxService { orgId: string, type: MiladyBackupSnapshotType = "manual", ): Promise { - const rec = await miladySandboxesRepository.findRunningSandbox(agentId, orgId); - if (!rec?.bridge_url) return { success: false, error: "Sandbox is not running" }; + const rec = await miladySandboxesRepository.findRunningSandbox( + agentId, + orgId, + ); + if (!rec?.bridge_url) + return { success: false, error: "Sandbox is not running" }; const { stateData, sizeBytes } = await this.fetchSnapshotState(rec); @@ -789,7 +885,11 @@ export class MiladySandboxService { return { success: true, backup }; } - async restore(agentId: string, orgId: string, backupId?: string): Promise { + async restore( + agentId: string, + orgId: string, + backupId?: string, + ): Promise { const rec = await miladySandboxesRepository.findByIdAndOrg(agentId, orgId); if (!rec) return { success: false, error: "Agent not found" }; @@ -804,7 +904,9 @@ export class MiladySandboxService { } if (rec.status !== "running" && backupId) { - const latestBackup = await miladySandboxesRepository.getLatestBackup(rec.id); + const latestBackup = await miladySandboxesRepository.getLatestBackup( + rec.id, + ); if (!latestBackup || backup.id !== latestBackup.id) { return { success: false, @@ -819,10 +921,15 @@ export class MiladySandboxService { } const prov = await this.provision(agentId, orgId); - return prov.success ? { success: true, backup } : { success: false, error: prov.error }; + return prov.success + ? { success: true, backup } + : { success: false, error: prov.error }; } - async listBackups(agentId: string, orgId: string): Promise { + async listBackups( + agentId: string, + orgId: string, + ): Promise { const rec = await miladySandboxesRepository.findByIdAndOrg(agentId, orgId); return rec ? miladySandboxesRepository.listBackups(rec.id) : []; } @@ -830,12 +937,18 @@ export class MiladySandboxService { // Heartbeat async heartbeat(agentId: string, orgId: string): Promise { - const rec = await miladySandboxesRepository.findRunningSandbox(agentId, orgId); + const rec = await miladySandboxesRepository.findRunningSandbox( + agentId, + orgId, + ); if (!rec?.bridge_url) return false; const res = await (async () => { try { - const heartbeatEndpoint = await this.getSafeBridgeEndpoint(rec, "/bridge"); + const heartbeatEndpoint = await this.getSafeBridgeEndpoint( + rec, + "/bridge", + ); return await fetch(heartbeatEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -871,7 +984,10 @@ export class MiladySandboxService { // Shutdown - async shutdown(agentId: string, orgId: string): Promise<{ success: boolean; error?: string }> { + async shutdown( + agentId: string, + orgId: string, + ): Promise<{ success: boolean; error?: string }> { let snapshotAgentId: string | null = null; let preShutdownSnapshot: { stateData: MiladyBackupStateData; @@ -881,13 +997,15 @@ export class MiladySandboxService { const snapshotSource = await this.getAgentForWrite(agentId, orgId); if (snapshotSource?.status === "running" && snapshotSource.bridge_url) { - preShutdownSnapshot = await this.fetchSnapshotState(snapshotSource).catch((error) => { - logger.warn("[milady-sandbox] Pre-shutdown backup fetch failed", { - agentId, - error: error instanceof Error ? error.message : String(error), - }); - return null; - }); + preShutdownSnapshot = await this.fetchSnapshotState(snapshotSource).catch( + (error) => { + logger.warn("[milady-sandbox] Pre-shutdown backup fetch failed", { + agentId, + error: error instanceof Error ? error.message : String(error), + }); + return null; + }, + ); } const result = await dbWrite.transaction(async (tx) => { @@ -896,7 +1014,11 @@ export class MiladySandboxService { const rec = await this.getAgentForLifecycleMutation(tx, agentId, orgId); if (!rec) return { success: false, error: "Agent not found" } as const; - const hasActiveProvisionJob = await this.hasActiveProvisionJobTx(tx, agentId, orgId); + const hasActiveProvisionJob = await this.hasActiveProvisionJobTx( + tx, + agentId, + orgId, + ); if (rec.status === "provisioning" || hasActiveProvisionJob) { return { success: false, @@ -944,12 +1066,14 @@ export class MiladySandboxService { }); if (result.success && snapshotAgentId) { - await miladySandboxesRepository.pruneBackups(snapshotAgentId, MAX_BACKUPS).catch((error) => { - logger.warn("[milady-sandbox] Backup pruning failed after shutdown", { - agentId, - error: error instanceof Error ? error.message : String(error), + await miladySandboxesRepository + .pruneBackups(snapshotAgentId, MAX_BACKUPS) + .catch((error) => { + logger.warn("[milady-sandbox] Backup pruning failed after shutdown", { + agentId, + error: error instanceof Error ? error.message : String(error), + }); }); - }); logger.info("[milady-sandbox] Shutdown complete", { agentId }); } @@ -958,7 +1082,11 @@ export class MiladySandboxService { // Private helpers - private async lockLifecycle(tx: LifecycleTx, agentId: string, orgId: string): Promise { + private async lockLifecycle( + tx: LifecycleTx, + agentId: string, + orgId: string, + ): Promise { await tx.execute(miladyProvisionAdvisoryLockSql(orgId, agentId)); } @@ -1008,7 +1136,10 @@ export class MiladySandboxService { throw new Error("Sandbox is not running"); } - const snapshotEndpoint = await this.getSafeBridgeEndpoint(rec, "/api/snapshot"); + const snapshotEndpoint = await this.getSafeBridgeEndpoint( + rec, + "/api/snapshot", + ); const res = await fetch(snapshotEndpoint, { method: "POST", signal: AbortSignal.timeout(15_000), @@ -1075,7 +1206,10 @@ export class MiladySandboxService { // (BRANCHES_LIMIT_EXCEEDED at 100 projects / 10 branches per project). const sharedDbUrl = process.env.DATABASE_URL; if (!sharedDbUrl) { - return { success: false, error: "DATABASE_URL not configured in cloud environment" }; + return { + success: false, + error: "DATABASE_URL not configured in cloud environment", + }; } await miladySandboxesRepository.update(rec.id, { @@ -1087,7 +1221,10 @@ export class MiladySandboxService { return { success: true, connectionUri: sharedDbUrl }; } - private async cleanupNeon(projectId: string | null | undefined, branchId?: string | null) { + private async cleanupNeon( + projectId: string | null | undefined, + branchId?: string | null, + ) { // In shared-DB mode no per-agent Neon project exists; nothing to clean up. if (!projectId) return; @@ -1102,10 +1239,13 @@ export class MiladySandboxService { } } catch (error) { if (error instanceof NeonClientError && error.statusCode === 404) { - logger.info("[milady-sandbox] Neon resource already absent during cleanup", { - projectId, - branchId, - }); + logger.info( + "[milady-sandbox] Neon resource already absent during cleanup", + { + projectId, + branchId, + }, + ); return; } throw error; @@ -1127,7 +1267,11 @@ export class MiladySandboxService { sandboxOrBridgeUrl: | Pick< MiladySandbox, - "bridge_url" | "node_id" | "bridge_port" | "headscale_ip" | "sandbox_id" + | "bridge_url" + | "node_id" + | "bridge_port" + | "headscale_ip" + | "sandbox_id" > | string, state: MiladyBackupStateData, @@ -1146,7 +1290,9 @@ export class MiladySandboxService { }); if (!res.ok) { const text = await res.text().catch(() => ""); - throw new Error(`State restore failed: HTTP ${res.status} ${text.slice(0, 200)}`); + throw new Error( + `State restore failed: HTTP ${res.status} ${text.slice(0, 200)}`, + ); } } } diff --git a/packages/lib/services/model-catalog.ts b/packages/lib/services/model-catalog.ts index 9681719c7..ea272c272 100644 --- a/packages/lib/services/model-catalog.ts +++ b/packages/lib/services/model-catalog.ts @@ -1,6 +1,10 @@ import { cache } from "@/lib/cache/client"; import { CacheKeys, CacheStaleTTL, CacheTTL } from "@/lib/cache/keys"; -import { type CatalogModel, GROQ_NATIVE_MODELS, mergeCatalogModels } from "@/lib/models"; +import { + type CatalogModel, + GROQ_NATIVE_MODELS, + mergeCatalogModels, +} from "@/lib/models"; import { getOpenRouterProvider, getProvider, @@ -29,7 +33,9 @@ function buildSWRValue(data: T): SWRCachedValue { async function fetchGatewayModelCatalog(): Promise { if (!hasGatewayProviderConfigured()) { - logger.info("[Model Catalog] Gateway provider is not configured; skipping catalog fetch"); + logger.info( + "[Model Catalog] Gateway provider is not configured; skipping catalog fetch", + ); return []; } @@ -73,18 +79,24 @@ async function fetchOpenRouterModelCatalog(): Promise { const data = (await response.json()) as OpenAIModelsResponse; if (!Array.isArray(data.data)) { - logger.warn("[Model Catalog] OpenRouter returned an invalid model catalog"); + logger.warn( + "[Model Catalog] OpenRouter returned an invalid model catalog", + ); return []; } return data.data; } catch (error) { - logger.warn("[Model Catalog] Failed to fetch OpenRouter model catalog", { error }); + logger.warn("[Model Catalog] Failed to fetch OpenRouter model catalog", { + error, + }); return []; } } -export async function getCachedOpenRouterModelCatalog(): Promise { +export async function getCachedOpenRouterModelCatalog(): Promise< + CatalogModel[] +> { const cached = await cache.getWithSWR( CacheKeys.models.openrouterCatalog(), CacheStaleTTL.models.catalog, @@ -96,7 +108,9 @@ export async function getCachedOpenRouterModelCatalog(): Promise } export async function getCachedMergedModelCatalog(): Promise { - const gatewayModels = hasGatewayProviderConfigured() ? await getCachedGatewayModelCatalog() : []; + const gatewayModels = hasGatewayProviderConfigured() + ? await getCachedGatewayModelCatalog() + : []; let models = gatewayModels; if (hasGroqProviderConfigured()) { @@ -111,7 +125,9 @@ export async function getCachedMergedModelCatalog(): Promise { return models; } -export async function getCachedGatewayModelById(modelId: string): Promise { +export async function getCachedGatewayModelById( + modelId: string, +): Promise { const gatewayModels = await getCachedGatewayModelCatalog(); return gatewayModels.find((model) => model.id === modelId) ?? null; diff --git a/packages/lib/services/neon-client.ts b/packages/lib/services/neon-client.ts index ead837f9e..1fc81f137 100644 --- a/packages/lib/services/neon-client.ts +++ b/packages/lib/services/neon-client.ts @@ -114,7 +114,10 @@ export class NeonClient { const connectionUri = data.connection_uris?.[0]?.connection_uri; if (!connectionUri) { - throw new NeonClientError("No connection URI in Neon response", "MISSING_CONNECTION_URI"); + throw new NeonClientError( + "No connection URI in Neon response", + "MISSING_CONNECTION_URI", + ); } // Extract host from connection URI safely @@ -154,16 +157,22 @@ export class NeonClient { * @returns Branch details including connection URI * @throws NeonClientError on API failure */ - async createBranch(projectId: string, branchName: string): Promise { + async createBranch( + projectId: string, + branchName: string, + ): Promise { logger.info("Creating Neon branch", { projectId, branchName }); - const response = await this.fetchWithRetry(`/projects/${projectId}/branches`, { - method: "POST", - body: JSON.stringify({ - branch: { name: branchName }, - endpoints: [{ type: "read_write" }], - }), - }); + const response = await this.fetchWithRetry( + `/projects/${projectId}/branches`, + { + method: "POST", + body: JSON.stringify({ + branch: { name: branchName }, + endpoints: [{ type: "read_write" }], + }), + }, + ); const data = await response.json(); const branch = data.branch; @@ -243,9 +252,12 @@ export class NeonClient { * @returns Connection URI */ async getConnectionUri(projectId: string): Promise { - const response = await this.fetchWithRetry(`/projects/${projectId}/connection_uri`, { - method: "GET", - }); + const response = await this.fetchWithRetry( + `/projects/${projectId}/connection_uri`, + { + method: "GET", + }, + ); const data = await response.json(); return data.uri; @@ -298,8 +310,12 @@ export class NeonClient { } // Retry on rate limit or server errors - if ((response.status === 429 || response.status >= 500) && retryCount < MAX_RETRIES) { - const delay = INITIAL_RETRY_DELAY_MS * RETRY_BACKOFF_MULTIPLIER ** retryCount; + if ( + (response.status === 429 || response.status >= 500) && + retryCount < MAX_RETRIES + ) { + const delay = + INITIAL_RETRY_DELAY_MS * RETRY_BACKOFF_MULTIPLIER ** retryCount; logger.warn("Neon API request failed, retrying", { status: response.status, @@ -322,7 +338,8 @@ export class NeonClient { // Network error - retry if (retryCount < MAX_RETRIES) { - const delay = INITIAL_RETRY_DELAY_MS * RETRY_BACKOFF_MULTIPLIER ** retryCount; + const delay = + INITIAL_RETRY_DELAY_MS * RETRY_BACKOFF_MULTIPLIER ** retryCount; logger.warn("Neon API network error, retrying", { error: error instanceof Error ? error.message : "Unknown", diff --git a/packages/lib/services/oauth/cache-version.ts b/packages/lib/services/oauth/cache-version.ts index ec6dc9c41..2b57f6a45 100644 --- a/packages/lib/services/oauth/cache-version.ts +++ b/packages/lib/services/oauth/cache-version.ts @@ -18,7 +18,10 @@ const VERSION_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days * Get the current cache version for an org+platform pair. * Returns 0 if no version exists (first use). */ -export async function getOAuthVersion(orgId: string, platform: string): Promise { +export async function getOAuthVersion( + orgId: string, + platform: string, +): Promise { const key = `${VERSION_KEY_PREFIX}:${orgId}:${platform}`; const version = await cache.get(key); return version ?? 0; @@ -29,7 +32,10 @@ export async function getOAuthVersion(orgId: string, platform: string): Promise< * Call this whenever OAuth state changes: connect, disconnect, token refresh. * All existing cache entries with the old version will auto-miss. */ -export async function incrementOAuthVersion(orgId: string, platform: string): Promise { +export async function incrementOAuthVersion( + orgId: string, + platform: string, +): Promise { const key = `${VERSION_KEY_PREFIX}:${orgId}:${platform}`; const newVersion = await cache.incr(key); await cache.expire(key, VERSION_TTL_SECONDS); diff --git a/packages/lib/services/oauth/connection-adapters/blooio-adapter.ts b/packages/lib/services/oauth/connection-adapters/blooio-adapter.ts index b4f0461ca..ff3f26650 100644 --- a/packages/lib/services/oauth/connection-adapters/blooio-adapter.ts +++ b/packages/lib/services/oauth/connection-adapters/blooio-adapter.ts @@ -35,17 +35,30 @@ export const blooioAdapter: ConnectionAdapter = { if (!hasApiKey) return []; - const fromNumber = await getSecretValue(organizationId, PATTERNS.fromNumber!); + const fromNumber = await getSecretValue( + organizationId, + PATTERNS.fromNumber!, + ); return [ - createSecretsConnection(PLATFORM, organizationId, getEarliestSecretDate(platformSecrets), { - platformUserId: "blooio-user", - displayName: fromNumber ? `Blooio (${fromNumber})` : "Blooio iMessage", - }), + createSecretsConnection( + PLATFORM, + organizationId, + getEarliestSecretDate(platformSecrets), + { + platformUserId: "blooio-user", + displayName: fromNumber + ? `Blooio (${fromNumber})` + : "Blooio iMessage", + }, + ), ]; }, - async getToken(organizationId: string, connectionId: string): Promise { + async getToken( + organizationId: string, + connectionId: string, + ): Promise { verifyConnectionId(PLATFORM, organizationId, connectionId); const apiKey = await getSecretValue(organizationId, PATTERNS.apiKey!); @@ -63,7 +76,11 @@ export const blooioAdapter: ConnectionAdapter = { async revoke(organizationId: string, connectionId: string): Promise { verifyConnectionId(PLATFORM, organizationId, connectionId); - const count = await deletePlatformSecrets(organizationId, PREFIX, "oauth-service"); + const count = await deletePlatformSecrets( + organizationId, + PREFIX, + "oauth-service", + ); blooioAutomationService.invalidateStatusCache(organizationId); logger.info("[BlooioAdapter] Connection revoked", { connectionId, diff --git a/packages/lib/services/oauth/connection-adapters/generic-adapter.ts b/packages/lib/services/oauth/connection-adapters/generic-adapter.ts index d18c673a5..dadc2511c 100644 --- a/packages/lib/services/oauth/connection-adapters/generic-adapter.ts +++ b/packages/lib/services/oauth/connection-adapters/generic-adapter.ts @@ -18,7 +18,8 @@ import { refreshOAuth2Token } from "../providers"; import type { OAuthConnection, TokenResult } from "../types"; import type { ConnectionAdapter } from "./index"; -const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; // Buffer before token expiry to trigger refresh (5 minutes) const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; @@ -28,7 +29,8 @@ const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; * This allows the adapter to be used for any platform that stores in platform_credentials. */ export function createGenericAdapter(platform: string): ConnectionAdapter { - const platformEnum = platform as (typeof platformCredentials.platform.enumValues)[number]; + const platformEnum = + platform as (typeof platformCredentials.platform.enumValues)[number]; async function decryptTokenSecret( secretId: string, @@ -45,14 +47,17 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { return await secretsService.getDecryptedValue(secretId, organizationId); } catch (error) { if (error instanceof DecryptionError) { - logger.error(`[GenericAdapter] Token decryption failed for ${platform}`, { - connectionId, - organizationId, - secretId, - tokenType, - phase: error.phase, - error: error.message, - }); + logger.error( + `[GenericAdapter] Token decryption failed for ${platform}`, + { + connectionId, + organizationId, + secretId, + tokenType, + phase: error.phase, + error: error.message, + }, + ); // Mark connection as needing re-authentication await dbWrite @@ -112,7 +117,8 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { connectionRole: cred.source_context && typeof cred.source_context === "object" && - (cred.source_context as Record).miladyGoogleSide === "agent" + (cred.source_context as Record) + .miladyGoogleSide === "agent" ? "agent" : "owner", platform, @@ -140,7 +146,10 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { } }, - async getToken(organizationId: string, connectionId: string): Promise { + async getToken( + organizationId: string, + connectionId: string, + ): Promise { const cred = await findCredential(organizationId, connectionId); if (!cred) throw Errors.connectionNotFound(connectionId); if (cred.status === "revoked") throw Errors.connectionRevoked(platform); @@ -153,7 +162,8 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { // Check if token needs refresh const tokenExpired = cred.token_expires_at && - new Date(cred.token_expires_at).getTime() - TOKEN_EXPIRY_BUFFER_MS < Date.now(); + new Date(cred.token_expires_at).getTime() - TOKEN_EXPIRY_BUFFER_MS < + Date.now(); let accessToken: string; let expiresAt: Date | undefined = cred.token_expires_at || undefined; @@ -176,7 +186,10 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { ); // Refresh the token using the generic flow - const refreshResult = await refreshOAuth2Token(provider, refreshToken); + const refreshResult = await refreshOAuth2Token( + provider, + refreshToken, + ); // Store the new access token const audit = { @@ -205,14 +218,17 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { audit, ); } catch (refreshTokenError) { - logger.error(`[GenericAdapter] Failed to store new refresh token for ${platform}`, { - connectionId, - organizationId, - error: - refreshTokenError instanceof Error - ? refreshTokenError.message - : String(refreshTokenError), - }); + logger.error( + `[GenericAdapter] Failed to store new refresh token for ${platform}`, + { + connectionId, + organizationId, + error: + refreshTokenError instanceof Error + ? refreshTokenError.message + : String(refreshTokenError), + }, + ); await dbWrite .update(platformCredentials) @@ -254,11 +270,14 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { organizationId, }); } catch (error) { - logger.error(`[GenericAdapter] Token refresh failed for ${platform}`, { - connectionId, - organizationId, - error: error instanceof Error ? error.message : String(error), - }); + logger.error( + `[GenericAdapter] Token refresh failed for ${platform}`, + { + connectionId, + organizationId, + error: error instanceof Error ? error.message : String(error), + }, + ); throw Errors.tokenRefreshFailed( platform, error instanceof Error ? error.message : "Unknown error", @@ -304,12 +323,15 @@ export function createGenericAdapter(platform: string): ConnectionAdapter { try { await secretsService.delete(id, organizationId, audit); } catch (error) { - logger.warn(`[GenericAdapter] Failed to delete ${tokenType} secret during revoke`, { - secretId: id, - platform, - organizationId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + `[GenericAdapter] Failed to delete ${tokenType} secret during revoke`, + { + secretId: id, + platform, + organizationId, + error: error instanceof Error ? error.message : String(error), + }, + ); } }; diff --git a/packages/lib/services/oauth/connection-adapters/index.ts b/packages/lib/services/oauth/connection-adapters/index.ts index 44ebd489a..ebd0064d4 100644 --- a/packages/lib/services/oauth/connection-adapters/index.ts +++ b/packages/lib/services/oauth/connection-adapters/index.ts @@ -73,7 +73,10 @@ export function getAdapter(platform: string): ConnectionAdapter | null { if (dynamicAdapters[platform]) return dynamicAdapters[platform]; const provider = getProvider(platform); - if (provider?.useGenericRoutes && provider.storage === "platform_credentials") { + if ( + provider?.useGenericRoutes && + provider.storage === "platform_credentials" + ) { dynamicAdapters[platform] = createGenericAdapter(platform); return dynamicAdapters[platform]; } @@ -83,7 +86,10 @@ export function getAdapter(platform: string): ConnectionAdapter | null { /** Get all registered adapters (static + cached dynamic). */ export function getAllAdapters(): ConnectionAdapter[] { for (const provider of Object.values(OAUTH_PROVIDERS)) { - if (provider.storage === "platform_credentials" && provider.useGenericRoutes) { + if ( + provider.storage === "platform_credentials" && + provider.useGenericRoutes + ) { void getAdapter(provider.id); } } diff --git a/packages/lib/services/oauth/connection-adapters/secrets-adapter-utils.ts b/packages/lib/services/oauth/connection-adapters/secrets-adapter-utils.ts index c0d899bb3..635cb8436 100644 --- a/packages/lib/services/oauth/connection-adapters/secrets-adapter-utils.ts +++ b/packages/lib/services/oauth/connection-adapters/secrets-adapter-utils.ts @@ -11,12 +11,18 @@ import { Errors } from "../errors"; import type { OAuthConnection, OAuthConnectionSource } from "../types"; /** Generate a stable connection ID for secrets-based adapters */ -export function generateConnectionId(platform: string, organizationId: string): string { +export function generateConnectionId( + platform: string, + organizationId: string, +): string { return `${platform}:${organizationId}`; } /** Check if a connection ID belongs to a platform */ -export function ownsConnectionId(platform: string, connectionId: string): boolean { +export function ownsConnectionId( + platform: string, + connectionId: string, +): boolean { return connectionId.startsWith(`${platform}:`); } @@ -33,11 +39,19 @@ export function verifyConnectionId( } /** Fetch all secrets matching a prefix for an organization */ -export async function fetchPlatformSecrets(organizationId: string, prefix: string) { +export async function fetchPlatformSecrets( + organizationId: string, + prefix: string, +) { return dbRead .select() .from(secrets) - .where(and(eq(secrets.organization_id, organizationId), like(secrets.name, `${prefix}%`))); + .where( + and( + eq(secrets.organization_id, organizationId), + like(secrets.name, `${prefix}%`), + ), + ); } /** Get decrypted secret value */ @@ -56,7 +70,12 @@ export async function updateSecretAccessTime( const [record] = await dbRead .select() .from(secrets) - .where(and(eq(secrets.organization_id, organizationId), eq(secrets.name, secretName))) + .where( + and( + eq(secrets.organization_id, organizationId), + eq(secrets.name, secretName), + ), + ) .limit(1); if (record) { @@ -94,7 +113,9 @@ export async function deletePlatformSecrets( } /** Get earliest creation date from a list of secrets */ -export function getEarliestSecretDate(secretRecords: { created_at: Date }[]): Date { +export function getEarliestSecretDate( + secretRecords: { created_at: Date }[], +): Date { if (secretRecords.length === 0) return new Date(); return secretRecords.reduce((earliest, secret) => { const secretDate = new Date(secret.created_at); diff --git a/packages/lib/services/oauth/connection-adapters/twilio-adapter.ts b/packages/lib/services/oauth/connection-adapters/twilio-adapter.ts index 8b7e59678..e4391ac91 100644 --- a/packages/lib/services/oauth/connection-adapters/twilio-adapter.ts +++ b/packages/lib/services/oauth/connection-adapters/twilio-adapter.ts @@ -36,8 +36,12 @@ export const twilioAdapter: ConnectionAdapter = { async listConnections(organizationId: string): Promise { const platformSecrets = await fetchPlatformSecrets(organizationId, PREFIX); - const hasAccountSid = platformSecrets.some((s) => s.name === PATTERNS.accountSid); - const hasAuthToken = platformSecrets.some((s) => s.name === PATTERNS.authToken); + const hasAccountSid = platformSecrets.some( + (s) => s.name === PATTERNS.accountSid, + ); + const hasAuthToken = platformSecrets.some( + (s) => s.name === PATTERNS.authToken, + ); if (!hasAccountSid || !hasAuthToken) return []; @@ -47,14 +51,24 @@ export const twilioAdapter: ConnectionAdapter = { ]); return [ - createSecretsConnection(PLATFORM, organizationId, getEarliestSecretDate(platformSecrets), { - platformUserId: fullSid ? maskAccountSid(fullSid) : "unknown", - displayName: phoneNumber ? `Twilio (${phoneNumber})` : "Twilio Account", - }), + createSecretsConnection( + PLATFORM, + organizationId, + getEarliestSecretDate(platformSecrets), + { + platformUserId: fullSid ? maskAccountSid(fullSid) : "unknown", + displayName: phoneNumber + ? `Twilio (${phoneNumber})` + : "Twilio Account", + }, + ), ]; }, - async getToken(organizationId: string, connectionId: string): Promise { + async getToken( + organizationId: string, + connectionId: string, + ): Promise { verifyConnectionId(PLATFORM, organizationId, connectionId); const [accountSid, authToken] = await Promise.all([ @@ -77,7 +91,11 @@ export const twilioAdapter: ConnectionAdapter = { async revoke(organizationId: string, connectionId: string): Promise { verifyConnectionId(PLATFORM, organizationId, connectionId); - const count = await deletePlatformSecrets(organizationId, PREFIX, "oauth-service"); + const count = await deletePlatformSecrets( + organizationId, + PREFIX, + "oauth-service", + ); twilioAutomationService.invalidateStatusCache(organizationId); logger.info("[TwilioAdapter] Connection revoked", { connectionId, diff --git a/packages/lib/services/oauth/connection-adapters/twitter-adapter.ts b/packages/lib/services/oauth/connection-adapters/twitter-adapter.ts index 54c9e73ad..76c8d35c8 100644 --- a/packages/lib/services/oauth/connection-adapters/twitter-adapter.ts +++ b/packages/lib/services/oauth/connection-adapters/twitter-adapter.ts @@ -30,7 +30,9 @@ export const twitterAdapter: ConnectionAdapter = { async listConnections(organizationId: string): Promise { const platformSecrets = await fetchPlatformSecrets(organizationId, PREFIX); - const hasAccessToken = platformSecrets.some((s) => s.name === PATTERNS.accessToken); + const hasAccessToken = platformSecrets.some( + (s) => s.name === PATTERNS.accessToken, + ); if (!hasAccessToken) return []; @@ -40,21 +42,35 @@ export const twitterAdapter: ConnectionAdapter = { ]); return [ - createSecretsConnection(PLATFORM, organizationId, getEarliestSecretDate(platformSecrets), { - platformUserId: userId || "unknown", - username: username || undefined, - displayName: username ? `@${username}` : undefined, - }), + createSecretsConnection( + PLATFORM, + organizationId, + getEarliestSecretDate(platformSecrets), + { + platformUserId: userId || "unknown", + username: username || undefined, + displayName: username ? `@${username}` : undefined, + }, + ), ]; }, - async getToken(organizationId: string, connectionId: string): Promise { + async getToken( + organizationId: string, + connectionId: string, + ): Promise { verifyConnectionId(PLATFORM, organizationId, connectionId); - const accessToken = await getSecretValue(organizationId, PATTERNS.accessToken!); + const accessToken = await getSecretValue( + organizationId, + PATTERNS.accessToken!, + ); if (!accessToken) throw Errors.platformNotConnected(PLATFORM); - const accessTokenSecret = await getSecretValue(organizationId, PATTERNS.accessTokenSecret!); + const accessTokenSecret = await getSecretValue( + organizationId, + PATTERNS.accessTokenSecret!, + ); await updateSecretAccessTime(organizationId, PATTERNS.accessToken!); return { @@ -68,7 +84,11 @@ export const twitterAdapter: ConnectionAdapter = { async revoke(organizationId: string, connectionId: string): Promise { verifyConnectionId(PLATFORM, organizationId, connectionId); - const count = await deletePlatformSecrets(organizationId, PREFIX, "oauth-service"); + const count = await deletePlatformSecrets( + organizationId, + PREFIX, + "oauth-service", + ); logger.info("[TwitterAdapter] Connection revoked", { connectionId, organizationId, diff --git a/packages/lib/services/oauth/errors.ts b/packages/lib/services/oauth/errors.ts index e77e49b37..2c3a3a3f1 100644 --- a/packages/lib/services/oauth/errors.ts +++ b/packages/lib/services/oauth/errors.ts @@ -194,7 +194,8 @@ export const Errors = { retryAfter, ), - internalError: (message: string) => new OAuthError(OAuthErrorCode.INTERNAL_ERROR, message, false), + internalError: (message: string) => + new OAuthError(OAuthErrorCode.INTERNAL_ERROR, message, false), }; /** Create a standard internal error response */ diff --git a/packages/lib/services/oauth/index.ts b/packages/lib/services/oauth/index.ts index eff76cc57..53cb170e8 100644 --- a/packages/lib/services/oauth/index.ts +++ b/packages/lib/services/oauth/index.ts @@ -9,7 +9,11 @@ * const connections = await oauthService.listConnections({ organizationId }); */ -export { type ConnectionAdapter, getAdapter, getAllAdapters } from "./connection-adapters"; +export { + type ConnectionAdapter, + getAdapter, + getAllAdapters, +} from "./connection-adapters"; // Errors export { ERROR_STATUS_MAP, diff --git a/packages/lib/services/oauth/invalidation.ts b/packages/lib/services/oauth/invalidation.ts index 7dfa4062d..9c6ac7baf 100644 --- a/packages/lib/services/oauth/invalidation.ts +++ b/packages/lib/services/oauth/invalidation.ts @@ -20,7 +20,9 @@ export async function invalidateOAuthState( ): Promise { try { await Promise.all([ - opts?.skipVersionBump ? Promise.resolve() : incrementOAuthVersion(orgId, platform), + opts?.skipVersionBump + ? Promise.resolve() + : incrementOAuthVersion(orgId, platform), invalidateByOrganization(orgId), userId ? entitySettingsCache.invalidateUser(userId) : Promise.resolve(), edgeRuntimeCache.bumpMcpVersion(orgId), diff --git a/packages/lib/services/oauth/oauth-service.ts b/packages/lib/services/oauth/oauth-service.ts index 392166225..2cb280689 100644 --- a/packages/lib/services/oauth/oauth-service.ts +++ b/packages/lib/services/oauth/oauth-service.ts @@ -10,7 +10,11 @@ import { logger } from "@/lib/utils/logger"; import { getOAuthVersion, incrementOAuthVersion } from "./cache-version"; import { getAdapter, getAllAdapters } from "./connection-adapters"; import { Errors } from "./errors"; -import { getProvider, isProviderConfigured, OAUTH_PROVIDERS } from "./provider-registry"; +import { + getProvider, + isProviderConfigured, + OAUTH_PROVIDERS, +} from "./provider-registry"; import { initiateOAuth2 } from "./providers"; import { tokenCache } from "./token-cache"; import type { @@ -28,7 +32,9 @@ import type { const DEFAULT_REDIRECT = "/dashboard/settings?tab=connections"; const STATE_TTL = 600; // 10 minutes -export function sortConnectionsByRecency(connections: OAuthConnection[]): OAuthConnection[] { +export function sortConnectionsByRecency( + connections: OAuthConnection[], +): OAuthConnection[] { return [...connections].sort((a, b) => { const aTime = a.lastUsedAt?.getTime() || a.linkedAt.getTime(); const bTime = b.lastUsedAt?.getTime() || b.linkedAt.getTime(); @@ -54,7 +60,9 @@ export function getPreferredActiveConnection( connectionRole?: OAuthConnectionRole, ): OAuthConnection | null { const scopedByRole = connectionRole - ? connections.filter((connection) => connection.connectionRole === connectionRole) + ? connections.filter( + (connection) => connection.connectionRole === connectionRole, + ) : connections; if (!userId) { return getMostRecentActiveConnection(scopedByRole); @@ -78,7 +86,9 @@ export function scopeConnectionsForUser( connectionRole?: OAuthConnectionRole, ): OAuthConnection[] { const scopedByRole = connectionRole - ? connections.filter((connection) => connection.connectionRole === connectionRole) + ? connections.filter( + (connection) => connection.connectionRole === connectionRole, + ) : connections; if (!userId) { return sortConnectionsByRecency(scopedByRole); @@ -113,15 +123,26 @@ class OAuthService { /** Initiate OAuth flow for a platform */ async initiateAuth(params: InitiateAuthParams): Promise { - const { organizationId, userId, platform, redirectUrl, scopes, connectionRole } = params; + const { + organizationId, + userId, + platform, + redirectUrl, + scopes, + connectionRole, + } = params; const provider = getProvider(platform); if (!provider) throw Errors.platformNotSupported(platform); - if (!isProviderConfigured(provider)) throw Errors.platformNotConfigured(platform); + if (!isProviderConfigured(provider)) + throw Errors.platformNotConfigured(platform); // API key providers return a form URL if (provider.type === "api_key") { - return { authUrl: provider.routes?.initiate || "", requiresCredentials: true }; + return { + authUrl: provider.routes?.initiate || "", + requiresCredentials: true, + }; } // Use generic OAuth2 flow for providers that opt-in @@ -150,9 +171,12 @@ class OAuthService { userId: string, redirectUrl?: string, ): Promise { - const { twitterAutomationService } = await import("@/lib/services/twitter-automation"); + const { twitterAutomationService } = await import( + "@/lib/services/twitter-automation" + ); - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const result = await twitterAutomationService.generateAuthLink( `${baseUrl}/api/v1/twitter/callback`, ); @@ -172,9 +196,13 @@ class OAuthService { } /** List all OAuth connections for an organization */ - async listConnections(params: ListConnectionsParams): Promise { + async listConnections( + params: ListConnectionsParams, + ): Promise { const { organizationId, platform, userId, connectionRole } = params; - const adapters = platform ? [getAdapter(platform)].filter(Boolean) : getAllAdapters(); + const adapters = platform + ? [getAdapter(platform)].filter(Boolean) + : getAllAdapters(); const results = await Promise.allSettled( adapters.map((a) => a!.listConnections(organizationId)), ); @@ -210,7 +238,10 @@ class OAuthService { // getValidToken caches a still-active token under the new version key. await adapter.revoke(organizationId, connectionId); - const version = await incrementOAuthVersion(organizationId, adapter.platform); + const version = await incrementOAuthVersion( + organizationId, + adapter.platform, + ); await tokenCache.invalidate(organizationId, connectionId, version); logger.info("[OAuthService] Connection revoked", { @@ -221,7 +252,9 @@ class OAuthService { } /** Get a valid access token for a connection (uses cache with version counter) */ - async getValidToken(params: GetTokenParams & { platform?: string }): Promise { + async getValidToken( + params: GetTokenParams & { platform?: string }, + ): Promise { const { organizationId, connectionId, platform } = params; // Look up adapter to determine platform if not provided @@ -233,7 +266,10 @@ class OAuthService { const cached = await tokenCache.get(organizationId, connectionId, version); if (cached) { - logger.debug("[OAuthService] Token from cache", { connectionId, version }); + logger.debug("[OAuthService] Token from cache", { + connectionId, + version, + }); return cached; } @@ -244,8 +280,11 @@ class OAuthService { } /** Get valid token by platform (uses most recently used active connection) */ - async getValidTokenByPlatform(params: GetTokenByPlatformParams): Promise { - const { token } = await this.getValidTokenByPlatformWithConnectionId(params); + async getValidTokenByPlatform( + params: GetTokenByPlatformParams, + ): Promise { + const { token } = + await this.getValidTokenByPlatformWithConnectionId(params); return token; } @@ -259,7 +298,11 @@ class OAuthService { if (!adapter) throw Errors.platformNotSupported(platform); const connections = await adapter.listConnections(organizationId); - const activeConnection = getPreferredActiveConnection(connections, userId, connectionRole); + const activeConnection = getPreferredActiveConnection( + connections, + userId, + connectionRole, + ); if (!activeConnection) throw Errors.platformNotConnected(platform); const token = await this.getValidToken({ @@ -281,13 +324,22 @@ class OAuthService { if (!adapter) return false; const connections = await adapter.listConnections(organizationId); - return getPreferredActiveConnection(connections, userId, connectionRole) !== null; + return ( + getPreferredActiveConnection(connections, userId, connectionRole) !== null + ); } /** Get all platforms with active connections */ - async getConnectedPlatforms(organizationId: string, userId?: string): Promise { + async getConnectedPlatforms( + organizationId: string, + userId?: string, + ): Promise { const connections = await this.listConnections({ organizationId, userId }); - return [...new Set(connections.filter((c) => c.status === "active").map((c) => c.platform))]; + return [ + ...new Set( + connections.filter((c) => c.status === "active").map((c) => c.platform), + ), + ]; } /** Invalidate all cached tokens for an organization */ @@ -304,7 +356,9 @@ class OAuthService { } return null; } - private sortConnectionsByRecency(connections: OAuthConnection[]): OAuthConnection[] { + private sortConnectionsByRecency( + connections: OAuthConnection[], + ): OAuthConnection[] { return sortConnectionsByRecency(connections); } } diff --git a/packages/lib/services/oauth/provider-registry.ts b/packages/lib/services/oauth/provider-registry.ts index ede0bae1c..c736f5aa6 100644 --- a/packages/lib/services/oauth/provider-registry.ts +++ b/packages/lib/services/oauth/provider-registry.ts @@ -255,7 +255,8 @@ export const OAUTH_PROVIDERS: Record = { // Using /consumers/ to support personal Microsoft accounts (@outlook.com, @hotmail.com, @live.com) // Use /common/ for multi-tenant apps that support both personal and work/school accounts // Use /organizations/ for work/school accounts only - authorization: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize", + authorization: + "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize", token: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", userInfo: "https://graph.microsoft.com/v1.0/me", }, @@ -377,7 +378,12 @@ export const OAUTH_PROVIDERS: Record = { userInfo: "https://slack.com/api/users.identity", revoke: "https://slack.com/api/auth.revoke", }, - defaultScopes: ["identity.basic", "users:read", "chat:write", "channels:read"], + defaultScopes: [ + "identity.basic", + "users:read", + "chat:write", + "channels:read", + ], userInfoMapping: { id: "user_id", displayName: "user", @@ -753,7 +759,10 @@ export function getProvider(platformId: string): OAuthProviderConfig | null { /** Check if provider has required env vars (API key providers always return true). */ export function isProviderConfigured(provider: OAuthProviderConfig): boolean { - return provider.envVars.length === 0 || provider.envVars.every((v) => !!process.env[v]); + return ( + provider.envVars.length === 0 || + provider.envVars.every((v) => !!process.env[v]) + ); } /** Get all providers with required env vars configured. */ @@ -763,7 +772,9 @@ export function getConfiguredProviders(): OAuthProviderConfig[] { /** Get configured OAuth providers (oauth2 or oauth1a, not api_key). */ export function getConfiguredOAuthProviders(): OAuthProviderConfig[] { - return getConfiguredProviders().filter((p) => p.type === "oauth2" || p.type === "oauth1a"); + return getConfiguredProviders().filter( + (p) => p.type === "oauth2" || p.type === "oauth1a", + ); } function normalizeScopes(scopes: string[] | undefined): string[] { @@ -775,7 +786,9 @@ function normalizeScopes(scopes: string[] | undefined): string[] { } export function getAllowedScopes(provider: OAuthProviderConfig): string[] { - return normalizeScopes(provider.allowedScopes ?? provider.defaultScopes ?? []); + return normalizeScopes( + provider.allowedScopes ?? provider.defaultScopes ?? [], + ); } export function resolveRequestedScopes( @@ -788,7 +801,9 @@ export function resolveRequestedScopes( } const allowedScopes = getAllowedScopes(provider); - const invalidScopes = normalizedRequested.filter((scope) => !allowedScopes.includes(scope)); + const invalidScopes = normalizedRequested.filter( + (scope) => !allowedScopes.includes(scope), + ); if (invalidScopes.length > 0) { throw Errors.invalidScopeRequest(provider.id, invalidScopes); } @@ -808,20 +823,32 @@ export function isValidProvider(platformId: string): boolean { /** Get client ID from provider's env vars. */ export function getClientId(provider: OAuthProviderConfig): string | undefined { - const v = provider.envVars.find((e) => e.includes("CLIENT_ID") || e.includes("API_KEY")); + const v = provider.envVars.find( + (e) => e.includes("CLIENT_ID") || e.includes("API_KEY"), + ); return v ? process.env[v] : undefined; } /** Get client secret from provider's env vars. */ -export function getClientSecret(provider: OAuthProviderConfig): string | undefined { - const v = provider.envVars.find((e) => e.includes("CLIENT_SECRET") || e.includes("SECRET_KEY")); +export function getClientSecret( + provider: OAuthProviderConfig, +): string | undefined { + const v = provider.envVars.find( + (e) => e.includes("CLIENT_SECRET") || e.includes("SECRET_KEY"), + ); return v ? process.env[v] : undefined; } /** Build callback URL for provider. */ -export function getCallbackUrl(provider: OAuthProviderConfig, baseUrl: string): string { - if (provider.useGenericRoutes) return `${baseUrl}/api/v1/oauth/${provider.id}/callback`; - return provider.routes?.callback ? `${baseUrl}${provider.routes.callback}` : ""; +export function getCallbackUrl( + provider: OAuthProviderConfig, + baseUrl: string, +): string { + if (provider.useGenericRoutes) + return `${baseUrl}/api/v1/oauth/${provider.id}/callback`; + return provider.routes?.callback + ? `${baseUrl}${provider.routes.callback}` + : ""; } /** Extract nested value using dot notation (e.g., "data.viewer.id"). */ diff --git a/packages/lib/services/oauth/providers/oauth2.ts b/packages/lib/services/oauth/providers/oauth2.ts index 5592fb6a2..61abbed1f 100644 --- a/packages/lib/services/oauth/providers/oauth2.ts +++ b/packages/lib/services/oauth/providers/oauth2.ts @@ -8,11 +8,17 @@ import { and, eq, sql } from "drizzle-orm"; import { dbWrite } from "@/db/client"; import { writeTransaction } from "@/db/helpers"; -import { platformCredentials, platformCredentialTypeEnum } from "@/db/schemas/platform-credentials"; +import { + platformCredentials, + platformCredentialTypeEnum, +} from "@/db/schemas/platform-credentials"; import { cache } from "@/lib/cache/client"; import { secretsService } from "@/lib/services/secrets"; import { logger } from "@/lib/utils/logger"; -import type { OAuthProviderConfig, UserInfoMapping } from "../provider-registry"; +import type { + OAuthProviderConfig, + UserInfoMapping, +} from "../provider-registry"; import { getCallbackUrl, getClientId, @@ -42,13 +48,19 @@ interface OAuth2State { * Generate PKCE code verifier and challenge. * Uses S256 challenge method per RFC 7636. */ -async function generatePKCE(): Promise<{ codeVerifier: string; codeChallenge: string }> { +async function generatePKCE(): Promise<{ + codeVerifier: string; + codeChallenge: string; +}> { // Generate 32 random bytes → 43-char base64url string const bytes = crypto.getRandomValues(new Uint8Array(32)); const codeVerifier = Buffer.from(bytes).toString("base64url"); // SHA-256 hash of verifier → base64url encoded - const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)); + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(codeVerifier), + ); const codeChallenge = Buffer.from(digest).toString("base64url"); return { codeVerifier, codeChallenge }; @@ -116,14 +128,19 @@ export async function initiateOAuth2( ): Promise { const clientId = getClientId(provider); if (!clientId) { - throw new Error(`OAuth not configured: missing client ID for ${provider.id}`); + throw new Error( + `OAuth not configured: missing client ID for ${provider.id}`, + ); } if (!provider.endpoints?.authorization) { - throw new Error(`OAuth not configured: missing authorization endpoint for ${provider.id}`); + throw new Error( + `OAuth not configured: missing authorization endpoint for ${provider.id}`, + ); } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const callbackUrl = getCallbackUrl(provider, baseUrl); const scopes = resolveRequestedScopes(provider, params.scopes); const redirectUrl = params.redirectUrl || "/auth/success"; @@ -152,7 +169,11 @@ export async function initiateOAuth2( codeVerifier, }; - await cache.set(`oauth2:${provider.id}:${state}`, stateData, STATE_TTL_SECONDS); + await cache.set( + `oauth2:${provider.id}:${state}`, + stateData, + STATE_TTL_SECONDS, + ); // Build authorization URL const authUrl = new URL(provider.endpoints.authorization); @@ -216,7 +237,8 @@ export async function handleOAuth2Callback( // Delete state to prevent replay attacks await cache.del(stateKey); - const { organizationId, userId, redirectUrl, scopes, codeVerifier } = stateData; + const { organizationId, userId, redirectUrl, scopes, codeVerifier } = + stateData; const connectionRole = stateData.connectionRole ?? "owner"; // Exchange code for tokens @@ -272,14 +294,19 @@ async function exchangeCodeForTokens( const clientSecret = getClientSecret(provider); if (!clientId || !clientSecret) { - throw new Error(`OAuth not configured: missing credentials for ${provider.id}`); + throw new Error( + `OAuth not configured: missing credentials for ${provider.id}`, + ); } if (!provider.endpoints?.token) { - throw new Error(`OAuth not configured: missing token endpoint for ${provider.id}`); + throw new Error( + `OAuth not configured: missing token endpoint for ${provider.id}`, + ); } - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; const callbackUrl = getCallbackUrl(provider, baseUrl); // Build token request body @@ -307,7 +334,9 @@ async function exchangeCodeForTokens( for (const [key, value] of Object.entries(provider.tokenHeaders)) { if (value === "Basic ${base64(CLIENT_ID:CLIENT_SECRET)}") { // Special placeholder for Basic auth - const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); + const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString( + "base64", + ); headers[key] = `Basic ${credentials}`; // Remove client credentials from body when using Basic auth delete bodyParams.client_id; @@ -419,8 +448,12 @@ async function fetchUserInfo( // Check for GraphQL errors (GraphQL APIs return 200 even on errors) if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) { - const errorMessage = data.errors.map((e: { message?: string }) => e.message).join(", "); - logger.error(`[OAuth2] GraphQL error for ${provider.id}`, { errors: data.errors }); + const errorMessage = data.errors + .map((e: { message?: string }) => e.message) + .join(", "); + logger.error(`[OAuth2] GraphQL error for ${provider.id}`, { + errors: data.errors, + }); throw new Error(`GraphQL error: ${errorMessage}`); } @@ -478,7 +511,9 @@ function extractUserInfoFromTokens( hasUserInfoMapping: !!provider.userInfoMapping, }, ); - const hash = Buffer.from(tokens.access_token.substring(0, 32)).toString("base64url"); + const hash = Buffer.from(tokens.access_token.substring(0, 32)).toString( + "base64url", + ); return { id: `${provider.id}_${hash}`, raw: tokens, @@ -488,7 +523,10 @@ function extractUserInfoFromTokens( /** * Extract user info using the provider's mapping configuration. */ -function extractUserInfo(mapping: UserInfoMapping | undefined, data: unknown): ExtractedUserInfo { +function extractUserInfo( + mapping: UserInfoMapping | undefined, + data: unknown, +): ExtractedUserInfo { if (!mapping) { // Default mapping for standard OAuth2 claims const obj = data as Record; @@ -497,7 +535,9 @@ function extractUserInfo(mapping: UserInfoMapping | undefined, data: unknown): E email: obj.email as string | undefined, username: obj.username as string | undefined, displayName: (obj.name || obj.display_name) as string | undefined, - avatarUrl: (obj.picture || obj.avatar_url || obj.avatar) as string | undefined, + avatarUrl: (obj.picture || obj.avatar_url || obj.avatar) as + | string + | undefined, raw: obj, }; } @@ -509,12 +549,18 @@ function extractUserInfo(mapping: UserInfoMapping | undefined, data: unknown): E return { id: String(id), - email: mapping.email ? (getNestedValue(data, mapping.email) as string) : undefined, - username: mapping.username ? (getNestedValue(data, mapping.username) as string) : undefined, + email: mapping.email + ? (getNestedValue(data, mapping.email) as string) + : undefined, + username: mapping.username + ? (getNestedValue(data, mapping.username) as string) + : undefined, displayName: mapping.displayName ? (getNestedValue(data, mapping.displayName) as string) : undefined, - avatarUrl: mapping.avatarUrl ? (getNestedValue(data, mapping.avatarUrl) as string) : undefined, + avatarUrl: mapping.avatarUrl + ? (getNestedValue(data, mapping.avatarUrl) as string) + : undefined, raw: data as Record, }; } @@ -552,14 +598,22 @@ async function createOrRotateSecret( errorMsg.includes("duplicate") || errorMsg.includes("unique constraint") ) { - logger.info(`[OAuth2] Secret "${name}" already exists, attempting to rotate`, { - organizationId, - }); + logger.info( + `[OAuth2] Secret "${name}" already exists, attempting to rotate`, + { + organizationId, + }, + ); // Find the existing secret by listing and filtering const allSecrets = await secretsService.list(organizationId); const existingSecret = allSecrets.find((s) => s.name === name); if (existingSecret) { - await secretsService.rotate(existingSecret.id, organizationId, value, audit); + await secretsService.rotate( + existingSecret.id, + organizationId, + value, + audit, + ); return { id: existingSecret.id }; } } @@ -589,7 +643,8 @@ async function storeConnection( // Track newly created secrets for cleanup on failure const newlyCreatedSecretIds: string[] = []; - const providerPlatform = provider.id as (typeof platformCredentialTypeEnum.enumValues)[number]; + const providerPlatform = + provider.id as (typeof platformCredentialTypeEnum.enumValues)[number]; const cleanupNewlyCreatedSecrets = async (context: string) => { if (newlyCreatedSecretIds.length === 0) return; logger.warn( @@ -604,7 +659,10 @@ async function storeConnection( await secretsService.delete(secretId, organizationId, audit); } catch (cleanupError) { logger.error(`[OAuth2] Failed to cleanup secret ${secretId}`, { - error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), + error: + cleanupError instanceof Error + ? cleanupError.message + : String(cleanupError), }); } } @@ -664,7 +722,10 @@ async function storeConnection( accessTokenSecretId = existing[0].access_token_secret_id; } catch (rotateError) { // If secret doesn't exist (orphaned reference), create a new one - const errorMsg = rotateError instanceof Error ? rotateError.message : String(rotateError); + const errorMsg = + rotateError instanceof Error + ? rotateError.message + : String(rotateError); if (errorMsg.includes("not found")) { logger.warn( `[OAuth2] Access token secret not found (orphaned reference), creating new secret`, @@ -699,7 +760,10 @@ async function storeConnection( refreshTokenSecretId = existing[0].refresh_token_secret_id; } catch (rotateError) { // If secret doesn't exist (orphaned reference), create a new one - const errorMsg = rotateError instanceof Error ? rotateError.message : String(rotateError); + const errorMsg = + rotateError instanceof Error + ? rotateError.message + : String(rotateError); if (errorMsg.includes("not found")) { logger.warn( `[OAuth2] Refresh token secret not found (orphaned reference), creating new secret`, @@ -841,7 +905,9 @@ async function storeConnection( return connectionId; } catch (error) { if (insertBlocked) { - await cleanupNewlyCreatedSecrets("Database insert blocked by existing connection"); + await cleanupNewlyCreatedSecrets( + "Database insert blocked by existing connection", + ); throw new Error("OAUTH_ACCOUNT_ALREADY_LINKED"); } @@ -856,16 +922,24 @@ async function storeConnection( export async function refreshOAuth2Token( provider: OAuthProviderConfig, refreshToken: string, -): Promise<{ accessToken: string; expiresIn?: number; newRefreshToken?: string }> { +): Promise<{ + accessToken: string; + expiresIn?: number; + newRefreshToken?: string; +}> { const clientId = getClientId(provider); const clientSecret = getClientSecret(provider); if (!clientId || !clientSecret) { - throw new Error(`OAuth not configured: missing credentials for ${provider.id}`); + throw new Error( + `OAuth not configured: missing credentials for ${provider.id}`, + ); } if (!provider.endpoints?.token) { - throw new Error(`OAuth not configured: missing token endpoint for ${provider.id}`); + throw new Error( + `OAuth not configured: missing token endpoint for ${provider.id}`, + ); } const bodyParams: Record = { @@ -883,7 +957,9 @@ export async function refreshOAuth2Token( if (provider.tokenHeaders) { for (const [key, value] of Object.entries(provider.tokenHeaders)) { if (value === "Basic ${base64(CLIENT_ID:CLIENT_SECRET)}") { - const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); + const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString( + "base64", + ); headers[key] = `Basic ${credentials}`; delete bodyParams.client_id; delete bodyParams.client_secret; diff --git a/packages/lib/services/oauth/token-cache.ts b/packages/lib/services/oauth/token-cache.ts index 95fa06e23..a89669b16 100644 --- a/packages/lib/services/oauth/token-cache.ts +++ b/packages/lib/services/oauth/token-cache.ts @@ -19,7 +19,11 @@ const EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes const DEFAULT_TTL_SECONDS = 60 * 60; // 1 hour for OAuth 1.0a const MAX_TTL_SECONDS = 24 * 60 * 60; // 24 hours max -function getCacheKey(organizationId: string, connectionId: string, version: number): string { +function getCacheKey( + organizationId: string, + connectionId: string, + version: number, +): string { return `oauth_token:v${version}:${organizationId}:${connectionId}`; } @@ -29,7 +33,10 @@ function calculateTTL(expiresAt?: Date): number { const bufferTime = expiresAt.getTime() - EXPIRY_BUFFER_MS; if (bufferTime <= Date.now()) return 0; - return Math.min(Math.floor((bufferTime - Date.now()) / 1000), MAX_TTL_SECONDS); + return Math.min( + Math.floor((bufferTime - Date.now()) / 1000), + MAX_TTL_SECONDS, + ); } export const tokenCache = { @@ -38,7 +45,9 @@ export const tokenCache = { connectionId: string, version: number, ): Promise { - const cached = await cache.get(getCacheKey(organizationId, connectionId, version)); + const cached = await cache.get( + getCacheKey(organizationId, connectionId, version), + ); if (!cached) return null; // Parse expiresAt back to Date (JSON serialization loses Date type) @@ -65,16 +74,30 @@ export const tokenCache = { ): Promise { const ttl = calculateTTL(token.expiresAt); if (ttl <= 0) { - logger.debug("[TokenCache] Token expires too soon, not caching", { connectionId }); + logger.debug("[TokenCache] Token expires too soon, not caching", { + connectionId, + }); return; } const key = getCacheKey(organizationId, connectionId, version); - await cache.set(key, { token: { ...token, fromCache: false }, cachedAt: Date.now() }, ttl); - logger.debug("[TokenCache] Cached token", { connectionId, version, ttlSeconds: ttl }); + await cache.set( + key, + { token: { ...token, fromCache: false }, cachedAt: Date.now() }, + ttl, + ); + logger.debug("[TokenCache] Cached token", { + connectionId, + version, + ttlSeconds: ttl, + }); }, - async invalidate(organizationId: string, connectionId: string, version: number): Promise { + async invalidate( + organizationId: string, + connectionId: string, + version: number, + ): Promise { await cache.del(getCacheKey(organizationId, connectionId, version)); }, @@ -99,6 +122,10 @@ export const tokenCache = { // `oauth_token:v{version}:orgId:platform:orgId` const secretsConnectionId = `${platform}:${organizationId}`; await cache.del(getCacheKey(organizationId, secretsConnectionId, version)); - logger.debug("[TokenCache] Invalidated platform token", { organizationId, platform, version }); + logger.debug("[TokenCache] Invalidated platform token", { + organizationId, + platform, + version, + }); }, }; diff --git a/packages/lib/services/oauth/types.ts b/packages/lib/services/oauth/types.ts index 7edd3afca..dbe4d5b83 100644 --- a/packages/lib/services/oauth/types.ts +++ b/packages/lib/services/oauth/types.ts @@ -7,7 +7,12 @@ export type OAuthProviderType = "oauth2" | "oauth1a" | "api_key"; -export type OAuthConnectionStatus = "pending" | "active" | "expired" | "revoked" | "error"; +export type OAuthConnectionStatus = + | "pending" + | "active" + | "expired" + | "revoked" + | "error"; export type OAuthConnectionSource = "platform_credentials" | "secrets"; diff --git a/packages/lib/services/org-rate-limits.ts b/packages/lib/services/org-rate-limits.ts index 6cc7c69be..becb8c104 100644 --- a/packages/lib/services/org-rate-limits.ts +++ b/packages/lib/services/org-rate-limits.ts @@ -66,11 +66,17 @@ const TIER_THRESHOLDS: ReadonlyArray< ]; /** Sorted highest-first at module load for threshold matching. */ -const SORTED_THRESHOLDS = [...TIER_THRESHOLDS].sort((a, b) => b.minSpend - a.minSpend); +const SORTED_THRESHOLDS = [...TIER_THRESHOLDS].sort( + (a, b) => b.minSpend - a.minSpend, +); const FREE_TIER = SORTED_THRESHOLDS[SORTED_THRESHOLDS.length - 1]; /** Credit transaction metadata types that represent free/bonus credits (excluded from spend). */ -const FREE_CREDIT_TYPES = ["initial_free_credits", "wallet_signup", "signup_code_bonus"]; +const FREE_CREDIT_TYPES = [ + "initial_free_credits", + "wallet_signup", + "signup_code_bonus", +]; const TIER_CACHE_TTL_SECONDS = 3600; // 1h const tierCacheKey = (orgId: string) => `orgtier:${orgId}:v1`; @@ -105,10 +111,13 @@ export async function recalculateOrgTier(orgId: string): Promise { ), ), orgRateLimitOverridesRepository.findByOrganizationId(orgId).catch((err) => { - logger.warn("[OrgRateLimits] Failed to load overrides, using tier defaults", { - orgId, - error: err instanceof Error ? err.message : String(err), - }); + logger.warn( + "[OrgRateLimits] Failed to load overrides, using tier defaults", + { + orgId, + error: err instanceof Error ? err.message : String(err), + }, + ); return undefined; }), ]); @@ -116,7 +125,8 @@ export async function recalculateOrgTier(orgId: string): Promise { const totalSpend = Number.parseFloat(creditResult[0]?.totalSpend ?? "0"); // 2. Match tier (first threshold where totalSpend >= minSpend) - const matchedTier = SORTED_THRESHOLDS.find((t) => totalSpend >= t.minSpend) ?? FREE_TIER; + const matchedTier = + SORTED_THRESHOLDS.find((t) => totalSpend >= t.minSpend) ?? FREE_TIER; // 3. Merge override non-null fields let tierData: OrgTierData = { @@ -146,10 +156,13 @@ export async function recalculateOrgTier(orgId: string): Promise { try { await cache.set(tierCacheKey(orgId), tierData, TIER_CACHE_TTL_SECONDS); } catch (err) { - logger.warn("[OrgRateLimits] Failed to cache tier, will re-query on next request", { - orgId, - error: err instanceof Error ? err.message : String(err), - }); + logger.warn( + "[OrgRateLimits] Failed to cache tier, will re-query on next request", + { + orgId, + error: err instanceof Error ? err.message : String(err), + }, + ); } logger.debug("[OrgRateLimits] Tier computed", { diff --git a/packages/lib/services/organizations.ts b/packages/lib/services/organizations.ts index 202f62d0d..adb52d5ae 100644 --- a/packages/lib/services/organizations.ts +++ b/packages/lib/services/organizations.ts @@ -56,8 +56,12 @@ export class OrganizationsService { return await organizationsRepository.findBySlug(slug); } - async getByStripeCustomerId(stripeCustomerId: string): Promise { - return await organizationsRepository.findByStripeCustomerId(stripeCustomerId); + async getByStripeCustomerId( + stripeCustomerId: string, + ): Promise { + return await organizationsRepository.findByStripeCustomerId( + stripeCustomerId, + ); } async getWithUsers(id: string) { @@ -68,7 +72,10 @@ export class OrganizationsService { return await organizationsRepository.create(data); } - async update(id: string, data: Partial): Promise { + async update( + id: string, + data: Partial, + ): Promise { const result = await organizationsRepository.update(id, data); // Invalidate cache after update await this.invalidateCache(id); @@ -79,7 +86,10 @@ export class OrganizationsService { organizationId: string, amount: number, ): Promise<{ success: boolean; newBalance: number }> { - const result = await organizationsRepository.updateCreditBalance(organizationId, amount); + const result = await organizationsRepository.updateCreditBalance( + organizationId, + amount, + ); // Invalidate cache after balance change await this.invalidateCache(organizationId); return result; diff --git a/packages/lib/services/oxapay.ts b/packages/lib/services/oxapay.ts index 624ef5c99..5ad5cce86 100644 --- a/packages/lib/services/oxapay.ts +++ b/packages/lib/services/oxapay.ts @@ -1,6 +1,14 @@ import { logger } from "@/lib/utils/logger"; -export type OxaPayNetwork = "ERC20" | "TRC20" | "BEP20" | "POLYGON" | "SOL" | "BASE" | "ARB" | "OP"; +export type OxaPayNetwork = + | "ERC20" + | "TRC20" + | "BEP20" + | "POLYGON" + | "SOL" + | "BASE" + | "ARB" + | "OP"; export interface OxaPayInvoiceResult { trackId: string; @@ -63,7 +71,10 @@ async function oxaPayFetch(url: string, options: RequestInit): Promise { if (!response.ok) { logger.error("[OxaPay] HTTP error", { url, status: response.status }); - throw new OxaPayApiError(`OxaPay API returned HTTP ${response.status}`, response.status); + throw new OxaPayApiError( + `OxaPay API returned HTTP ${response.status}`, + response.status, + ); } let data: T; @@ -162,7 +173,11 @@ class OxaPayService { result: data.result, message: data.message, }); - throw new OxaPayApiError(data.message || "Invoice creation failed", undefined, data.result); + throw new OxaPayApiError( + data.message || "Invoice creation failed", + undefined, + data.result, + ); } logger.info("[OxaPay] Invoice created", { @@ -261,17 +276,23 @@ class OxaPayService { }>; }> > { - const data = await oxaPayFetch>(`${OXAPAY_API_BASE}/api/currencies`, { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); + const data = await oxaPayFetch>( + `${OXAPAY_API_BASE}/api/currencies`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); if (!data || typeof data !== "object") { throw new OxaPayApiError("Failed to fetch supported currencies"); } const currencies = Object.entries(data) - .filter(([_, info]: [string, unknown]) => (info as { status?: boolean })?.status) + .filter( + ([_, info]: [string, unknown]) => + (info as { status?: boolean })?.status, + ) .map(([_, info]: [string, unknown]) => { const currency = info as { symbol: string; @@ -305,10 +326,13 @@ class OxaPayService { async getSystemStatus(): Promise { try { - const data = await oxaPayFetch<{ status?: boolean }>(`${OXAPAY_API_BASE}/api/status`, { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); + const data = await oxaPayFetch<{ status?: boolean }>( + `${OXAPAY_API_BASE}/api/status`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); return data?.status === true; } catch { return false; @@ -336,7 +360,11 @@ class OxaPayService { */ isPaymentPending(status: string): boolean { const normalized = status.toLowerCase(); - return normalized === "waiting" || normalized === "paying" || normalized === "confirming"; + return ( + normalized === "waiting" || + normalized === "paying" || + normalized === "confirming" + ); } isPaymentExpired(status: string): boolean { diff --git a/packages/lib/services/pairing-token.ts b/packages/lib/services/pairing-token.ts index 686df5ae8..ab94d1dd4 100644 --- a/packages/lib/services/pairing-token.ts +++ b/packages/lib/services/pairing-token.ts @@ -32,11 +32,17 @@ class PairingTokenService { try { const url = new URL(origin); if (url.hostname.endsWith(a)) { - url.hostname = url.hostname.replace(new RegExp(`${a.replaceAll(".", "\\.")}$`), b); + url.hostname = url.hostname.replace( + new RegExp(`${a.replaceAll(".", "\\.")}$`), + b, + ); return url.origin; } if (url.hostname.endsWith(b)) { - url.hostname = url.hostname.replace(new RegExp(`${b.replaceAll(".", "\\.")}$`), a); + url.hostname = url.hostname.replace( + new RegExp(`${b.replaceAll(".", "\\.")}$`), + a, + ); return url.origin; } } catch { @@ -69,7 +75,10 @@ class PairingTokenService { return token; } - async validateToken(token: string, expectedOrigin?: string | null): Promise { + async validateToken( + token: string, + expectedOrigin?: string | null, + ): Promise { if (!expectedOrigin) { return null; } diff --git a/packages/lib/services/payment-methods.ts b/packages/lib/services/payment-methods.ts index db11d437e..bfe368a67 100644 --- a/packages/lib/services/payment-methods.ts +++ b/packages/lib/services/payment-methods.ts @@ -21,7 +21,9 @@ export class PaymentMethodsService { return org.stripe_customer_id; } - logger.info(`[PaymentMethodsService] Creating Stripe customer for org ${org.id} (${org.name})`); + logger.info( + `[PaymentMethodsService] Creating Stripe customer for org ${org.id} (${org.name})`, + ); try { const customer = await requireStripe().customers.create({ @@ -60,7 +62,10 @@ export class PaymentMethodsService { * @param paymentMethodId - The Stripe payment method ID to attach * @returns void */ - async attachPaymentMethod(organizationId: string, paymentMethodId: string): Promise { + async attachPaymentMethod( + organizationId: string, + paymentMethodId: string, + ): Promise { const org = await organizationsRepository.findById(organizationId); if (!org) { @@ -103,7 +108,10 @@ export class PaymentMethodsService { * @throws Error if organization doesn't have a Stripe customer * @returns void */ - async setDefaultPaymentMethod(organizationId: string, paymentMethodId: string): Promise { + async setDefaultPaymentMethod( + organizationId: string, + paymentMethodId: string, + ): Promise { const org = await organizationsRepository.findById(organizationId); if (!org) { @@ -111,12 +119,15 @@ export class PaymentMethodsService { } if (!org.stripe_customer_id) { - throw new Error("Organization does not have a Stripe customer. Please contact support."); + throw new Error( + "Organization does not have a Stripe customer. Please contact support.", + ); } // Verify the payment method belongs to this customer try { - const paymentMethod = await requireStripe().paymentMethods.retrieve(paymentMethodId); + const paymentMethod = + await requireStripe().paymentMethods.retrieve(paymentMethodId); if (paymentMethod.customer !== org.stripe_customer_id) { throw new Error("Payment method does not belong to this customer"); } @@ -136,7 +147,9 @@ export class PaymentMethodsService { }); } catch (error) { if (error instanceof Error) { - throw new Error(`Failed to update default payment method in Stripe: ${error.message}`); + throw new Error( + `Failed to update default payment method in Stripe: ${error.message}`, + ); } throw error; } @@ -158,7 +171,10 @@ export class PaymentMethodsService { * @throws Error if this is the last payment method and auto-top-up is enabled * @returns void */ - async removePaymentMethod(organizationId: string, paymentMethodId: string): Promise { + async removePaymentMethod( + organizationId: string, + paymentMethodId: string, + ): Promise { const org = await organizationsRepository.findById(organizationId); if (!org) { @@ -229,7 +245,9 @@ export class PaymentMethodsService { * @param organizationId - The organization ID * @returns Array of Stripe payment methods */ - async listPaymentMethods(organizationId: string): Promise { + async listPaymentMethods( + organizationId: string, + ): Promise { const org = await organizationsRepository.findById(organizationId); if (!org?.stripe_customer_id) { @@ -262,7 +280,8 @@ export class PaymentMethodsService { return null; } - const paymentMethod = await requireStripe().paymentMethods.retrieve(paymentMethodId); + const paymentMethod = + await requireStripe().paymentMethods.retrieve(paymentMethodId); // Verify it belongs to this customer if (paymentMethod.customer !== org.stripe_customer_id) { @@ -289,14 +308,19 @@ export class PaymentMethodsService { * @param organizationId - The organization ID * @returns The default payment method or null if none exists */ - async getDefaultPaymentMethod(organizationId: string): Promise { + async getDefaultPaymentMethod( + organizationId: string, + ): Promise { const org = await organizationsRepository.findById(organizationId); if (!org?.stripe_default_payment_method) { return null; } - return await this.getPaymentMethod(organizationId, org.stripe_default_payment_method); + return await this.getPaymentMethod( + organizationId, + org.stripe_default_payment_method, + ); } } diff --git a/packages/lib/services/payout-alerts.ts b/packages/lib/services/payout-alerts.ts index ccd61519b..b904186cf 100644 --- a/packages/lib/services/payout-alerts.ts +++ b/packages/lib/services/payout-alerts.ts @@ -77,7 +77,13 @@ export class PayoutAlertsService { * Send an alert to configured channels */ async sendAlert(payload: AlertPayload): Promise { - const { severity, title, message, details, timestamp = new Date() } = payload; + const { + severity, + title, + message, + details, + timestamp = new Date(), + } = payload; logger.info(`[PayoutAlerts] ${severity.toUpperCase()}: ${title}`, { message, @@ -192,7 +198,11 @@ export class PayoutAlertsService { /** * Alert: Hot wallet balance is low */ - async alertLowBalance(network: string, balance: number, threshold: number): Promise { + async alertLowBalance( + network: string, + balance: number, + threshold: number, + ): Promise { await this.sendAlert({ severity: "high", title: "Low Hot Wallet Balance", @@ -209,7 +219,10 @@ export class PayoutAlertsService { /** * Alert: Velocity limit triggered (possible attack) */ - async alertVelocityLimit(redemptionCount: number, windowMinutes: number): Promise { + async alertVelocityLimit( + redemptionCount: number, + windowMinutes: number, + ): Promise { await this.sendAlert({ severity: "critical", title: "Velocity Limit Triggered", @@ -246,7 +259,10 @@ export class PayoutAlertsService { /** * Alert: Consecutive payout failures */ - async alertConsecutiveFailures(failureCount: number, lastError: string): Promise { + async alertConsecutiveFailures( + failureCount: number, + lastError: string, + ): Promise { await this.sendAlert({ severity: "high", title: "Consecutive Payout Failures", @@ -305,7 +321,10 @@ export class PayoutAlertsService { /** * Alert: Emergency pause activated */ - async alertEmergencyPause(reason: string, activatedBy?: string): Promise { + async alertEmergencyPause( + reason: string, + activatedBy?: string, + ): Promise { await this.sendAlert({ severity: "critical", title: "Emergency Pause Activated", diff --git a/packages/lib/services/payout-processor.ts b/packages/lib/services/payout-processor.ts index 3f3298b7b..43fc71769 100644 --- a/packages/lib/services/payout-processor.ts +++ b/packages/lib/services/payout-processor.ts @@ -34,11 +34,21 @@ import bs58 from "bs58"; import { and, eq, isNull, lt, or, sql } from "drizzle-orm"; -import { type Address, createPublicClient, createWalletClient, http, parseUnits } from "viem"; +import { + type Address, + createPublicClient, + createWalletClient, + http, + parseUnits, +} from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { dbRead, dbWrite } from "@/db/client"; import { tokenRedemptions } from "@/db/schemas/token-redemptions"; -import { ELIZA_DECIMALS, ERC20_ABI, EVM_CHAINS } from "@/lib/config/token-constants"; +import { + ELIZA_DECIMALS, + ERC20_ABI, + EVM_CHAINS, +} from "@/lib/config/token-constants"; import { logger } from "@/lib/utils/logger"; import { ELIZA_TOKEN_ADDRESSES, @@ -97,11 +107,14 @@ interface ProcessingStats { export class PayoutProcessorService { private readonly evmPrivateKey: `0x${string}` | null; private readonly solanaKeypair: import("@solana/web3.js").Keypair | null; - private readonly solanaConnection: import("@solana/web3.js").Connection | null; + private readonly solanaConnection: + | import("@solana/web3.js").Connection + | null; constructor() { // Load EVM private key (support both naming conventions) - const evmKey = process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY; + const evmKey = + process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY; if (evmKey) { this.evmPrivateKey = evmKey.startsWith("0x") ? (evmKey as `0x${string}`) @@ -122,7 +135,8 @@ export class PayoutProcessorService { require("@solana/web3.js") as typeof import("@solana/web3.js"); const decoded = bs58.decode(solanaKey); this.solanaKeypair = Keypair.fromSecretKey(decoded); - const solanaRpc = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; + const solanaRpc = + process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; this.solanaConnection = new Connection(solanaRpc, "confirmed"); logger.info("[PayoutProcessor] Solana hot wallet configured"); } catch (error) { @@ -138,7 +152,9 @@ export class PayoutProcessorService { } else { this.solanaKeypair = null; this.solanaConnection = null; - logger.warn("[PayoutProcessor] SOLANA_PAYOUT_PRIVATE_KEY not set - Solana payouts disabled"); + logger.warn( + "[PayoutProcessor] SOLANA_PAYOUT_PRIVATE_KEY not set - Solana payouts disabled", + ); } } @@ -168,7 +184,9 @@ export class PayoutProcessorService { // Check if any payout method is configured const config = this.isConfigured(); if (!config.any) { - logger.warn("[PayoutProcessor] No payout wallets configured - skipping batch processing"); + logger.warn( + "[PayoutProcessor] No payout wallets configured - skipping batch processing", + ); return stats; } @@ -211,7 +229,11 @@ export class PayoutProcessorService { await this.markCompleted(redemption.id, result.txHash!); stats.succeeded++; } else { - await this.markFailed(redemption.id, result.error!, result.retryable ?? true); + await this.markFailed( + redemption.id, + result.error!, + result.retryable ?? true, + ); stats.failed++; } } @@ -232,7 +254,12 @@ export class PayoutProcessorService { processing_worker_id: PAYOUT_CONFIG.WORKER_ID, updated_at: new Date(), }) - .where(and(eq(tokenRedemptions.id, redemptionId), eq(tokenRedemptions.status, "approved"))) + .where( + and( + eq(tokenRedemptions.id, redemptionId), + eq(tokenRedemptions.status, "approved"), + ), + ) .returning(); return !!updated; @@ -256,7 +283,10 @@ export class PayoutProcessorService { } // Re-validate current price - const priceValidation = await this.validatePrice(network, Number(redemption.eliza_price_usd)); + const priceValidation = await this.validatePrice( + network, + Number(redemption.eliza_price_usd), + ); if (!priceValidation.valid) { return { success: false, @@ -423,10 +453,16 @@ export class PayoutProcessorService { ); // Get source token account (hot wallet's ATA) - const sourceAta = await getAssociatedTokenAddress(mintAddress, this.solanaKeypair.publicKey); + const sourceAta = await getAssociatedTokenAddress( + mintAddress, + this.solanaKeypair.publicKey, + ); // Get or create destination token account - const destinationAta = await getAssociatedTokenAddress(mintAddress, toAddress); + const destinationAta = await getAssociatedTokenAddress( + mintAddress, + toAddress, + ); const transaction = new Transaction(); @@ -455,7 +491,12 @@ export class PayoutProcessorService { // Add transfer instruction transaction.add( - createTransferInstruction(sourceAta, destinationAta, this.solanaKeypair.publicKey, amount), + createTransferInstruction( + sourceAta, + destinationAta, + this.solanaKeypair.publicKey, + amount, + ), ); // Send and confirm transaction @@ -479,7 +520,10 @@ export class PayoutProcessorService { /** * Mark redemption as completed. */ - private async markCompleted(redemptionId: string, txHash: string): Promise { + private async markCompleted( + redemptionId: string, + txHash: string, + ): Promise { await dbWrite .update(tokenRedemptions) .set({ @@ -552,7 +596,9 @@ export class PayoutProcessorService { const account = privateKeyToAccount(this.evmPrivateKey); for (const [network, chain] of Object.entries(EVM_CHAINS)) { - const tokenAddress = ELIZA_TOKEN_ADDRESSES[network as SupportedNetwork] as Address; + const tokenAddress = ELIZA_TOKEN_ADDRESSES[ + network as SupportedNetwork + ] as Address; const publicClient = createPublicClient({ chain, @@ -567,7 +613,8 @@ export class PayoutProcessorService { }); const balanceFormatted = - Number(balance) / 10 ** ELIZA_DECIMALS[network as keyof typeof ELIZA_DECIMALS]; + Number(balance) / + 10 ** ELIZA_DECIMALS[network as keyof typeof ELIZA_DECIMALS]; result.evm.balances[network] = balanceFormatted; if (balanceFormatted < PAYOUT_CONFIG.MIN_HOT_WALLET_BALANCE) { @@ -586,18 +633,26 @@ export class PayoutProcessorService { } } } else { - logger.info("[PayoutProcessor] EVM wallet not configured - skipping EVM balance check"); + logger.info( + "[PayoutProcessor] EVM wallet not configured - skipping EVM balance check", + ); } // Check Solana wallet if (this.solanaKeypair && this.solanaConnection) { - const { PublicKey } = require("@solana/web3.js") as typeof import("@solana/web3.js"); + const { PublicKey } = + require("@solana/web3.js") as typeof import("@solana/web3.js"); const { getAssociatedTokenAddress, getAccount } = require("@solana/spl-token") as typeof import("@solana/spl-token"); const mintAddress = new PublicKey(ELIZA_TOKEN_ADDRESSES.solana); - const ata = await getAssociatedTokenAddress(mintAddress, this.solanaKeypair.publicKey); + const ata = await getAssociatedTokenAddress( + mintAddress, + this.solanaKeypair.publicKey, + ); - const account = await getAccount(this.solanaConnection, ata).catch(() => null); + const account = await getAccount(this.solanaConnection, ata).catch( + () => null, + ); if (!account) { logger.warn("[PayoutProcessor] Solana token account not found", { @@ -605,7 +660,8 @@ export class PayoutProcessorService { }); result.solana.balance = 0; } else { - const balanceFormatted = Number(account.amount) / 10 ** ELIZA_DECIMALS.solana; + const balanceFormatted = + Number(account.amount) / 10 ** ELIZA_DECIMALS.solana; result.solana.balance = balanceFormatted; if (balanceFormatted < PAYOUT_CONFIG.MIN_HOT_WALLET_BALANCE) { @@ -624,7 +680,9 @@ export class PayoutProcessorService { } } } else { - logger.info("[PayoutProcessor] Solana wallet not configured - skipping Solana balance check"); + logger.info( + "[PayoutProcessor] Solana wallet not configured - skipping Solana balance check", + ); } return result; diff --git a/packages/lib/services/payout-status.ts b/packages/lib/services/payout-status.ts index ee597f483..dcadbd0fc 100644 --- a/packages/lib/services/payout-status.ts +++ b/packages/lib/services/payout-status.ts @@ -9,7 +9,10 @@ import { type Address, createPublicClient, http, parseAbi } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { ELIZA_DECIMALS, EVM_CHAINS } from "@/lib/config/token-constants"; import { logger } from "@/lib/utils/logger"; -import { ELIZA_TOKEN_ADDRESSES, type SupportedNetwork } from "./eliza-token-price"; +import { + ELIZA_TOKEN_ADDRESSES, + type SupportedNetwork, +} from "./eliza-token-price"; // ============================================================================ // TYPES @@ -55,7 +58,12 @@ class PayoutStatusService { */ async getStatus(forceRefresh = false): Promise { // Return cached if valid - if (!forceRefresh && this.cachedStatus && this.cacheExpiry && new Date() < this.cacheExpiry) { + if ( + !forceRefresh && + this.cachedStatus && + this.cacheExpiry && + new Date() < this.cacheExpiry + ) { return this.cachedStatus; } @@ -63,8 +71,11 @@ class PayoutStatusService { const warnings: string[] = []; // Check EVM networks (support both naming conventions) - const evmPrivateKey = process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY; - const evmWalletAddress = evmPrivateKey ? this.getEvmWalletAddress(evmPrivateKey) : null; + const evmPrivateKey = + process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY; + const evmWalletAddress = evmPrivateKey + ? this.getEvmWalletAddress(evmPrivateKey) + : null; for (const network of ["ethereum", "base", "bnb"] as const) { const status = await this.checkEvmNetwork(network, evmWalletAddress); @@ -89,7 +100,9 @@ class PayoutStatusService { } // Determine overall operational status - const operationalNetworks = networks.filter((n) => n.status === "operational"); + const operationalNetworks = networks.filter( + (n) => n.status === "operational", + ); const operational = operationalNetworks.length > 0; // Add general warnings @@ -144,7 +157,9 @@ class PayoutStatusService { * Get user-friendly message for payout unavailability */ getUserMessage(network?: SupportedNetwork): string | null { - const evmConfigured = !!(process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY); + const evmConfigured = !!( + process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY + ); const solanaConfigured = !!process.env.SOLANA_PAYOUT_PRIVATE_KEY; if (!evmConfigured && !solanaConfigured) { @@ -209,7 +224,9 @@ class PayoutStatusService { transport: http(), }); - const ERC20_ABI = parseAbi(["function balanceOf(address account) view returns (uint256)"]); + const ERC20_ABI = parseAbi([ + "function balanceOf(address account) view returns (uint256)", + ]); let balance = 0; let error: string | null = null; @@ -278,7 +295,9 @@ class PayoutStatusService { }; } - private async checkSolanaNetwork(walletAddress: string | null): Promise { + private async checkSolanaNetwork( + walletAddress: string | null, + ): Promise { if (!walletAddress) { return { network: "solana", @@ -291,7 +310,8 @@ class PayoutStatusService { }; } - const solanaRpc = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; + const solanaRpc = + process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; const { Connection, PublicKey } = require("@solana/web3.js") as typeof import("@solana/web3.js"); const { getAssociatedTokenAddress, getAccount } = diff --git a/packages/lib/services/provider-health.ts b/packages/lib/services/provider-health.ts index ffc61ee78..c01d8b5fe 100644 --- a/packages/lib/services/provider-health.ts +++ b/packages/lib/services/provider-health.ts @@ -30,7 +30,12 @@ export class ProviderHealthService { responseTime?: number, errorRate?: number, ): Promise { - return await providerHealthRepository.updateStatus(provider, status, responseTime, errorRate); + return await providerHealthRepository.updateStatus( + provider, + status, + responseTime, + errorRate, + ); } } diff --git a/packages/lib/services/provisioning-jobs.ts b/packages/lib/services/provisioning-jobs.ts index b2b64915d..cdb05e87e 100644 --- a/packages/lib/services/provisioning-jobs.ts +++ b/packages/lib/services/provisioning-jobs.ts @@ -117,7 +117,9 @@ export class ProvisioningJobService { }; return await dbWrite.transaction(async (tx) => { - await tx.execute(miladyProvisionAdvisoryLockSql(params.organizationId, params.agentId)); + await tx.execute( + miladyProvisionAdvisoryLockSql(params.organizationId, params.agentId), + ); const [sandbox] = await tx .select({ @@ -139,9 +141,15 @@ export class ProvisioningJobService { if (params.expectedUpdatedAt) { const expectedMs = new Date(params.expectedUpdatedAt).getTime(); - const currentMs = sandbox.updated_at ? new Date(sandbox.updated_at).getTime() : Number.NaN; - - if (Number.isFinite(expectedMs) && Number.isFinite(currentMs) && currentMs !== expectedMs) { + const currentMs = sandbox.updated_at + ? new Date(sandbox.updated_at).getTime() + : Number.NaN; + + if ( + Number.isFinite(expectedMs) && + Number.isFinite(currentMs) && + currentMs !== expectedMs + ) { throw new Error("Agent state changed while starting"); } } @@ -191,7 +199,10 @@ export class ProvisioningJobService { /** * Get a job by ID scoped to a single organization. */ - async getJobForOrg(jobId: string, organizationId: string): Promise { + async getJobForOrg( + jobId: string, + organizationId: string, + ): Promise { return jobsRepository.findByIdAndOrg(jobId, organizationId); } @@ -276,28 +287,44 @@ export class ProvisioningJobService { result.errors.push({ jobId: job.id, error: errorMsg }); // Increment attempt; will auto-fail if max_attempts reached - const updated = await jobsRepository.incrementAttempt(job.id, errorMsg, job.max_attempts); + const updated = await jobsRepository.incrementAttempt( + job.id, + errorMsg, + job.max_attempts, + ); // When retries are exhausted (permanent failure), mark the // sandbox as "error" immediately so the UI reflects reality // instead of staying stuck in "provisioning". - if (updated?.status === "failed" && job.type === JOB_TYPES.MILADY_PROVISION) { + if ( + updated?.status === "failed" && + job.type === JOB_TYPES.MILADY_PROVISION + ) { const data = job.data as unknown as MiladyProvisionJobData; try { await miladySandboxesRepository.update(data.agentId, { status: "error", error_message: `Provisioning permanently failed after ${job.max_attempts} attempts: ${errorMsg}`, } as Parameters[1]); - logger.warn("[provisioning-jobs] Marked sandbox as error after permanent failure", { - jobId: job.id, - agentId: data.agentId, - }); + logger.warn( + "[provisioning-jobs] Marked sandbox as error after permanent failure", + { + jobId: job.id, + agentId: data.agentId, + }, + ); } catch (sandboxErr) { - logger.error("[provisioning-jobs] Failed to mark sandbox as error", { - jobId: job.id, - agentId: data.agentId, - error: sandboxErr instanceof Error ? sandboxErr.message : String(sandboxErr), - }); + logger.error( + "[provisioning-jobs] Failed to mark sandbox as error", + { + jobId: job.id, + agentId: data.agentId, + error: + sandboxErr instanceof Error + ? sandboxErr.message + : String(sandboxErr), + }, + ); } } } @@ -331,7 +358,10 @@ export class ProvisioningJobService { agentId: data.agentId, }); - const provResult = await miladySandboxService.provision(data.agentId, data.organizationId); + const provResult = await miladySandboxService.provision( + data.agentId, + data.organizationId, + ); if (!provResult.success) { // Store partial result for debugging @@ -387,7 +417,10 @@ export class ProvisioningJobService { return totalRecovered; } - private async fireWebhook(job: Job, result: MiladyProvisionJobResult): Promise { + private async fireWebhook( + job: Job, + result: MiladyProvisionJobResult, + ): Promise { if (!job.webhook_url) return; try { diff --git a/packages/lib/services/proxy-billing.ts b/packages/lib/services/proxy-billing.ts index ae56faf6a..c886937ad 100644 --- a/packages/lib/services/proxy-billing.ts +++ b/packages/lib/services/proxy-billing.ts @@ -37,7 +37,10 @@ export async function deductProxyCredits(params: { return cost; } -export async function hasProxyCredits(organizationId: string, service: string): Promise { +export async function hasProxyCredits( + organizationId: string, + service: string, +): Promise { const cost = getProxyCost(service); const org = await organizationsRepository.findById(organizationId); if (!org) return false; @@ -45,4 +48,8 @@ export async function hasProxyCredits(organizationId: string, service: string): return balance >= cost; } -export const proxyBillingService = { getProxyCost, deductProxyCredits, hasProxyCredits }; +export const proxyBillingService = { + getProxyCost, + deductProxyCredits, + hasProxyCredits, +}; diff --git a/packages/lib/services/proxy/config.ts b/packages/lib/services/proxy/config.ts index 30391221f..edaf5d9b6 100644 --- a/packages/lib/services/proxy/config.ts +++ b/packages/lib/services/proxy/config.ts @@ -22,7 +22,9 @@ export const PROXY_CONFIG = { * - Higher TTL: Lower DB load, longer stale data exposure */ PRICING_CACHE_TTL: parseInt(process.env.PRICING_CACHE_TTL || "300"), - PRICING_CACHE_STALE_TIME: parseInt(process.env.PRICING_CACHE_STALE_TIME || "150"), + PRICING_CACHE_STALE_TIME: parseInt( + process.env.PRICING_CACHE_STALE_TIME || "150", + ), // Request timeouts (milliseconds) UPSTREAM_TIMEOUT_MS: parseInt(process.env.UPSTREAM_TIMEOUT_MS || "25000"), @@ -31,8 +33,10 @@ export const PROXY_CONFIG = { MAX_BATCH_SIZE: parseInt(process.env.MAX_BATCH_SIZE || "20"), // Helius RPC configuration - HELIUS_MAINNET_URL: process.env.HELIUS_MAINNET_URL || "https://mainnet.helius-rpc.com", - HELIUS_DEVNET_URL: process.env.HELIUS_DEVNET_URL || "https://devnet.helius-rpc.com", + HELIUS_MAINNET_URL: + process.env.HELIUS_MAINNET_URL || "https://mainnet.helius-rpc.com", + HELIUS_DEVNET_URL: + process.env.HELIUS_DEVNET_URL || "https://devnet.helius-rpc.com", // Fallback RPC URLs (used when primary fails) HELIUS_MAINNET_FALLBACK_URL: process.env.HELIUS_MAINNET_FALLBACK_URL, @@ -40,22 +44,35 @@ export const PROXY_CONFIG = { // Retry configuration RPC_MAX_RETRIES: parseInt(process.env.RPC_MAX_RETRIES || "5"), - RPC_INITIAL_RETRY_DELAY_MS: parseInt(process.env.RPC_INITIAL_RETRY_DELAY_MS || "1000"), - RPC_MAX_RETRY_DELAY_MS: parseInt(process.env.RPC_MAX_RETRY_DELAY_MS || "16000"), + RPC_INITIAL_RETRY_DELAY_MS: parseInt( + process.env.RPC_INITIAL_RETRY_DELAY_MS || "1000", + ), + RPC_MAX_RETRY_DELAY_MS: parseInt( + process.env.RPC_MAX_RETRY_DELAY_MS || "16000", + ), // Reduced retries for expensive methods (tier 2+: 10-100 provider credits per call). // Helius charges per request, not per successful response, so every retry is real spend. - RPC_EXPENSIVE_MAX_RETRIES: parseInt(process.env.RPC_EXPENSIVE_MAX_RETRIES || "2"), + RPC_EXPENSIVE_MAX_RETRIES: parseInt( + process.env.RPC_EXPENSIVE_MAX_RETRIES || "2", + ), // Circuit breaker — stops retrying when upstream is consistently failing. // Opens after THRESHOLD consecutive failures, stays open for OPEN_DURATION_MS, // then allows one probe request (half-open). Success resets the circuit. - RPC_CIRCUIT_FAILURE_THRESHOLD: parseInt(process.env.RPC_CIRCUIT_FAILURE_THRESHOLD || "10"), - RPC_CIRCUIT_OPEN_DURATION_MS: parseInt(process.env.RPC_CIRCUIT_OPEN_DURATION_MS || "30000"), + RPC_CIRCUIT_FAILURE_THRESHOLD: parseInt( + process.env.RPC_CIRCUIT_FAILURE_THRESHOLD || "10", + ), + RPC_CIRCUIT_OPEN_DURATION_MS: parseInt( + process.env.RPC_CIRCUIT_OPEN_DURATION_MS || "30000", + ), // Market Data API configuration - MARKET_DATA_BASE_URL: process.env.MARKET_DATA_BASE_URL || "https://public-api.birdeye.so", - MARKET_DATA_TIMEOUT_MS: parseInt(process.env.MARKET_DATA_TIMEOUT_MS || "15000"), + MARKET_DATA_BASE_URL: + process.env.MARKET_DATA_BASE_URL || "https://public-api.birdeye.so", + MARKET_DATA_TIMEOUT_MS: parseInt( + process.env.MARKET_DATA_TIMEOUT_MS || "15000", + ), MARKET_DATA_MAX_RETRIES: parseInt(process.env.MARKET_DATA_MAX_RETRIES || "3"), MARKET_DATA_INITIAL_RETRY_DELAY_MS: parseInt( process.env.MARKET_DATA_INITIAL_RETRY_DELAY_MS || "500", @@ -64,6 +81,8 @@ export const PROXY_CONFIG = { // Alchemy EVM RPC configuration ALCHEMY_TIMEOUT_MS: parseInt(process.env.ALCHEMY_TIMEOUT_MS || "25000"), ALCHEMY_MAX_RETRIES: parseInt(process.env.ALCHEMY_MAX_RETRIES || "3"), - ALCHEMY_INITIAL_RETRY_DELAY_MS: parseInt(process.env.ALCHEMY_INITIAL_RETRY_DELAY_MS || "500"), + ALCHEMY_INITIAL_RETRY_DELAY_MS: parseInt( + process.env.ALCHEMY_INITIAL_RETRY_DELAY_MS || "500", + ), ALCHEMY_MAX_BATCH_SIZE: parseInt(process.env.ALCHEMY_MAX_BATCH_SIZE || "20"), } as const; diff --git a/packages/lib/services/proxy/cors.ts b/packages/lib/services/proxy/cors.ts index 4dc87f25e..0903fedad 100644 --- a/packages/lib/services/proxy/cors.ts +++ b/packages/lib/services/proxy/cors.ts @@ -30,7 +30,10 @@ export function handleCorsOptions(methods: string): Response { }); } -export function applyCorsHeaders(response: Response, methods?: string): Response { +export function applyCorsHeaders( + response: Response, + methods?: string, +): Response { const headers = new Headers(response.headers); for (const [key, value] of Object.entries(getCorsHeaders(methods))) { diff --git a/packages/lib/services/proxy/engine.ts b/packages/lib/services/proxy/engine.ts index c4e80bbd4..4ba366930 100644 --- a/packages/lib/services/proxy/engine.ts +++ b/packages/lib/services/proxy/engine.ts @@ -9,7 +9,10 @@ import { } from "@/lib/auth"; import { cache } from "@/lib/cache/client"; import { withRateLimit } from "@/lib/middleware/rate-limit"; -import { creditsService, InsufficientCreditsError } from "@/lib/services/credits"; +import { + creditsService, + InsufficientCreditsError, +} from "@/lib/services/credits"; import { usageService } from "@/lib/services/usage"; import { logger } from "@/lib/utils/logger"; import { PricingNotFoundError } from "./pricing"; @@ -70,7 +73,9 @@ function getMethodFromBody(body: ProxyRequestBody): string { return "_batch"; } - return body && typeof body === "object" && "method" in body ? String(body.method) : "_default"; + return body && typeof body === "object" && "method" in body + ? String(body.method) + : "_default"; } function isCacheableResponseContentType(response: Response): boolean { @@ -107,7 +112,10 @@ function withCacheHeaders( return headers; } -function wrapResponseWithHeaders(response: Response, headers: Headers): Response { +function wrapResponseWithHeaders( + response: Response, + headers: Headers, +): Response { return new Response(response.body, { status: response.status, statusText: response.statusText, @@ -187,9 +195,14 @@ export function createHandler( const cachedResponse = await cache.get(cacheKey); if (cachedResponse) { - const age = Math.floor((Date.now() - cachedResponse.cachedAt) / 1000); + const age = Math.floor( + (Date.now() - cachedResponse.cachedAt) / 1000, + ); const storedMaxAge = cachedResponse.ttl ?? config.cache!.maxTTL; - const effectiveMaxAge = Math.min(cacheCandidate.clientMaxAge, storedMaxAge); + const effectiveMaxAge = Math.min( + cacheCandidate.clientMaxAge, + storedMaxAge, + ); if (age <= effectiveMaxAge) { const hitMultiplier = config.cache?.hitCostMultiplier ?? 0.5; @@ -200,7 +213,12 @@ export function createHandler( const response = new Response(cachedResponse.body, { status: cachedResponse.status, - headers: withCacheHeaders(cachedResponse.headers, "HIT", remainingMaxAge, age), + headers: withCacheHeaders( + cachedResponse.headers, + "HIT", + remainingMaxAge, + age, + ), }); void (async () => { @@ -225,7 +243,10 @@ export function createHandler( }, }); } catch (error) { - logger.error("[Proxy Engine] Usage tracking failed (cache hit)", { error }); + logger.error( + "[Proxy Engine] Usage tracking failed (cache hit)", + { error }, + ); } })(); @@ -251,13 +272,20 @@ export function createHandler( if (cacheKey && cacheCandidate && config.cache) { const ttl = Math.min(cacheCandidate.clientMaxAge, config.cache.maxTTL); - if (result.response.ok && isCacheableResponseContentType(result.response)) { + if ( + result.response.ok && + isCacheableResponseContentType(result.response) + ) { const clonedResponse = result.response.clone(); const responseBody = await clonedResponse.text(); const maxSize = config.cache.maxResponseSize ?? 65536; if (responseBody.length <= maxSize) { - const responseHeaders = withCacheHeaders(result.response.headers, "MISS", ttl); + const responseHeaders = withCacheHeaders( + result.response.headers, + "MISS", + ttl, + ); const headersObj: Record = {}; responseHeaders.forEach((value, key) => { headersObj[key] = value; @@ -309,7 +337,9 @@ export function createHandler( markup: "0", duration_ms: Date.now() - startTime, is_successful: result.response.ok, - error_message: result.response.ok ? undefined : `HTTP ${result.response.status}`, + error_message: result.response.ok + ? undefined + : `HTTP ${result.response.status}`, metadata: { cached: false, method, @@ -335,7 +365,10 @@ export function createHandler( } if (error instanceof ApiError) { - return NextResponse.json({ error: error.message }, { status: error.status }); + return NextResponse.json( + { error: error.message }, + { status: error.status }, + ); } if (error instanceof PricingNotFoundError) { @@ -343,7 +376,10 @@ export function createHandler( serviceId: error.serviceId, method: error.method, }); - return NextResponse.json({ error: "Service temporarily unavailable" }, { status: 500 }); + return NextResponse.json( + { error: "Service temporarily unavailable" }, + { status: 500 }, + ); } if (error instanceof Error) { @@ -351,13 +387,22 @@ export function createHandler( return NextResponse.json({ error: error.message }, { status: 400 }); } - if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) { - return NextResponse.json({ error: "Upstream service timeout" }, { status: 504 }); + if ( + error.name === "TimeoutError" || + error.message.toLowerCase().includes("timeout") + ) { + return NextResponse.json( + { error: "Upstream service timeout" }, + { status: 504 }, + ); } } logger.error("[Proxy Engine] Handler error", { error }); - return NextResponse.json({ error: "Upstream service error" }, { status: 502 }); + return NextResponse.json( + { error: "Upstream service error" }, + { status: 502 }, + ); } }; diff --git a/packages/lib/services/proxy/fetch.ts b/packages/lib/services/proxy/fetch.ts index 2fd169930..464cecfb2 100644 --- a/packages/lib/services/proxy/fetch.ts +++ b/packages/lib/services/proxy/fetch.ts @@ -50,7 +50,10 @@ function sanitizeUrl(url: string): string { * - 404 Not Found: resource doesn't exist, retrying won't help * - 5xx errors ARE retriable: server issues may be transient */ -export async function retryFetch(opts: RetryFetchOptions, attempt: number = 1): Promise { +export async function retryFetch( + opts: RetryFetchOptions, + attempt: number = 1, +): Promise { const { url, init, diff --git a/packages/lib/services/proxy/pricing.ts b/packages/lib/services/proxy/pricing.ts index 11023bd76..c5fce1ee0 100644 --- a/packages/lib/services/proxy/pricing.ts +++ b/packages/lib/services/proxy/pricing.ts @@ -19,7 +19,9 @@ export class PricingNotFoundError extends Error { } } -async function loadPricingMap(serviceId: string): Promise> { +async function loadPricingMap( + serviceId: string, +): Promise> { const existingLoad = inflightPricingLoads.get(serviceId); if (existingLoad) { return existingLoad; @@ -27,7 +29,8 @@ async function loadPricingMap(serviceId: string): Promise const cacheKey = `service-pricing:${serviceId}`; const loadPromise = (async () => { - const pricingRecords = await servicePricingRepository.listByService(serviceId); + const pricingRecords = + await servicePricingRepository.listByService(serviceId); const pricingMap: Record = {}; if (pricingRecords.length === 0) { @@ -53,7 +56,10 @@ async function loadPricingMap(serviceId: string): Promise return loadPromise; } -export async function getServiceMethodCost(serviceId: string, method: string): Promise { +export async function getServiceMethodCost( + serviceId: string, + method: string, +): Promise { const cacheKey = `service-pricing:${serviceId}`; const cached = await cache.get>(cacheKey); @@ -86,7 +92,9 @@ export async function getServiceMethodCost(serviceId: string, method: string): P return Number(cost); } -export async function invalidateServicePricingCache(serviceId: string): Promise { +export async function invalidateServicePricingCache( + serviceId: string, +): Promise { const cacheKey = `service-pricing:${serviceId}`; inflightPricingLoads.delete(serviceId); await cache.del(cacheKey); @@ -131,5 +139,8 @@ export async function calculateBatchCost( }), ); - return methods.reduce((total, method) => total + (costMap.get(method) ?? 0), 0); + return methods.reduce( + (total, method) => total + (costMap.get(method) ?? 0), + 0, + ); } diff --git a/packages/lib/services/proxy/services/chain-data.ts b/packages/lib/services/proxy/services/chain-data.ts index cf9d92be4..c59678c71 100644 --- a/packages/lib/services/proxy/services/chain-data.ts +++ b/packages/lib/services/proxy/services/chain-data.ts @@ -60,13 +60,16 @@ const PROVIDER_METHODS: Record = { // Filter out null/undefined values to avoid sending them to Alchemy buildRpcParams: (p) => { const params: Record = { - category: p.category ? [String(p.category)] : ["external", "erc20", "erc721", "erc1155"], + category: p.category + ? [String(p.category)] + : ["external", "erc20", "erc721", "erc1155"], }; if (p.fromAddress) params.fromAddress = String(p.fromAddress); if (p.toAddress) params.toAddress = String(p.toAddress); if (p.pageKey) params.pageKey = String(p.pageKey); const count = Number(p.maxCount); - params.maxCount = !p.maxCount || Number.isNaN(count) ? "0x64" : "0x" + count.toString(16); + params.maxCount = + !p.maxCount || Number.isNaN(count) ? "0x64" : "0x" + count.toString(16); return [params]; }, }, diff --git a/packages/lib/services/proxy/services/market-data.ts b/packages/lib/services/proxy/services/market-data.ts index f0e5069ff..0c749cd4d 100644 --- a/packages/lib/services/proxy/services/market-data.ts +++ b/packages/lib/services/proxy/services/market-data.ts @@ -47,7 +47,11 @@ const PROVIDER_PATHS: Record = { * - Cache hit rate would be extremely low * - Wasted Redis memory storing rare queries */ -const NON_CACHEABLE_METHODS = new Set(["getTokenTrades", "getTrending", "search"]); +const NON_CACHEABLE_METHODS = new Set([ + "getTokenTrades", + "getTrending", + "search", +]); export interface MarketDataRequest { method: string; diff --git a/packages/lib/services/proxy/services/rpc.ts b/packages/lib/services/proxy/services/rpc.ts index 9fef809b9..d956320d6 100644 --- a/packages/lib/services/proxy/services/rpc.ts +++ b/packages/lib/services/proxy/services/rpc.ts @@ -210,7 +210,8 @@ function buildEvmRpcHandler(chain: string): ServiceHandler { throw new Error("ALCHEMY_API_KEY not configured"); } - const slugMap = network === "mainnet" ? ALCHEMY_SLUGS : ALCHEMY_TESTNET_SLUGS; + const slugMap = + network === "mainnet" ? ALCHEMY_SLUGS : ALCHEMY_TESTNET_SLUGS; const slug = slugMap[chain]; if (!slug) { @@ -270,7 +271,10 @@ function buildEvmRpcHandler(chain: string): ServiceHandler { /** * Supported RPC chains (Solana + all Alchemy EVM chains) */ -export const SUPPORTED_RPC_CHAINS = new Set(["solana", ...Object.keys(ALCHEMY_SLUGS)]); +export const SUPPORTED_RPC_CHAINS = new Set([ + "solana", + ...Object.keys(ALCHEMY_SLUGS), +]); /** * Check if chain is supported for RPC diff --git a/packages/lib/services/proxy/services/solana-rpc.ts b/packages/lib/services/proxy/services/solana-rpc.ts index e78364672..beaf07015 100644 --- a/packages/lib/services/proxy/services/solana-rpc.ts +++ b/packages/lib/services/proxy/services/solana-rpc.ts @@ -183,7 +183,8 @@ async function getAllowedMethods(): Promise> { logger.debug("[Solana RPC] Allowed methods cache miss, querying database"); // Query database for active methods - const pricingRecords = await servicePricingRepository.listByService("solana-rpc"); + const pricingRecords = + await servicePricingRepository.listByService("solana-rpc"); const activeMethods = pricingRecords .filter((record) => record.is_active) .map((record) => record.method); @@ -194,7 +195,11 @@ async function getAllowedMethods(): Promise> { } // Cache the result - await cache.set(ALLOWED_METHODS_CACHE_KEY, activeMethods, ALLOWED_METHODS_CACHE_TTL); + await cache.set( + ALLOWED_METHODS_CACHE_KEY, + activeMethods, + ALLOWED_METHODS_CACHE_TTL, + ); logger.info("[Solana RPC] Loaded allowed methods from database", { count: activeMethods.length, @@ -204,10 +209,13 @@ async function getAllowedMethods(): Promise> { return new Set(activeMethods); } catch (error) { // Database or cache failure - use hardcoded fallback - logger.error("[Solana RPC] Failed to load allowed methods from database, using fallback", { - error: error instanceof Error ? error.message : "Unknown error", - fallback_count: HARDCODED_FALLBACK_METHODS.size, - }); + logger.error( + "[Solana RPC] Failed to load allowed methods from database, using fallback", + { + error: error instanceof Error ? error.message : "Unknown error", + fallback_count: HARDCODED_FALLBACK_METHODS.size, + }, + ); return HARDCODED_FALLBACK_METHODS; } @@ -237,7 +245,9 @@ async function extractMethodFromBody(body: ProxyRequestBody): Promise { throw new Error("Invalid JSON-RPC batch: empty array"); } if (body.length > PROXY_CONFIG.MAX_BATCH_SIZE) { - throw new Error(`Invalid JSON-RPC batch: maximum ${PROXY_CONFIG.MAX_BATCH_SIZE} requests`); + throw new Error( + `Invalid JSON-RPC batch: maximum ${PROXY_CONFIG.MAX_BATCH_SIZE} requests`, + ); } return "_batch"; } @@ -285,7 +295,10 @@ async function calculateBatchCost(body: JsonRpcBatchRequest): Promise { })), ); - return costs.reduce((total, { method, cost }) => total + cost * methodCounts.get(method)!, 0); + return costs.reduce( + (total, { method, cost }) => total + cost * methodCounts.get(method)!, + 0, + ); } export const solanaRpcConfig: ServiceConfig = { @@ -331,7 +344,10 @@ function sanitizeUrl(url: string): string { // • success → circuit closes (state deleted) // • failure → circuit re-opens immediately (failures still >= threshold) // --------------------------------------------------------------------------- -const circuitBreaker = new Map(); +const circuitBreaker = new Map< + string, + { failures: number; openUntil: number } +>(); function isCircuitOpen(network: string): boolean { const cb = circuitBreaker.get(network); @@ -433,7 +449,11 @@ async function fetchWithRetry( fetchLogs.push({ attempt, url: sanitized, - error: isTimeout ? "TimeoutError" : error instanceof Error ? error.message : String(error), + error: isTimeout + ? "TimeoutError" + : error instanceof Error + ? error.message + : String(error), durationMs: Date.now() - start, }); @@ -443,7 +463,11 @@ async function fetchWithRetry( if (attempt < maxAttempts) { const delayMs = Math.min(initialDelay * 2 ** (attempt - 1), maxDelay); - logger.warn("[Solana RPC] Retrying", { attempt, delayMs, url: sanitized }); + logger.warn("[Solana RPC] Retrying", { + attempt, + delayMs, + url: sanitized, + }); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } @@ -452,7 +476,10 @@ async function fetchWithRetry( throw lastError!; } -export const solanaRpcHandler: ServiceHandler = async ({ body, searchParams }) => { +export const solanaRpcHandler: ServiceHandler = async ({ + body, + searchParams, +}) => { if (!body) { throw new Error("Invalid JSON-RPC request: body is required"); } @@ -484,7 +511,9 @@ export const solanaRpcHandler: ServiceHandler = async ({ body, searchParams }) = } const primaryBaseUrl = - network === "mainnet" ? PROXY_CONFIG.HELIUS_MAINNET_URL : PROXY_CONFIG.HELIUS_DEVNET_URL; + network === "mainnet" + ? PROXY_CONFIG.HELIUS_MAINNET_URL + : PROXY_CONFIG.HELIUS_DEVNET_URL; const fallbackBaseUrl = network === "mainnet" @@ -502,9 +531,15 @@ export const solanaRpcHandler: ServiceHandler = async ({ body, searchParams }) = const primary = await fetchWithRetry(primaryUrl, rpcBody, maxRetries); fetchLogs.push(...primary.fetchLogs); - if (primary.response.ok || (primary.response.status >= 400 && primary.response.status < 500)) { + if ( + primary.response.ok || + (primary.response.status >= 400 && primary.response.status < 500) + ) { recordCircuitSuccess(network); - return { response: primary.response, usageMetadata: { fetch_logs: fetchLogs } }; + return { + response: primary.response, + usageMetadata: { fetch_logs: fetchLogs }, + }; } primaryStatus = primary.response.status; @@ -539,7 +574,10 @@ export const solanaRpcHandler: ServiceHandler = async ({ body, searchParams }) = if (fallback.response.ok) { logger.info("[Solana RPC] Fallback succeeded"); recordCircuitSuccess(network); - return { response: fallback.response, usageMetadata: { fetch_logs: fetchLogs } }; + return { + response: fallback.response, + usageMetadata: { fetch_logs: fetchLogs }, + }; } const fallbackError = await fallback.response.text(); diff --git a/packages/lib/services/proxy/types.ts b/packages/lib/services/proxy/types.ts index fadeaf7ae..2876a33aa 100644 --- a/packages/lib/services/proxy/types.ts +++ b/packages/lib/services/proxy/types.ts @@ -2,7 +2,11 @@ import type { NextRequest } from "next/server"; import type { UserWithOrganization } from "@/db/repositories/users"; import type { ApiKey } from "@/db/schemas/api-keys"; -export type AuthLevel = "session" | "sessionWithOrg" | "apiKey" | "apiKeyWithOrg"; +export type AuthLevel = + | "session" + | "sessionWithOrg" + | "apiKey" + | "apiKeyWithOrg"; export interface CacheConfig { maxTTL: number; @@ -46,7 +50,10 @@ export interface ServiceConfig { auth: AuthLevel; rateLimit?: RateLimitConfig; cache?: CacheConfig; - getCost: (body: ProxyRequestBody, searchParams: URLSearchParams) => Promise; + getCost: ( + body: ProxyRequestBody, + searchParams: URLSearchParams, + ) => Promise; } export interface HandlerContext { diff --git a/packages/lib/services/redeemable-earnings.ts b/packages/lib/services/redeemable-earnings.ts index ff3e6a49a..84181279e 100644 --- a/packages/lib/services/redeemable-earnings.ts +++ b/packages/lib/services/redeemable-earnings.ts @@ -88,7 +88,9 @@ interface RefundEarningsParams { reason: string; } -const normalizeLedgerMetadata = (metadata?: Record): Record => { +const normalizeLedgerMetadata = ( + metadata?: Record, +): Record => { if (!metadata) return {}; const mapped: Record = {}; for (const [key, value] of Object.entries(metadata)) { @@ -258,10 +260,12 @@ class RedeemableEarningsService { user_id: userId, total_earned: amountDecimal, available_balance: amountDecimal, - earned_from_miniapps: source === "miniapp" ? amountDecimal : "0.0000", + earned_from_miniapps: + source === "miniapp" ? amountDecimal : "0.0000", earned_from_agents: source === "agent" ? amountDecimal : "0.0000", earned_from_mcps: source === "mcp" ? amountDecimal : "0.0000", - earned_from_affiliates: source === "affiliate" ? amountDecimal : "0.0000", + earned_from_affiliates: + source === "affiliate" ? amountDecimal : "0.0000", earned_from_app_owner_shares: source === "app_owner_revenue_share" ? amountDecimal : "0.0000", earned_from_creator_shares: @@ -372,7 +376,14 @@ class RedeemableEarningsService { ledgerEntryId: string; error?: string; }> { - const { userId, amount, source, sourceId, description, metadata = {} } = params; + const { + userId, + amount, + source, + sourceId, + description, + metadata = {}, + } = params; if (amount <= 0) { return { @@ -462,11 +473,14 @@ class RedeemableEarningsService { }); if (result.skipped) { - logger.info("[RedeemableEarnings] No earnings to reduce (user has no record)", { - userId: `${userId.slice(0, 8)}...`, - amount, - source, - }); + logger.info( + "[RedeemableEarnings] No earnings to reduce (user has no record)", + { + userId: `${userId.slice(0, 8)}...`, + amount, + source, + }, + ); return { success: true, @@ -505,7 +519,9 @@ class RedeemableEarningsService { * CRITICAL: This moves earnings from available to pending. * The earnings are still owned by the user but cannot be redeemed again. */ - async lockForRedemption(params: LockEarningsParams): Promise { + async lockForRedemption( + params: LockEarningsParams, + ): Promise { const { userId, amount, redemptionId, metadata } = params; if (amount <= 0) { diff --git a/packages/lib/services/referrals.ts b/packages/lib/services/referrals.ts index f5e957201..69bf6d735 100644 --- a/packages/lib/services/referrals.ts +++ b/packages/lib/services/referrals.ts @@ -15,7 +15,8 @@ import { creditsService } from "./credits"; function isUniqueViolation(error: unknown): boolean { const code = error instanceof Error ? Reflect.get(error, "code") : undefined; return ( - code === "23505" || (error instanceof Error && error.message.includes("unique constraint")) + code === "23505" || + (error instanceof Error && error.message.includes("unique constraint")) ); } @@ -82,7 +83,8 @@ if (Math.abs(SPLITS_TOTAL - 1) > 1e-9) { `Referral revenue splits must sum to 1.0 (got ${SPLITS_TOTAL}). Fix REFERRAL_REVENUE_SPLITS.`, ); } -const MULTI_TIER_TOTAL = REFERRAL_REVENUE_SPLITS.CREATOR_TIER + REFERRAL_REVENUE_SPLITS.EDITOR_TIER; +const MULTI_TIER_TOTAL = + REFERRAL_REVENUE_SPLITS.CREATOR_TIER + REFERRAL_REVENUE_SPLITS.EDITOR_TIER; if (Math.abs(MULTI_TIER_TOTAL - REFERRAL_REVENUE_SPLITS.CREATOR) > 1e-9) { throw new Error( `Multi-tier creator split must equal CREATOR (${REFERRAL_REVENUE_SPLITS.CREATOR}); got ${MULTI_TIER_TOTAL}.`, @@ -144,7 +146,8 @@ export class ReferralsService { }); } catch (error) { if (isUniqueViolation(error)) { - const concurrentCode = await referralCodesRepository.findByUserId(userId); + const concurrentCode = + await referralCodesRepository.findByUserId(userId); if (concurrentCode) { return concurrentCode; } @@ -174,16 +177,19 @@ export class ReferralsService { ): Promise<{ success: boolean; message: string; bonusAmount?: number }> { const normalizedCode = code.trim().toUpperCase(); - const existingSignup = await referralSignupsRepository.findByReferredUserId(referredUserId); + const existingSignup = + await referralSignupsRepository.findByReferredUserId(referredUserId); if (existingSignup) { - const existingCode = await referralCodesRepository.findByCode(normalizedCode); + const existingCode = + await referralCodesRepository.findByCode(normalizedCode); if (existingCode && existingSignup.referral_code_id === existingCode.id) { return { success: true, message: "Referral code already applied" }; } return { success: false, message: "Already used a referral code" }; } - const referralCode = await referralCodesRepository.findByCode(normalizedCode); + const referralCode = + await referralCodesRepository.findByCode(normalizedCode); if (!referralCode) { return { success: false, message: "Invalid referral code" }; } @@ -198,7 +204,10 @@ export class ReferralsService { // Prevent self-referral abuse via app owner revenue share if (appContext?.appOwnerId === referredUserId) { - return { success: false, message: "Cannot claim app owner revenue from your own purchase" }; + return { + success: false, + message: "Cannot claim app owner revenue from your own purchase", + }; } // Get referrer's organization to credit them @@ -264,9 +273,15 @@ export class ReferralsService { // PERFORMANCE: Mark signup bonus as credited and update stats in parallel await Promise.all([ - referralSignupsRepository.markBonusCredited(signup.id, REWARDS.SIGNUP_BONUS), + referralSignupsRepository.markBonusCredited( + signup.id, + REWARDS.SIGNUP_BONUS, + ), referralCodesRepository.incrementReferrals(referralCode.id), - referralCodesRepository.addSignupEarnings(referralCode.id, REWARDS.SIGNUP_BONUS), + referralCodesRepository.addSignupEarnings( + referralCode.id, + REWARDS.SIGNUP_BONUS, + ), ]); logger.info("[Referrals] Referral code applied", { @@ -309,7 +324,8 @@ export class ReferralsService { return { elizaCloudAmount: purchaseAmount, splits: [] }; } - const { ELIZA_CLOUD, APP_OWNER, CREATOR, CREATOR_TIER, EDITOR_TIER } = REFERRAL_REVENUE_SPLITS; + const { ELIZA_CLOUD, APP_OWNER, CREATOR, CREATOR_TIER, EDITOR_TIER } = + REFERRAL_REVENUE_SPLITS; let elizaCloudAmount = purchaseAmount * ELIZA_CLOUD; const appOwnerAmount = purchaseAmount * APP_OWNER; @@ -333,9 +349,13 @@ export class ReferralsService { const creatorId = signup.creator_id || signup.referrer_user_id; - const referralCode = await referralCodesRepository.findById(signup.referral_code_id); + const referralCode = await referralCodesRepository.findById( + signup.referral_code_id, + ); if (referralCode && referralCode.parent_referral_id) { - const parentCode = await referralCodesRepository.findById(referralCode.parent_referral_id); + const parentCode = await referralCodesRepository.findById( + referralCode.parent_referral_id, + ); if (parentCode) { splits.push({ userId: creatorId, @@ -425,7 +445,10 @@ export class ReferralsService { referredUserId: string, ): Promise<{ qualified: boolean; bonusAwarded?: number }> { // Find unqualified referral for this user - const signup = await referralSignupsRepository.findUnqualifiedByReferredUserId(referredUserId); + const signup = + await referralSignupsRepository.findUnqualifiedByReferredUserId( + referredUserId, + ); if (!signup) { return { qualified: false }; @@ -434,9 +457,12 @@ export class ReferralsService { // Get referrer's organization to credit them const referrer = await usersRepository.findById(signup.referrer_user_id); if (!referrer?.organization_id) { - logger.warn("[Referrals] Referrer has no organization for qualified bonus", { - referrerId: signup.referrer_user_id, - }); + logger.warn( + "[Referrals] Referrer has no organization for qualified bonus", + { + referrerId: signup.referrer_user_id, + }, + ); return { qualified: false }; } @@ -452,7 +478,8 @@ export class ReferralsService { await creditsService.addCredits({ organizationId: referrer.organization_id, amount: REWARDS.QUALIFIED_BONUS, - description: "Referral qualified bonus - referred user linked social account", + description: + "Referral qualified bonus - referred user linked social account", metadata: { referred_user_id: referredUserId, type: "referral_qualified_bonus", @@ -503,15 +530,16 @@ export class SocialRewardsService { // Atomically check if claimed today and create record if not // This prevents race conditions where multiple concurrent requests could both pass the check - const shareRecord = await socialShareRewardsRepository.createIfNotClaimedToday( - userId, - platform, - { - share_type: shareType, - share_url: shareUrl, - credits_awarded: String(rewardAmount), - }, - ); + const shareRecord = + await socialShareRewardsRepository.createIfNotClaimedToday( + userId, + platform, + { + share_type: shareType, + share_url: shareUrl, + credits_awarded: String(rewardAmount), + }, + ); if (!shareRecord) { return { @@ -568,11 +596,18 @@ export class SocialRewardsService { async getShareStatus( userId: string, ): Promise> { - const platforms: SocialPlatform[] = ["x", "farcaster", "telegram", "discord"]; + const platforms: SocialPlatform[] = [ + "x", + "farcaster", + "telegram", + "discord", + ]; // PERFORMANCE: Check all platforms in parallel instead of sequential loop const claimedStatuses = await Promise.all( - platforms.map((platform) => socialShareRewardsRepository.hasClaimedToday(userId, platform)), + platforms.map((platform) => + socialShareRewardsRepository.hasClaimedToday(userId, platform), + ), ); return { @@ -590,7 +625,10 @@ export class SocialRewardsService { return socialShareRewardsRepository.getTotalEarnings(userId); } - async getRewardHistory(userId: string, limit = 50): Promise { + async getRewardHistory( + userId: string, + limit = 50, + ): Promise { return socialShareRewardsRepository.listByUserId(userId, limit); } diff --git a/packages/lib/services/resource-authorization.ts b/packages/lib/services/resource-authorization.ts index 0af25fe36..ad2b98ec6 100644 --- a/packages/lib/services/resource-authorization.ts +++ b/packages/lib/services/resource-authorization.ts @@ -26,7 +26,9 @@ export interface ResourceAccessParams { * @returns True if access is granted. * @throws Error if resource access is denied. */ -export async function verifyResourceAccess(params: ResourceAccessParams): Promise { +export async function verifyResourceAccess( + params: ResourceAccessParams, +): Promise { const { organizationId, eventType, resourceId } = params; switch (eventType) { @@ -57,7 +59,12 @@ export async function verifyResourceAccess(params: ResourceAccessParams): Promis const container = await dbRead .select() .from(containers) - .where(and(eq(containers.id, resourceId), eq(containers.organization_id, organizationId))) + .where( + and( + eq(containers.id, resourceId), + eq(containers.organization_id, organizationId), + ), + ) .limit(1); if (!container || container.length === 0) { @@ -78,7 +85,9 @@ export async function verifyResourceAccess(params: ResourceAccessParams): Promis * @param organizationId - Organization ID to verify. * @returns True if organization exists. */ -export async function verifyOrganizationAccess(organizationId: string): Promise { +export async function verifyOrganizationAccess( + organizationId: string, +): Promise { const org = await dbRead.query.organizations.findFirst({ where: eq(organizations.id, organizationId), columns: { id: true }, diff --git a/packages/lib/services/room-title.ts b/packages/lib/services/room-title.ts index d146a52f5..ca6ddfb1d 100644 --- a/packages/lib/services/room-title.ts +++ b/packages/lib/services/room-title.ts @@ -10,7 +10,9 @@ import { logger } from "@/lib/utils/logger"; * @param roomId - The room ID to generate title for * @returns The generated title, or null if title generation was skipped */ -export async function generateRoomTitle(roomId: string): Promise { +export async function generateRoomTitle( + roomId: string, +): Promise { const room = await roomsRepository.findById(roomId); if (!room) { @@ -103,7 +105,11 @@ function generateFallbackTitle(message: string): string { } // Question patterns - if (/^(what|how|why|when|where|who|can|could|would|should|is|are|do|does)/i.test(cleaned)) { + if ( + /^(what|how|why|when|where|who|can|could|would|should|is|are|do|does)/i.test( + cleaned, + ) + ) { const words = message.trim().split(/\s+/).slice(0, 6); if (words.length >= 3) { return capitalizeFirst(words.slice(0, 5).join(" ")); diff --git a/packages/lib/services/sandbox-provider.ts b/packages/lib/services/sandbox-provider.ts index 1b1dddf2c..75895fd3f 100644 --- a/packages/lib/services/sandbox-provider.ts +++ b/packages/lib/services/sandbox-provider.ts @@ -62,12 +62,16 @@ export async function createSandboxProvider(): Promise { switch (providerName) { case "vercel": { - const { VercelSandboxProvider } = await import("./vercel-sandbox-provider"); + const { VercelSandboxProvider } = await import( + "./vercel-sandbox-provider" + ); return new VercelSandboxProvider(); } case "docker": { - const { DockerSandboxProvider } = await import("./docker-sandbox-provider"); + const { DockerSandboxProvider } = await import( + "./docker-sandbox-provider" + ); return new DockerSandboxProvider(); } diff --git a/packages/lib/services/sandbox-snapshots.ts b/packages/lib/services/sandbox-snapshots.ts index 5ce6a2308..b7fa56968 100644 --- a/packages/lib/services/sandbox-snapshots.ts +++ b/packages/lib/services/sandbox-snapshots.ts @@ -16,7 +16,10 @@ import { and, desc, eq, gt, lt, sql } from "drizzle-orm"; import { dbRead, dbWrite } from "@/db/client"; -import { type SandboxTemplateSnapshot, sandboxTemplateSnapshots } from "@/db/schemas/app-sandboxes"; +import { + type SandboxTemplateSnapshot, + sandboxTemplateSnapshots, +} from "@/db/schemas/app-sandboxes"; import { logger } from "@/lib/utils/logger"; // Snapshot expiration: 7 days (Vercel's limit) @@ -113,10 +116,13 @@ export async function getValidSnapshot( } catch (error) { // This is expected if the table doesn't exist yet (migration not run) // or if there are no snapshots - just continue without snapshot - logger.debug("Snapshot lookup failed (this is normal if no snapshots exist)", { - templateKey, - error: error instanceof Error ? error.message : "Unknown", - }); + logger.debug( + "Snapshot lookup failed (this is normal if no snapshots exist)", + { + templateKey, + error: error instanceof Error ? error.message : "Unknown", + }, + ); return null; } } @@ -161,7 +167,13 @@ export async function createSnapshotFromSandbox( return null; } - const { templateKey, githubRepo, githubCommitSha, nodeModulesSizeMb, totalFiles } = options; + const { + templateKey, + githubRepo, + githubCommitSha, + nodeModulesSizeMb, + totalFiles, + } = options; logger.info("Creating snapshot from sandbox", { sandboxId: sandbox.sandboxId, @@ -176,7 +188,9 @@ export async function createSnapshotFromSandbox( const snapshotId = snapshotResult.snapshotId; // Calculate expiration date (7 days from now) - const expiresAt = new Date(Date.now() + SNAPSHOT_EXPIRY_DAYS * 24 * 60 * 60 * 1000); + const expiresAt = new Date( + Date.now() + SNAPSHOT_EXPIRY_DAYS * 24 * 60 * 60 * 1000, + ); // Store in database const [newSnapshot] = await dbWrite @@ -234,7 +248,9 @@ export async function verifySnapshot(snapshotId: string): Promise { } : { snapshotId }; - const snapshot = await Snapshot.get(getOptions as Parameters[0]); + const snapshot = await Snapshot.get( + getOptions as Parameters[0], + ); // Snapshot status could be 'created', 'deleted', or 'failed' - we only consider it valid if created return snapshot?.status === "created"; } catch (error) { @@ -287,7 +303,9 @@ export async function deleteSnapshot(snapshotId: string): Promise { } : { snapshotId }; - const snapshot = await Snapshot.get(getOptions as Parameters[0]); + const snapshot = await Snapshot.get( + getOptions as Parameters[0], + ); if (snapshot) { await snapshot.delete(); } @@ -325,12 +343,13 @@ export async function cleanupExpiredSnapshots(): Promise { const now = new Date(); // Find all expired snapshots - const expiredSnapshots = await dbRead.query.sandboxTemplateSnapshots.findMany({ - where: and( - eq(sandboxTemplateSnapshots.status, "ready"), - lt(sandboxTemplateSnapshots.expires_at, now), - ), - }); + const expiredSnapshots = + await dbRead.query.sandboxTemplateSnapshots.findMany({ + where: and( + eq(sandboxTemplateSnapshots.status, "ready"), + lt(sandboxTemplateSnapshots.expires_at, now), + ), + }); if (expiredSnapshots.length === 0) { return 0; @@ -387,7 +406,9 @@ export async function getSnapshotStats(): Promise<{ /** * List all snapshots for a template. */ -export async function listSnapshots(templateKey?: string): Promise { +export async function listSnapshots( + templateKey?: string, +): Promise { try { if (templateKey) { return await dbRead.query.sandboxTemplateSnapshots.findMany({ diff --git a/packages/lib/services/sandbox.ts b/packages/lib/services/sandbox.ts index aeb364615..15df944aa 100644 --- a/packages/lib/services/sandbox.ts +++ b/packages/lib/services/sandbox.ts @@ -33,7 +33,12 @@ import { } from "./sandbox-snapshots"; // Re-export types for consumers -export type { SandboxConfig, SandboxInstance, SandboxProgress, SandboxSessionData }; +export type { + SandboxConfig, + SandboxInstance, + SandboxProgress, + SandboxSessionData, +}; // SDK Templates (Fallback for templates without built-in SDK) // Loaded from external files instead of inline string literals. @@ -53,8 +58,10 @@ let _elizaSdkFile: string | undefined; let _elizaHookFile: string | undefined; let _elizaAnalyticsComponent: string | undefined; -const getElizaSdkFile = () => (_elizaSdkFile ??= loadTemplate("eliza-sdk.ts.template")); -const getElizaHookFile = () => (_elizaHookFile ??= loadTemplate("use-eliza.ts.template")); +const getElizaSdkFile = () => + (_elizaSdkFile ??= loadTemplate("eliza-sdk.ts.template")); +const getElizaHookFile = () => + (_elizaHookFile ??= loadTemplate("use-eliza.ts.template")); const getElizaAnalyticsComponent = () => (_elizaAnalyticsComponent ??= loadTemplate("eliza-analytics.tsx.template")); @@ -62,7 +69,8 @@ const getElizaAnalyticsComponent = () => // Constants // ============================================================================ -const DEFAULT_TEMPLATE_URL = "https://github.com/eliza-cloud-apps/cloud-apps-template.git"; +const DEFAULT_TEMPLATE_URL = + "https://github.com/eliza-cloud-apps/cloud-apps-template.git"; const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes // ============================================================================ @@ -172,16 +180,21 @@ export class SandboxService { let sandbox: SandboxInstance; try { - sandbox = (await Sandbox.create(createOptions)) as unknown as SandboxInstance; + sandbox = (await Sandbox.create( + createOptions, + )) as unknown as SandboxInstance; } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); const apiError = error as { json?: unknown; text?: string; response?: { status?: number }; }; - const jsonDetails = apiError.json ? JSON.stringify(apiError.json, null, 2) : undefined; + const jsonDetails = apiError.json + ? JSON.stringify(apiError.json, null, 2) + : undefined; const textDetails = apiError.text; const statusCode = apiError.response?.status; @@ -240,7 +253,8 @@ export class SandboxService { } const devServerUrl = sandbox.domain(3000); - const sandboxId = sandbox.sandboxId || extractSandboxIdFromUrl(devServerUrl); + const sandboxId = + sandbox.sandboxId || extractSandboxIdFromUrl(devServerUrl); logger.info("Sandbox created", { sandboxId, devServerUrl }); getActiveSandboxes().set(sandboxId, sandbox); @@ -250,14 +264,20 @@ export class SandboxService { if (useSnapshotId) { createdFromSnapshot = true; await recordSnapshotUsage(useSnapshotId); - logger.info("Sandbox created from snapshot", { sandboxId, snapshotId: useSnapshotId }); + logger.info("Sandbox created from snapshot", { + sandboxId, + snapshotId: useSnapshotId, + }); } // Skip bun install and dependencies if created from snapshot (already included) if (!createdFromSnapshot) { // Install bun runtime logger.info("Installing bun runtime", { sandboxId }); - onProgress?.({ step: "installing", message: "Installing bun runtime..." }); + onProgress?.({ + step: "installing", + message: "Installing bun runtime...", + }); const bunInstall = await sandbox.runCommand({ cmd: "npm", @@ -265,17 +285,23 @@ export class SandboxService { }); if (bunInstall.exitCode !== 0) { - logger.warn("Failed to install bun globally, will fall back to pnpm/npm", { - sandboxId, - stderr: await bunInstall.stderr(), - }); + logger.warn( + "Failed to install bun globally, will fall back to pnpm/npm", + { + sandboxId, + stderr: await bunInstall.stderr(), + }, + ); } else { logger.info("Bun installed successfully", { sandboxId }); } // Install dependencies logger.info("Installing dependencies", { sandboxId }); - onProgress?.({ step: "installing", message: "Installing dependencies..." }); + onProgress?.({ + step: "installing", + message: "Installing dependencies...", + }); const installResult = await installDependencies(sandbox); if (installResult.includes("Failed")) { @@ -284,7 +310,10 @@ export class SandboxService { onProgress?.({ step: "installing", message: "Dependencies installed" }); } else { - logger.info("Skipping bun install and dependencies (created from snapshot)", { sandboxId }); + logger.info( + "Skipping bun install and dependencies (created from snapshot)", + { sandboxId }, + ); onProgress?.({ step: "installing", message: "Using pre-installed dependencies from snapshot", @@ -392,7 +421,8 @@ export class SandboxService { config.env?.NEXT_PUBLIC_ELIZA_API_URL; const elizaProxyUrl = - config.env?.NEXT_PUBLIC_ELIZA_PROXY_URL || process.env.NEXT_PUBLIC_ELIZA_PROXY_URL; + config.env?.NEXT_PUBLIC_ELIZA_PROXY_URL || + process.env.NEXT_PUBLIC_ELIZA_PROXY_URL; if (elizaProxyUrl) { mergedEnv.NEXT_PUBLIC_ELIZA_PROXY_URL = elizaProxyUrl; @@ -407,7 +437,8 @@ export class SandboxService { apiUrl: elizaApiUrl, }); } else if (isLocalDev && !config.env?.NEXT_PUBLIC_ELIZA_API_URL) { - const localServerUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + const localServerUrl = + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; mergedEnv.NEXT_PUBLIC_ELIZA_PROXY_URL = localServerUrl; logger.info("Local dev: defaulting to postMessage proxy bridge", { sandboxId, @@ -496,7 +527,11 @@ export class SandboxService { return content; } - async writeFile(sandboxId: string, path: string, content: string): Promise { + async writeFile( + sandboxId: string, + path: string, + content: string, + ): Promise { const sandbox = getActiveSandboxes().get(sandboxId); if (!sandbox) throw new Error(`Sandbox ${sandboxId} not found`); await writeFileViaSh(sandbox, path, content); @@ -524,7 +559,10 @@ export class SandboxService { // Package Operations // ============================================================================ - async installPackages(sandboxId: string, packages: string[]): Promise { + async installPackages( + sandboxId: string, + packages: string[], + ): Promise { const sandbox = getActiveSandboxes().get(sandboxId); if (!sandbox) throw new Error(`Sandbox ${sandboxId} not found`); const { installPackages } = await import("./sandbox/index"); @@ -810,17 +848,29 @@ export class SandboxService { const healthCheck = await sandbox.runCommand({ cmd: "curl", - args: ["-s", "-o", "/dev/null", "-w", "%{http_code}", "-m", "5", "http://localhost:3000"], + args: [ + "-s", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "-m", + "5", + "http://localhost:3000", + ], }); const statusCode = await healthCheck.stdout(); const isHealthy = statusCode === "200" || statusCode === "304"; if (!isHealthy) { - logger.info("Sandbox reconnected but dev server not responding, attempting restart", { - sandboxId, - statusCode, - }); + logger.info( + "Sandbox reconnected but dev server not responding, attempting restart", + { + sandboxId, + statusCode, + }, + ); onProgress?.({ step: "starting", message: "Restarting dev server..." }); await sandbox.runCommand({ @@ -866,7 +916,8 @@ export class SandboxService { startedAt: new Date(), }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.info("Sandbox reconnection failed", { sandboxId, error: errorMessage, diff --git a/packages/lib/services/sandbox/build-tools.ts b/packages/lib/services/sandbox/build-tools.ts index 43763df91..0cddc5df7 100644 --- a/packages/lib/services/sandbox/build-tools.ts +++ b/packages/lib/services/sandbox/build-tools.ts @@ -31,7 +31,10 @@ export async function checkBuild(sandbox: SandboxInstance): Promise { }); // Get both stdout and stderr - const [stdout, stderr] = await Promise.all([command.stdout(), command.stderr()]); + const [stdout, stderr] = await Promise.all([ + command.stdout(), + command.stderr(), + ]); const output = `${stdout}\n${stderr}`.trim(); const exitCode = command.exitCode ?? -1; @@ -53,10 +56,14 @@ export async function checkBuild(sandbox: SandboxInstance): Promise { line.includes("Type ") || line.includes("TS"), ) - .filter((line) => !line.includes("warning") && !line.includes("DeprecationWarning")) + .filter( + (line) => + !line.includes("warning") && !line.includes("DeprecationWarning"), + ) .slice(0, 15); - const errorSummary = errorLines.length > 0 ? errorLines.join("\n") : output.slice(0, 1500); + const errorSummary = + errorLines.length > 0 ? errorLines.join("\n") : output.slice(0, 1500); return `BUILD ERRORS:\n${errorSummary}\n\nPlease fix these errors!`; } catch (error) { @@ -166,7 +173,9 @@ export async function runProductionBuild( const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined; const combinedSignal = - signal && timeoutSignal ? AbortSignal.any([signal, timeoutSignal]) : (signal ?? timeoutSignal); + signal && timeoutSignal + ? AbortSignal.any([signal, timeoutSignal]) + : (signal ?? timeoutSignal); const command = await sandbox.runCommand({ cmd: "bun", diff --git a/packages/lib/services/sandbox/file-ops.ts b/packages/lib/services/sandbox/file-ops.ts index d95dc68ed..b93482879 100644 --- a/packages/lib/services/sandbox/file-ops.ts +++ b/packages/lib/services/sandbox/file-ops.ts @@ -15,7 +15,9 @@ import type { SandboxInstance } from "./types"; * Convert a ReadableStream to string. * Used to process file content from sandbox.readFile() */ -async function streamToString(stream: ReadableStream): Promise { +async function streamToString( + stream: ReadableStream, +): Promise { const reader = stream.getReader(); const decoder = new TextDecoder("utf-8"); const chunks: string[] = []; @@ -84,7 +86,10 @@ export async function writeFileViaSh( try { // Use native SDK methods (preferred) - if (typeof sandbox.mkDir === "function" && typeof sandbox.writeFiles === "function") { + if ( + typeof sandbox.mkDir === "function" && + typeof sandbox.writeFiles === "function" + ) { // Create directory if needed if (dir) { await sandbox.mkDir(dir); @@ -148,7 +153,9 @@ export async function writeFilesViaSh( } } - const validFiles = files.filter((f) => !failed.some((ff) => ff.path === f.path)); + const validFiles = files.filter( + (f) => !failed.some((ff) => ff.path === f.path), + ); if (validFiles.length === 0) { return { written, failed }; @@ -187,10 +194,13 @@ export async function writeFilesViaSh( return { written, failed }; } } catch (error) { - logger.debug("Native batch writeFiles failed, falling back to individual writes", { - fileCount: validFiles.length, - error: error instanceof Error ? error.message : "Unknown", - }); + logger.debug( + "Native batch writeFiles failed, falling back to individual writes", + { + fileCount: validFiles.length, + error: error instanceof Error ? error.message : "Unknown", + }, + ); } // Fallback: write files individually @@ -213,7 +223,10 @@ export async function writeFilesViaSh( * Create a directory in the sandbox using native SDK method. * Falls back to shell command if native method is unavailable. */ -export async function mkDirViaSh(sandbox: SandboxInstance, dirPath: string): Promise { +export async function mkDirViaSh( + sandbox: SandboxInstance, + dirPath: string, +): Promise { try { // Use native SDK method (preferred) if (typeof sandbox.mkDir === "function") { @@ -235,7 +248,10 @@ export async function mkDirViaSh(sandbox: SandboxInstance, dirPath: string): Pro * List files in a directory, excluding common non-source directories. * Uses shell command as SDK doesn't have a native list method. */ -export async function listFilesViaSh(sandbox: SandboxInstance, dirPath: string): Promise { +export async function listFilesViaSh( + sandbox: SandboxInstance, + dirPath: string, +): Promise { const excludes = [ ".git", ".next", @@ -253,5 +269,7 @@ export async function listFilesViaSh(sandbox: SandboxInstance, dirPath: string): cmd: "sh", args: ["-c", findCmd], }); - return result.exitCode === 0 ? (await result.stdout()).split("\n").filter(Boolean) : []; + return result.exitCode === 0 + ? (await result.stdout()).split("\n").filter(Boolean) + : []; } diff --git a/packages/lib/services/sandbox/package-manager.ts b/packages/lib/services/sandbox/package-manager.ts index 44e43c091..cd5b99383 100644 --- a/packages/lib/services/sandbox/package-manager.ts +++ b/packages/lib/services/sandbox/package-manager.ts @@ -65,7 +65,9 @@ export async function installDependencies( }); if (packageJsonCheck.exitCode !== 0) { - logger.error("package.json not found - template may have failed to clone properly"); + logger.error( + "package.json not found - template may have failed to clone properly", + ); return "Failed to install dependencies: package.json not found. The sandbox template may have failed to initialize properly. Please try again."; } diff --git a/packages/lib/services/sandbox/sandbox-manager.ts b/packages/lib/services/sandbox/sandbox-manager.ts index 276e64d29..d3717bdb8 100644 --- a/packages/lib/services/sandbox/sandbox-manager.ts +++ b/packages/lib/services/sandbox/sandbox-manager.ts @@ -14,7 +14,13 @@ import { logger } from "@/lib/utils/logger"; */ export interface SandboxSummary { sandboxId: string; - status: "pending" | "running" | "stopping" | "stopped" | "failed" | "snapshotting"; + status: + | "pending" + | "running" + | "stopping" + | "stopped" + | "failed" + | "snapshotting"; createdAt: Date; timeout: number; } @@ -111,7 +117,9 @@ async function getSandboxSDK(): Promise { * List all sandboxes for a project. * Useful for admin dashboards and cleanup operations. */ -export async function listSandboxes(options: ListSandboxesOptions): Promise { +export async function listSandboxes( + options: ListSandboxesOptions, +): Promise { const { projectId, limit = 50, since, until, signal } = options; try { diff --git a/packages/lib/services/sandbox/tool-executor.ts b/packages/lib/services/sandbox/tool-executor.ts index 38795aa17..2948ccb43 100644 --- a/packages/lib/services/sandbox/tool-executor.ts +++ b/packages/lib/services/sandbox/tool-executor.ts @@ -62,7 +62,10 @@ async function withTimeoutAndAbort( ): Promise { const timeoutPromise = new Promise((_, reject) => { setTimeout( - () => reject(new Error(`Tool '${toolName}' timed out after ${timeoutMs / 1000}s`)), + () => + reject( + new Error(`Tool '${toolName}' timed out after ${timeoutMs / 1000}s`), + ), timeoutMs, ); }); @@ -121,7 +124,9 @@ export async function executeToolCall( // Determine timeout upfront - database commands get extended timeout const command = toolName === "run_command" ? (args?.command as string) : ""; const isDatabaseCommand = command && needsDatabaseCredentials(command); - const effectiveTimeout = isDatabaseCommand ? DATABASE_COMMAND_TIMEOUT_MS : TOOL_TIMEOUT_MS; + const effectiveTimeout = isDatabaseCommand + ? DATABASE_COMMAND_TIMEOUT_MS + : TOOL_TIMEOUT_MS; try { const execution = async (): Promise => { @@ -199,7 +204,10 @@ export async function executeToolCall( if (isDatabaseCommand && appId) { const app = await appsRepository.findById(appId); - if (app?.user_database_status === "ready" && app.user_database_uri) { + if ( + app?.user_database_status === "ready" && + app.user_database_uri + ) { // Decrypt the connection URI (handles both encrypted and legacy plaintext) const decryptedUri = await getDecryptedDatabaseUri(app); if (decryptedUri) { @@ -230,7 +238,8 @@ export async function executeToolCall( }); // Sanitize output to prevent credential leakage - const rawOutput = `Exit ${r.exitCode}: ${await r.stdout()} ${await r.stderr()}`.trim(); + const rawOutput = + `Exit ${r.exitCode}: ${await r.stdout()} ${await r.stderr()}`.trim(); return sanitizeOutput(rawOutput); } @@ -240,9 +249,15 @@ export async function executeToolCall( }; // Execute with timeout and abort signal support (database commands get extended timeout) - result = await withTimeoutAndAbort(execution(), effectiveTimeout, toolName, abortSignal); + result = await withTimeoutAndAbort( + execution(), + effectiveTimeout, + toolName, + abortSignal, + ); } catch (toolError) { - const toolErrorMsg = toolError instanceof Error ? toolError.message : String(toolError); + const toolErrorMsg = + toolError instanceof Error ? toolError.message : String(toolError); // Log abort errors at info level, others at error level if (toolErrorMsg.includes("aborted")) { diff --git a/packages/lib/services/sandbox/tool-schemas.ts b/packages/lib/services/sandbox/tool-schemas.ts index 1f76266ba..26d4d6eaa 100644 --- a/packages/lib/services/sandbox/tool-schemas.ts +++ b/packages/lib/services/sandbox/tool-schemas.ts @@ -24,7 +24,11 @@ export const toolSchemas = { path: z.string().default(".").describe("Directory path"), }), run_command: z.object({ - command: z.string().describe("Command to run (drizzle-kit commands auto-inject DATABASE_URL)"), + command: z + .string() + .describe( + "Command to run (drizzle-kit commands auto-inject DATABASE_URL)", + ), }), }; diff --git a/packages/lib/services/sandbox/types.ts b/packages/lib/services/sandbox/types.ts index 1cf028abb..ede7ac4f0 100644 --- a/packages/lib/services/sandbox/types.ts +++ b/packages/lib/services/sandbox/types.ts @@ -32,7 +32,10 @@ export interface CommandResult { signal?: AbortSignal; }) => AsyncGenerator<{ stream: "stdout" | "stderr"; data: string }>; wait: (opts?: { signal?: AbortSignal }) => Promise; - kill: (signal?: string, opts?: { abortSignal?: AbortSignal }) => Promise; + kill: ( + signal?: string, + opts?: { abortSignal?: AbortSignal }, + ) => Promise; } export interface CommandFinished extends CommandResult { @@ -57,22 +60,33 @@ export interface SandboxInstance { args?: string[], opts?: { signal?: AbortSignal }, ) => Promise; - getCommand: (cmdId: string, opts?: { signal?: AbortSignal }) => Promise; + getCommand: ( + cmdId: string, + opts?: { signal?: AbortSignal }, + ) => Promise; // File operations (native SDK methods) readFile: ( file: { path: string; cwd?: string }, opts?: { signal?: AbortSignal }, ) => Promise; - writeFiles: (files: SandboxFile[], opts?: { signal?: AbortSignal }) => Promise; + writeFiles: ( + files: SandboxFile[], + opts?: { signal?: AbortSignal }, + ) => Promise; mkDir: (path: string, opts?: { signal?: AbortSignal }) => Promise; // Lifecycle stop: (opts?: { signal?: AbortSignal }) => Promise; - extendTimeout: (durationMs: number, opts?: { signal?: AbortSignal }) => Promise; + extendTimeout: ( + durationMs: number, + opts?: { signal?: AbortSignal }, + ) => Promise; // Snapshotting (optional - may not be available on all sandbox instances) - snapshot?: (opts?: { signal?: AbortSignal }) => Promise<{ snapshotId: string }>; + snapshot?: (opts?: { + signal?: AbortSignal; + }) => Promise<{ snapshotId: string }>; } export type SandboxProgress = diff --git a/packages/lib/services/secrets/encryption.ts b/packages/lib/services/secrets/encryption.ts index c573ab743..913fffd0e 100644 --- a/packages/lib/services/secrets/encryption.ts +++ b/packages/lib/services/secrets/encryption.ts @@ -43,10 +43,13 @@ export class LocalKMSProvider implements KMSProvider { constructor(masterKeyHex?: string) { const keySource = masterKeyHex || process.env.SECRETS_MASTER_KEY; if (!keySource && process.env.NODE_ENV === "production") { - throw new Error("SECRETS_MASTER_KEY environment variable is required in production"); + throw new Error( + "SECRETS_MASTER_KEY environment variable is required in production", + ); } const key = keySource || "0".repeat(64); - if (key.length !== 64) throw new Error("Master key must be 64 hex characters (32 bytes)"); + if (key.length !== 64) + throw new Error("Master key must be 64 hex characters (32 bytes)"); this.masterKey = Buffer.from(key, "hex"); } @@ -55,22 +58,35 @@ export class LocalKMSProvider implements KMSProvider { const nonce = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", this.masterKey, nonce); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); - const ciphertext = Buffer.concat([nonce, cipher.getAuthTag(), encrypted]).toString("base64"); + const ciphertext = Buffer.concat([ + nonce, + cipher.getAuthTag(), + encrypted, + ]).toString("base64"); return { plaintext, ciphertext, keyId: this.keyId }; } async decrypt(ciphertext: string): Promise { const data = Buffer.from(ciphertext, "base64"); - const decipher = createDecipheriv("aes-256-gcm", this.masterKey, data.subarray(0, 12)); + const decipher = createDecipheriv( + "aes-256-gcm", + this.masterKey, + data.subarray(0, 12), + ); decipher.setAuthTag(data.subarray(12, 28)); - return Buffer.concat([decipher.update(data.subarray(28)), decipher.final()]); + return Buffer.concat([ + decipher.update(data.subarray(28)), + decipher.final(), + ]); } isConfigured = () => true; } type KMSClientType = { - send(command: unknown): Promise<{ Plaintext?: Uint8Array; CiphertextBlob?: Uint8Array }>; + send( + command: unknown, + ): Promise<{ Plaintext?: Uint8Array; CiphertextBlob?: Uint8Array }>; }; export class AWSKMSProvider implements KMSProvider { @@ -117,7 +133,8 @@ export class AWSKMSProvider implements KMSProvider { KeyId: this.keyId, }), ); - if (!response.Plaintext) throw new Error("KMS Decrypt returned empty response"); + if (!response.Plaintext) + throw new Error("KMS Decrypt returned empty response"); return Buffer.from(response.Plaintext); } @@ -128,16 +145,27 @@ export class SecretsEncryptionService { private kms: KMSProvider; constructor(kms?: KMSProvider) { - this.kms = kms || (process.env.AWS_KMS_KEY_ID ? new AWSKMSProvider() : new LocalKMSProvider()); + this.kms = + kms || + (process.env.AWS_KMS_KEY_ID + ? new AWSKMSProvider() + : new LocalKMSProvider()); } isConfigured = () => this.kms.isConfigured(); async encrypt(plaintext: string): Promise { - const { plaintext: dek, ciphertext: encryptedDek, keyId } = await this.kms.generateDataKey(); + const { + plaintext: dek, + ciphertext: encryptedDek, + keyId, + } = await this.kms.generateDataKey(); const nonce = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", dek, nonce); - const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + const encrypted = Buffer.concat([ + cipher.update(plaintext, "utf8"), + cipher.final(), + ]); dek.fill(0); return { encryptedValue: encrypted.toString("base64"), @@ -166,7 +194,11 @@ export class SecretsEncryptionService { } try { - const decipher = createDecipheriv("aes-256-gcm", dek, Buffer.from(nonce, "base64")); + const decipher = createDecipheriv( + "aes-256-gcm", + dek, + Buffer.from(nonce, "base64"), + ); decipher.setAuthTag(Buffer.from(authTag, "base64")); const result = Buffer.concat([ decipher.update(Buffer.from(encryptedValue, "base64")), @@ -191,5 +223,7 @@ export class SecretsEncryptionService { let instance: SecretsEncryptionService | null = null; -export const getEncryptionService = () => instance || (instance = new SecretsEncryptionService()); -export const createEncryptionService = (kms?: KMSProvider) => new SecretsEncryptionService(kms); +export const getEncryptionService = () => + instance || (instance = new SecretsEncryptionService()); +export const createEncryptionService = (kms?: KMSProvider) => + new SecretsEncryptionService(kms); diff --git a/packages/lib/services/secrets/helpers.ts b/packages/lib/services/secrets/helpers.ts index 64c4b1b6f..c172f3d2c 100644 --- a/packages/lib/services/secrets/helpers.ts +++ b/packages/lib/services/secrets/helpers.ts @@ -1,4 +1,7 @@ -import type { SecretEnvironment, SecretProjectType } from "@/db/schemas/secrets"; +import type { + SecretEnvironment, + SecretProjectType, +} from "@/db/schemas/secrets"; import { secretsService } from "./secrets"; export interface SecretContext { @@ -33,7 +36,9 @@ export interface SandboxSecretContext { appId?: string; } -export async function loadSecrets(ctx: SecretContext): Promise> { +export async function loadSecrets( + ctx: SecretContext, +): Promise> { assertSecretsConfigured(); const orgSecrets = await secretsService.getDecrypted({ @@ -107,7 +112,9 @@ export function assertSecretsConfigured(): void { export class SecretsNotConfiguredError extends Error { constructor() { - super("Secrets service is not configured. Set SECRETS_MASTER_KEY or configure AWS KMS."); + super( + "Secrets service is not configured. Set SECRETS_MASTER_KEY or configure AWS KMS.", + ); this.name = "SecretsNotConfiguredError"; } } diff --git a/packages/lib/services/secrets/secrets.ts b/packages/lib/services/secrets/secrets.ts index 86915266d..c5d69817b 100644 --- a/packages/lib/services/secrets/secrets.ts +++ b/packages/lib/services/secrets/secrets.ts @@ -16,7 +16,10 @@ import type { SecretProvider, SecretScope, } from "@/db/schemas/secrets"; -import { getEncryptionService, type SecretsEncryptionService } from "./encryption"; +import { + getEncryptionService, + type SecretsEncryptionService, +} from "./encryption"; const MAX_SECRET_VALUE_BYTES = 65536; // 64KB max for secret values @@ -142,7 +145,10 @@ class SecretsService { return this.encryption.isConfigured(); } - async create(params: CreateSecretParams, audit: AuditContext): Promise { + async create( + params: CreateSecretParams, + audit: AuditContext, + ): Promise { const { organizationId, name, @@ -171,8 +177,13 @@ class SecretsService { ); } - const { encrypted_value, encrypted_dek, nonce, auth_tag, encryption_key_id } = - await this.encryptValue(value); + const { + encrypted_value, + encrypted_dek, + nonce, + auth_tag, + encryption_key_id, + } = await this.encryptValue(value); const secret = await secretsRepository.create({ organization_id: organizationId, @@ -204,13 +215,19 @@ class SecretsService { environment?: SecretEnvironment, audit?: AuditContext, ): Promise { - const secret = await secretsRepository.findByName(organizationId, name, projectId, environment); + const secret = await secretsRepository.findByName( + organizationId, + name, + projectId, + environment, + ); if (!secret) return null; const value = await this.decryptSecret(secret); await secretsRepository.recordAccess(secret.id); - if (audit) await this.logAudit(secret.id, organizationId, "read", name, audit); + if (audit) + await this.logAudit(secret.id, organizationId, "read", name, audit); return value; } @@ -222,7 +239,8 @@ class SecretsService { const secret = await this.getExistingSecret(secretId, organizationId); const value = await this.decryptSecret(secret); await secretsRepository.recordAccess(secretId); - if (audit) await this.logAudit(secretId, organizationId, "read", secret.name, audit); + if (audit) + await this.logAudit(secretId, organizationId, "read", secret.name, audit); return value; } @@ -245,7 +263,14 @@ class SecretsService { for (const secret of secrets) { result[secret.name] = await this.decryptSecret(secret); await secretsRepository.recordAccess(secret.id); - if (audit) await this.logAudit(secret.id, params.organizationId, "read", secret.name, audit); + if (audit) + await this.logAudit( + secret.id, + params.organizationId, + "read", + secret.name, + audit, + ); } return result; } @@ -274,13 +299,24 @@ class SecretsService { version: existing.version + 1, }); } - if (params.description !== undefined) updateData.description = params.description; - if (params.expiresAt !== undefined) updateData.expires_at = params.expiresAt; - - const updated = await secretsRepository.update(secretId, updateData as Partial); + if (params.description !== undefined) + updateData.description = params.description; + if (params.expiresAt !== undefined) + updateData.expires_at = params.expiresAt; + + const updated = await secretsRepository.update( + secretId, + updateData as Partial, + ); if (!updated) throw new Error("Failed to update secret"); - await this.logAudit(secretId, organizationId, "updated", existing.name, audit); + await this.logAudit( + secretId, + organizationId, + "updated", + existing.name, + audit, + ); return this.toMetadata(updated); } @@ -300,11 +336,20 @@ class SecretsService { }); if (!updated) throw new Error("Failed to rotate secret"); - await this.logAudit(secretId, organizationId, "rotated", existing.name, audit); + await this.logAudit( + secretId, + organizationId, + "rotated", + existing.name, + audit, + ); return this.toMetadata(updated); } - private async getExistingSecret(secretId: string, organizationId: string): Promise { + private async getExistingSecret( + secretId: string, + organizationId: string, + ): Promise { const existing = await secretsRepository.findById(secretId); if (!existing || existing.organization_id !== organizationId) { throw new Error("Secret not found"); @@ -314,7 +359,9 @@ class SecretsService { private async encryptValue(value: string) { if (Buffer.byteLength(value, "utf8") > MAX_SECRET_VALUE_BYTES) { - throw new Error(`Secret value exceeds maximum size of ${MAX_SECRET_VALUE_BYTES} bytes`); + throw new Error( + `Secret value exceeds maximum size of ${MAX_SECRET_VALUE_BYTES} bytes`, + ); } const { encryptedValue, encryptedDek, nonce, authTag, keyId } = await this.encryption.encrypt(value); @@ -327,11 +374,21 @@ class SecretsService { }; } - async delete(secretId: string, organizationId: string, audit: AuditContext): Promise { + async delete( + secretId: string, + organizationId: string, + audit: AuditContext, + ): Promise { const existing = await this.getExistingSecret(secretId, organizationId); const deleted = await secretsRepository.delete(secretId); if (!deleted) throw new Error("Failed to delete secret"); - await this.logAudit(secretId, organizationId, "deleted", existing.name, audit); + await this.logAudit( + secretId, + organizationId, + "deleted", + existing.name, + audit, + ); } async deleteByName( @@ -402,7 +459,9 @@ class SecretsService { let providerDataNonce: string | undefined; let providerDataAuthTag: string | undefined; if (providerData) { - const dataResult = await this.encryption.encrypt(JSON.stringify(providerData)); + const dataResult = await this.encryption.encrypt( + JSON.stringify(providerData), + ); encryptedProviderData = dataResult.encryptedValue; providerDataNonce = dataResult.nonce; providerDataAuthTag = dataResult.authTag; @@ -537,7 +596,8 @@ class SecretsService { createdAt: Date; }> > { - const sessions = await oauthSessionsRepository.listByOrganization(organizationId); + const sessions = + await oauthSessionsRepository.listByOrganization(organizationId); return sessions.map((s) => ({ id: s.id, provider: s.provider, @@ -637,7 +697,10 @@ class SecretsService { }; } - async bindSecret(params: BindSecretParams, audit: AuditContext): Promise { + async bindSecret( + params: BindSecretParams, + audit: AuditContext, + ): Promise { const result = await this.bindSecrets( [params.secretId], params.projectId, @@ -705,7 +768,10 @@ class SecretsService { organizationId: string, audit: AuditContext, ): Promise { - const binding = await secretBindingsRepository.findByIdAndOrg(bindingId, organizationId); + const binding = await secretBindingsRepository.findByIdAndOrg( + bindingId, + organizationId, + ); if (!binding) { throw new Error("Binding not found"); } @@ -762,12 +828,15 @@ class SecretsService { })); } - async getAppSecretRequirements(appId: string): Promise { + async getAppSecretRequirements( + appId: string, + ): Promise { return appSecretRequirementsRepository.findByApp(appId); } async getApprovedAppSecrets(appId: string): Promise { - const approved = await appSecretRequirementsRepository.findApprovedByApp(appId); + const approved = + await appSecretRequirementsRepository.findApprovedByApp(appId); return approved.map((r) => r.secret_name); } @@ -775,21 +844,29 @@ class SecretsService { appId: string, requirements: Array<{ secretName: string; required: boolean }>, ): Promise { - return appSecretRequirementsRepository.syncRequirements(appId, requirements); + return appSecretRequirementsRepository.syncRequirements( + appId, + requirements, + ); } async approveAppSecretRequirement( requirementId: string, approvedBy: string, ): Promise { - const req = await appSecretRequirementsRepository.approve(requirementId, approvedBy); + const req = await appSecretRequirementsRepository.approve( + requirementId, + approvedBy, + ); if (!req) { throw new Error("Requirement not found"); } return req; } - async revokeAppSecretRequirement(requirementId: string): Promise { + async revokeAppSecretRequirement( + requirementId: string, + ): Promise { const req = await appSecretRequirementsRepository.revoke(requirementId); if (!req) { throw new Error("Requirement not found"); @@ -886,7 +963,10 @@ class SecretsService { return this.get(systemOrgId, name); } - async createSystemSecret(name: string, value: string): Promise { + async createSystemSecret( + name: string, + value: string, + ): Promise { const systemOrgId = process.env.SYSTEM_ORG_ID || "system"; const audit: AuditContext = { actorType: "system", @@ -914,15 +994,19 @@ class SecretsService { let instance: SecretsService | null = null; -export const getSecretsService = () => instance || (instance = new SecretsService()); - -export const secretsService = new Proxy({} as SecretsService & { isConfigured: boolean }, { - get(_, prop: keyof SecretsService | "isConfigured") { - const svc = getSecretsService(); - if (prop === "isConfigured") return svc.isConfigured(); - const val = svc[prop]; - return typeof val === "function" ? val.bind(svc) : val; +export const getSecretsService = () => + instance || (instance = new SecretsService()); + +export const secretsService = new Proxy( + {} as SecretsService & { isConfigured: boolean }, + { + get(_, prop: keyof SecretsService | "isConfigured") { + const svc = getSecretsService(); + if (prop === "isConfigured") return svc.isConfigured(); + const val = svc[prop]; + return typeof val === "function" ? val.bind(svc) : val; + }, }, -}); +); export { SecretsService }; diff --git a/packages/lib/services/seo.ts b/packages/lib/services/seo.ts index 8dd4f32fa..a5ebac5f4 100644 --- a/packages/lib/services/seo.ts +++ b/packages/lib/services/seo.ts @@ -86,7 +86,9 @@ async function callDataForSeoKeywords( keywords: string[], locale: string, locationCode?: number, -): Promise<{ keyword: string; searchVolume: number; cpc: number; competition: number }[]> { +): Promise< + { keyword: string; searchVolume: number; cpc: number; competition: number }[] +> { const login = ensureEnv("DATAFORSEO_LOGIN", "DataForSEO API access"); const password = ensureEnv("DATAFORSEO_PASSWORD", "DataForSEO API access"); @@ -220,7 +222,9 @@ async function callClaudeSeoDraft( prompt: [ `Locale: ${locale}`, pageUrl ? `Page URL: ${pageUrl}` : "", - keywords && keywords.length > 0 ? `Target keywords: ${keywords.join(", ")}` : "", + keywords && keywords.length > 0 + ? `Target keywords: ${keywords.join(", ")}` + : "", `Context: ${promptContext}`, "Return strictly JSON. Do not include markdown.", ] @@ -237,9 +241,14 @@ async function callClaudeSeoDraft( }>(text); } -async function submitIndexNow(urlToSubmit: string): Promise<{ submitted: boolean }> { +async function submitIndexNow( + urlToSubmit: string, +): Promise<{ submitted: boolean }> { const key = ensureEnv("INDEXNOW_KEY", "IndexNow submissions"); - const keyLocation = ensureEnv("INDEXNOW_KEY_LOCATION", "IndexNow key location"); + const keyLocation = ensureEnv( + "INDEXNOW_KEY_LOCATION", + "IndexNow key location", + ); const url = await assertSafeOutboundUrl(urlToSubmit); const response = await fetch("https://api.indexnow.org/indexnow", { @@ -273,7 +282,9 @@ async function runHealthCheck(pageUrl: string): Promise<{ redirect: "error", }); const body = await response.text(); - const canonicalMatch = body.match(/ { + async createRequest( + params: CreateSeoRequestParams, + ): Promise { if (params.idempotencyKey) { const existing = await seoRequestsRepository.findByIdempotency( params.organizationId, params.idempotencyKey, ); if (existing) { - const artifacts = await seoArtifactsRepository.listByRequest(existing.id); - const providerCalls = await seoProviderCallsRepository.listByRequest(existing.id); + const artifacts = await seoArtifactsRepository.listByRequest( + existing.id, + ); + const providerCalls = await seoProviderCallsRepository.listByRequest( + existing.id, + ); return { request: existing, artifacts, providerCalls }; } } @@ -324,7 +341,9 @@ export class SeoService { request: SeoRequest, params: CreateSeoRequestParams, ): Promise { - const pageUrlRequiredTypes: Array<(typeof seoRequestTypeEnum.enumValues)[number]> = [ + const pageUrlRequiredTypes: Array< + (typeof seoRequestTypeEnum.enumValues)[number] + > = [ "meta_generate", "schema_generate", "publish_bundle", @@ -347,7 +366,12 @@ export class SeoService { metadata?: Record, ) => { if (amount <= 0) return; - await chargeCredits(request.organization_id, amount, description, metadata); + await chargeCredits( + request.organization_id, + amount, + description, + metadata, + ); totalCost += amount; }; @@ -372,7 +396,8 @@ export class SeoService { }); return payload; } catch (error) { - const message = error instanceof Error ? error.message : "Provider call failed"; + const message = + error instanceof Error ? error.message : "Provider call failed"; await seoProviderCallsRepository.updateStatus(created.id, "failed", { error: message, }); @@ -401,9 +426,13 @@ export class SeoService { request.page_url ?? undefined, request.keywords ?? undefined, ); - await charge(SEO_PRICING.claudeGenerationFloor, "SEO meta generation (Claude)", { - request_id: request.id, - }); + await charge( + SEO_PRICING.claudeGenerationFloor, + "SEO meta generation (Claude)", + { + request_id: request.id, + }, + ); const artifact = await seoArtifactsRepository.create({ request_id: request.id, type: "meta", @@ -437,9 +466,13 @@ export class SeoService { request.page_url ?? undefined, request.keywords ?? undefined, ); - await charge(SEO_PRICING.claudeGenerationFloor, "SEO schema generation (Claude)", { - request_id: request.id, - }); + await charge( + SEO_PRICING.claudeGenerationFloor, + "SEO schema generation (Claude)", + { + request_id: request.id, + }, + ); const artifact = await seoArtifactsRepository.create({ request_id: request.id, type: "schema", @@ -473,9 +506,13 @@ export class SeoService { request.locale, params.locationCode, ); - await charge(SEO_PRICING.keywordResearch, "SEO keyword research (DataForSEO)", { - request_id: request.id, - }); + await charge( + SEO_PRICING.keywordResearch, + "SEO keyword research (DataForSEO)", + { + request_id: request.id, + }, + ); const artifact = await seoArtifactsRepository.create({ request_id: request.id, type: "keywords", @@ -513,9 +550,13 @@ export class SeoService { device: request.device, searchEngine: request.search_engine, }); - await charge(SEO_PRICING.serpSnapshot, "SEO SERP snapshot (SerpApi)", { - request_id: request.id, - }); + await charge( + SEO_PRICING.serpSnapshot, + "SEO SERP snapshot (SerpApi)", + { + request_id: request.id, + }, + ); const artifact = await seoArtifactsRepository.create({ request_id: request.id, type: "serp_snapshot", @@ -651,7 +692,8 @@ export class SeoService { providerCalls, }; } catch (error) { - const message = error instanceof Error ? error.message : "SEO request failed"; + const message = + error instanceof Error ? error.message : "SEO request failed"; await seoRequestsRepository.updateStatus(request.id, "failed", { error: message, }); diff --git a/packages/lib/services/server-wallets.ts b/packages/lib/services/server-wallets.ts index 3ffba4701..eaae4b093 100644 --- a/packages/lib/services/server-wallets.ts +++ b/packages/lib/services/server-wallets.ts @@ -3,7 +3,10 @@ import { StewardApiError } from "@stwd/sdk"; import { eq } from "drizzle-orm"; import { verifyMessage } from "viem"; import { db } from "@/db/client"; -import { type AgentServerWallet, agentServerWallets } from "@/db/schemas/agent-server-wallets"; +import { + type AgentServerWallet, + agentServerWallets, +} from "@/db/schemas/agent-server-wallets"; import { getPrivyClient } from "@/lib/auth/privy-client"; import { cache } from "@/lib/cache/client"; import { WALLET_PROVIDER_FLAGS } from "@/lib/config/wallet-provider-flags"; @@ -23,7 +26,9 @@ class WalletAlreadyExistsError extends Error { class PrivyWalletsDisabledError extends Error { constructor() { - super("Privy wallet creation is disabled; enable Steward for new wallets first"); + super( + "Privy wallet creation is disabled; enable Steward for new wallets first", + ); this.name = "PrivyWalletsDisabledError"; } } @@ -53,7 +58,9 @@ class RpcReplayError extends Error { class ServerWalletNotFoundError extends Error { constructor() { - super("Server wallet not found: No provisioned wallet matches this client address."); + super( + "Server wallet not found: No provisioned wallet matches this client address.", + ); this.name = "ServerWalletNotFoundError"; } } @@ -86,7 +93,8 @@ export interface ExecuteParams { function isUniqueViolation(error: unknown): boolean { const code = error instanceof Error ? Reflect.get(error, "code") : undefined; return ( - code === "23505" || (error instanceof Error && error.message.includes("unique constraint")) + code === "23505" || + (error instanceof Error && error.message.includes("unique constraint")) ); } @@ -149,16 +157,24 @@ async function provisionStewardWallet({ try { // Create agent + wallet in Steward (idempotent — 409 means already exists) - const agent = await steward.createWallet(agentName, `Agent ${agentName}`, clientAddress); + const agent = await steward.createWallet( + agentName, + `Agent ${agentName}`, + clientAddress, + ); const walletAddress = agent.walletAddress; if (!walletAddress) { - throw new Error(`Steward did not return a wallet address for agent ${agentName}`); + throw new Error( + `Steward did not return a wallet address for agent ${agentName}`, + ); } const record = await persistWalletRecord(agent.id, walletAddress); - logger.info(`[server-wallets] Provisioned Steward wallet for ${agent.id}: ${walletAddress}`); + logger.info( + `[server-wallets] Provisioned Steward wallet for ${agent.id}: ${walletAddress}`, + ); return record; } catch (error: unknown) { if (isUniqueViolation(error)) { @@ -170,11 +186,16 @@ async function provisionStewardWallet({ const walletAddress = existingAgent.walletAddress; if (!walletAddress) { - throw new Error(`Steward agent ${agentName} already exists but has no wallet address`); + throw new Error( + `Steward agent ${agentName} already exists but has no wallet address`, + ); } try { - const record = await persistWalletRecord(existingAgent.id, walletAddress); + const record = await persistWalletRecord( + existingAgent.id, + walletAddress, + ); logger.info( `[server-wallets] Reused existing Steward wallet for ${existingAgent.id}: ${walletAddress}`, ); @@ -235,7 +256,9 @@ async function provisionPrivyWallet({ if (walletApiWithDelete.delete) { await walletApiWithDelete.delete(walletId).catch(() => { - console.error(`Failed to clean up orphaned Privy wallet ${walletId}`); + console.error( + `Failed to clean up orphaned Privy wallet ${walletId}`, + ); }); } else { logger.warn( @@ -269,7 +292,11 @@ export async function getOrganizationIdForClientAddress( // RPC execution — top-level (validates signature, routes by provider) // --------------------------------------------------------------------------- -export async function executeServerWalletRpc({ clientAddress, payload, signature }: ExecuteParams) { +export async function executeServerWalletRpc({ + clientAddress, + payload, + signature, +}: ExecuteParams) { // Timestamp check const now = Date.now(); if (!payload.timestamp || now - payload.timestamp > 5 * 60 * 1000) { @@ -288,7 +315,11 @@ export async function executeServerWalletRpc({ clientAddress, payload, signature // Nonce replay protection const nonceKey = `rpc-nonce:${clientAddress}:${payload.nonce}`; - const nonceSet = await cache.setIfNotExists(nonceKey, "1", 24 * 60 * 60 * 1000); + const nonceSet = await cache.setIfNotExists( + nonceKey, + "1", + 24 * 60 * 60 * 1000, + ); if (!nonceSet) { throw new RpcReplayError(); } @@ -312,12 +343,17 @@ export async function executeServerWalletRpc({ clientAddress, payload, signature // RPC execution — Steward // --------------------------------------------------------------------------- -async function executeStewardRpc(wallet: AgentServerWallet, payload: RpcPayload) { +async function executeStewardRpc( + wallet: AgentServerWallet, + payload: RpcPayload, +) { const steward = getStewardClient(); const agentId = wallet.steward_agent_id; if (!agentId) { - throw new Error(`Wallet ${wallet.id} is marked as steward but has no steward_agent_id`); + throw new Error( + `Wallet ${wallet.id} is marked as steward but has no steward_agent_id`, + ); } switch (payload.method) { @@ -340,7 +376,10 @@ async function executeStewardRpc(wallet: AgentServerWallet, payload: RpcPayload) } case "eth_signTypedData_v4": { - const [, typedData] = payload.params as [string, string | Record]; + const [, typedData] = payload.params as [ + string, + string | Record, + ]; const parsed = typeof typedData === "string" ? (JSON.parse(typedData) as Record) @@ -348,7 +387,10 @@ async function executeStewardRpc(wallet: AgentServerWallet, payload: RpcPayload) // EIP-712 uses "message" but SDK expects "value" return steward.signTypedData(agentId, { domain: parsed.domain as Record, - types: parsed.types as Record>, + types: parsed.types as Record< + string, + Array<{ name: string; type: string }> + >, primaryType: parsed.primaryType as string, value: (parsed.message ?? parsed.value) as Record, }); @@ -368,7 +410,9 @@ async function executeStewardRpc(wallet: AgentServerWallet, payload: RpcPayload) async function executePrivyRpc(wallet: AgentServerWallet, payload: RpcPayload) { if (!wallet.privy_wallet_id) { - throw new Error(`Wallet ${wallet.id} is marked as privy but has no privy_wallet_id`); + throw new Error( + `Wallet ${wallet.id} is marked as privy but has no privy_wallet_id`, + ); } const privy = getPrivyClient(); diff --git a/packages/lib/services/signup-code.ts b/packages/lib/services/signup-code.ts index ca57e4904..54c9132d2 100644 --- a/packages/lib/services/signup-code.ts +++ b/packages/lib/services/signup-code.ts @@ -29,8 +29,11 @@ function loadCodes(): Map { try { data = JSON.parse(raw) as SignupCodesConfig; } catch (err) { - const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err); - logger.warn(`[SignupCode] Invalid SIGNUP_CODES_JSON (${message}), using no codes`); + const message = + err instanceof Error ? `${err.name}: ${err.message}` : String(err); + logger.warn( + `[SignupCode] Invalid SIGNUP_CODES_JSON (${message}), using no codes`, + ); return new Map(); } const codes = data.codes; @@ -41,7 +44,8 @@ function loadCodes(): Map { for (const [code, amount] of Object.entries(codes)) { const normalized = code?.trim().toLowerCase(); if (!normalized) continue; - const num = typeof amount === "number" ? amount : parseFloat(String(amount)); + const num = + typeof amount === "number" ? amount : parseFloat(String(amount)); if (!isNaN(num) && num > 0) { map.set(normalized, num); } @@ -64,7 +68,9 @@ export function getBonusForCode(code: string): number | undefined { return getCodes().get(code.trim().toLowerCase()); } -export async function hasUsedSignupCode(organizationId: string): Promise { +export async function hasUsedSignupCode( + organizationId: string, +): Promise { return creditTransactionsRepository.hasSignupCodeBonus(organizationId); } @@ -74,7 +80,10 @@ function redactCode(code: string): string { return s.slice(0, 2) + "***"; } -export async function redeemSignupCode(organizationId: string, code: string): Promise { +export async function redeemSignupCode( + organizationId: string, + code: string, +): Promise { const bonus = getBonusForCode(code); if (bonus === undefined) { throw new Error(ERRORS.INVALID_CODE); diff --git a/packages/lib/services/social-media/alerts.ts b/packages/lib/services/social-media/alerts.ts index 3e872cf3b..2de8fcd05 100644 --- a/packages/lib/services/social-media/alerts.ts +++ b/packages/lib/services/social-media/alerts.ts @@ -16,7 +16,10 @@ const SEVERITY_COLORS: Record = { low: { hex: "#00CED1", emoji: "ℹ️" }, }; -async function sendDiscordAlert(webhookUrl: string, payload: AlertPayload): Promise { +async function sendDiscordAlert( + webhookUrl: string, + payload: AlertPayload, +): Promise { const { hex, emoji } = SEVERITY_COLORS[payload.severity]; await fetch(webhookUrl, { @@ -44,7 +47,10 @@ async function sendDiscordAlert(webhookUrl: string, payload: AlertPayload): Prom }); } -async function sendSlackAlert(webhookUrl: string, payload: AlertPayload): Promise { +async function sendSlackAlert( + webhookUrl: string, + payload: AlertPayload, +): Promise { const { emoji } = SEVERITY_COLORS[payload.severity]; await fetch(webhookUrl, { @@ -89,7 +95,9 @@ async function sendTelegramAlert( `${emoji} *${payload.title}*`, "", payload.message, - ...(payload.platforms?.length ? ["", `Platforms: ${payload.platforms.join(", ")}`] : []), + ...(payload.platforms?.length + ? ["", `Platforms: ${payload.platforms.join(", ")}`] + : []), ].join("\n"); await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, { @@ -110,7 +118,9 @@ async function sendWhatsAppAlert( `${emoji} ${payload.title}`, "", payload.message, - ...(payload.platforms?.length ? ["", `Platforms: ${payload.platforms.join(", ")}`] : []), + ...(payload.platforms?.length + ? ["", `Platforms: ${payload.platforms.join(", ")}`] + : []), ].join("\n"); await fetch(apiUrl, { @@ -123,7 +133,9 @@ async function sendWhatsAppAlert( }); } -export async function sendSocialMediaAlert(payload: AlertPayload): Promise { +export async function sendSocialMediaAlert( + payload: AlertPayload, +): Promise { const sends: Promise[] = []; const discordWebhook = process.env.SOCIAL_ALERTS_DISCORD_WEBHOOK; @@ -136,8 +148,10 @@ export async function sendSocialMediaAlert(payload: AlertPayload): Promise if (discordWebhook) sends.push(sendDiscordAlert(discordWebhook, payload)); if (slackWebhook) sends.push(sendSlackAlert(slackWebhook, payload)); - if (tgToken && tgChat) sends.push(sendTelegramAlert(tgToken, tgChat, payload)); - if (waUrl && waKey && waTo) sends.push(sendWhatsAppAlert(waUrl, waKey, waTo, payload)); + if (tgToken && tgChat) + sends.push(sendTelegramAlert(tgToken, tgChat, payload)); + if (waUrl && waKey && waTo) + sends.push(sendWhatsAppAlert(waUrl, waKey, waTo, payload)); if (sends.length === 0) { logger.warn("[SocialMediaAlerts] No alert channels configured"); @@ -147,7 +161,9 @@ export async function sendSocialMediaAlert(payload: AlertPayload): Promise const results = await Promise.allSettled(sends); const failures = results.filter((r) => r.status === "rejected").length; if (failures > 0) - logger.error(`[SocialMediaAlerts] ${failures}/${results.length} channels failed`); + logger.error( + `[SocialMediaAlerts] ${failures}/${results.length} channels failed`, + ); } export async function alertOnPostFailure( @@ -158,13 +174,18 @@ export async function alertOnPostFailure( const allFailed = _errors.length === platforms.length; await sendSocialMediaAlert({ severity: allFailed ? "high" : "medium", - title: allFailed ? "All Social Media Posts Failed" : "Partial Social Media Post Failure", + title: allFailed + ? "All Social Media Posts Failed" + : "Partial Social Media Post Failure", message: `${_errors.length}/${platforms.length} posts failed for org ${organizationId.slice(0, 8)}...`, platforms, }); } -export async function alertOnTokenExpiry(organizationId: string, platform: string): Promise { +export async function alertOnTokenExpiry( + organizationId: string, + platform: string, +): Promise { await sendSocialMediaAlert({ severity: "medium", title: "Social Media Token Expired", @@ -173,7 +194,10 @@ export async function alertOnTokenExpiry(organizationId: string, platform: strin }); } -export async function alertOnRateLimit(platform: string, retryAfter?: number): Promise { +export async function alertOnRateLimit( + platform: string, + retryAfter?: number, +): Promise { await sendSocialMediaAlert({ severity: "low", title: "Social Media Rate Limited", diff --git a/packages/lib/services/social-media/index.ts b/packages/lib/services/social-media/index.ts index bc3c5fd03..2bab37683 100644 --- a/packages/lib/services/social-media/index.ts +++ b/packages/lib/services/social-media/index.ts @@ -30,7 +30,11 @@ import { slackProvider } from "./providers/slack"; import { telegramProvider } from "./providers/telegram"; import { tiktokProvider } from "./providers/tiktok"; import { twitterProvider } from "./providers/twitter"; -import { getRefreshGuidance, needsRefresh, refreshToken } from "./token-refresh"; +import { + getRefreshGuidance, + needsRefresh, + refreshToken, +} from "./token-refresh"; const POST_CREDIT_COST = 0.01; @@ -83,7 +87,8 @@ class SocialMediaService { .where(and(...conditions)) .limit(1); - if (!credential) return this.getCredentialsFromSecrets(organizationId, platform); + if (!credential) + return this.getCredentialsFromSecrets(organizationId, platform); // Handle manual credentials (Bluesky app password, Telegram bot token) if (credential.api_key_secret_id) { @@ -91,7 +96,8 @@ class SocialMediaService { credential.api_key_secret_id, organizationId, ); - if (!apiKey) return this.getCredentialsFromSecrets(organizationId, platform); + if (!apiKey) + return this.getCredentialsFromSecrets(organizationId, platform); if (platform === "bluesky") { return { @@ -113,14 +119,21 @@ class SocialMediaService { // Handle OAuth credentials const [accessToken, storedRefreshToken] = await Promise.all([ credential.access_token_secret_id - ? secretsService.getDecryptedValue(credential.access_token_secret_id, organizationId) + ? secretsService.getDecryptedValue( + credential.access_token_secret_id, + organizationId, + ) : undefined, credential.refresh_token_secret_id - ? secretsService.getDecryptedValue(credential.refresh_token_secret_id, organizationId) + ? secretsService.getDecryptedValue( + credential.refresh_token_secret_id, + organizationId, + ) : undefined, ]); - if (!accessToken) return this.getCredentialsFromSecrets(organizationId, platform); + if (!accessToken) + return this.getCredentialsFromSecrets(organizationId, platform); let credentials: SocialCredentials = { platform, @@ -141,10 +154,14 @@ class SocialMediaService { // Auto-refresh if token is expired if (needsRefresh(credentials)) { - const refreshed = await refreshToken(platform, credentials).catch((err) => { - logger.warn(`[SocialMedia] Token refresh failed for ${platform}: ${err.message}`); - return null; - }); + const refreshed = await refreshToken(platform, credentials).catch( + (err) => { + logger.warn( + `[SocialMedia] Token refresh failed for ${platform}: ${err.message}`, + ); + return null; + }, + ); if (refreshed) { credentials = { @@ -152,9 +169,13 @@ class SocialMediaService { accessToken: refreshed.accessToken, refreshToken: refreshed.refreshToken, }; - this.updateStoredToken(organizationId, credential.id, refreshed).catch((err) => { - logger.error(`[SocialMedia] Failed to persist refreshed token: ${err.message}`); - }); + this.updateStoredToken(organizationId, credential.id, refreshed).catch( + (err) => { + logger.error( + `[SocialMedia] Failed to persist refreshed token: ${err.message}`, + ); + }, + ); logger.info(`[SocialMedia] Token refreshed for ${platform}`); } else { throw new Error(`Token expired. ${getRefreshGuidance(platform)}`); @@ -253,7 +274,10 @@ class SocialMediaService { }; } case "telegram": { - const botToken = await secretsService.get(organizationId, `${prefix}_BOT_TOKEN`); + const botToken = await secretsService.get( + organizationId, + `${prefix}_BOT_TOKEN`, + ); if (!botToken) return null; return { platform, botToken }; } @@ -269,7 +293,10 @@ class SocialMediaService { } case "tiktok": case "linkedin": { - const accessToken = await secretsService.get(organizationId, `${prefix}_ACCESS_TOKEN`); + const accessToken = await secretsService.get( + organizationId, + `${prefix}_ACCESS_TOKEN`, + ); if (!accessToken) return null; return { platform, accessToken }; } @@ -314,7 +341,14 @@ class SocialMediaService { } async createPost(input: CreatePostInput): Promise { - const { organizationId, userId, content, platforms, platformOptions, credentialIds } = input; + const { + organizationId, + userId, + content, + platforms, + platformOptions, + credentialIds, + } = input; logger.info("[SocialMedia] Creating post", { organizationId, @@ -330,7 +364,8 @@ class SocialMediaService { metadata: { userId, platforms }, }); - if (!deduction.success) throw new Error(`Insufficient credits: need $${totalCost.toFixed(4)}`); + if (!deduction.success) + throw new Error(`Insufficient credits: need $${totalCost.toFixed(4)}`); const results = await Promise.all( platforms.map(async (platform): Promise => { @@ -355,7 +390,11 @@ class SocialMediaService { }; try { - return await provider.createPost(credentials, content, platformOptions); + return await provider.createPost( + credentials, + content, + platformOptions, + ); } catch (error) { const errorMessage = extractErrorMessage(error); logger.error("[SocialMedia] Post failed", { @@ -418,12 +457,15 @@ class SocialMediaService { platform, credentialId, ); - if (!credentials) return { success: false, error: `No credentials found for ${platform}` }; + if (!credentials) + return { success: false, error: `No credentials found for ${platform}` }; return provider.deletePost(credentials, postId); } - async getPostAnalytics(input: GetAnalyticsInput): Promise { + async getPostAnalytics( + input: GetAnalyticsInput, + ): Promise { const { organizationId, platform, postId, credentialId } = input; if (!postId) throw new Error("postId is required for post analytics"); @@ -465,7 +507,8 @@ class SocialMediaService { credentialId?: string, ): Promise<{ mediaId: string; url?: string }> { const provider = this.getProvider(platform); - if (!provider.uploadMedia) throw new Error(`Media upload not supported for ${platform}`); + if (!provider.uploadMedia) + throw new Error(`Media upload not supported for ${platform}`); const credentials = await this.getCredentialsForPlatform( organizationId, @@ -512,9 +555,15 @@ class SocialMediaService { metadata: { platform, postId }, }); - if (!deduction.success) return { platform, success: false, error: "Insufficient credits" }; + if (!deduction.success) + return { platform, success: false, error: "Insufficient credits" }; - const result = await provider.replyToPost(credentials, postId, content, options); + const result = await provider.replyToPost( + credentials, + postId, + content, + options, + ); if (!result.success) { await creditsService.refundCredits({ @@ -535,14 +584,16 @@ class SocialMediaService { credentialId?: string, ): Promise<{ success: boolean; error?: string }> { const provider = this.getProvider(platform); - if (!provider.likePost) return { success: false, error: `Like not supported for ${platform}` }; + if (!provider.likePost) + return { success: false, error: `Like not supported for ${platform}` }; const credentials = await this.getCredentialsForPlatform( organizationId, platform, credentialId, ); - if (!credentials) return { success: false, error: `No credentials found for ${platform}` }; + if (!credentials) + return { success: false, error: `No credentials found for ${platform}` }; return provider.likePost(credentials, postId); } @@ -594,7 +645,8 @@ class SocialMediaService { platform, credentialId, ); - if (!credentials) return { valid: false, error: `No credentials found for ${platform}` }; + if (!credentials) + return { valid: false, error: `No credentials found for ${platform}` }; return provider.validateCredentials(credentials); } diff --git a/packages/lib/services/social-media/providers/bluesky.ts b/packages/lib/services/social-media/providers/bluesky.ts index 599a1c0a8..442b781c2 100644 --- a/packages/lib/services/social-media/providers/bluesky.ts +++ b/packages/lib/services/social-media/providers/bluesky.ts @@ -40,7 +40,10 @@ interface BskyFacet { features: Array<{ $type: string; uri?: string; tag?: string; did?: string }>; } -async function createSession(handle: string, appPassword: string): Promise { +async function createSession( + handle: string, + appPassword: string, +): Promise { const { data } = await withRetry( () => fetch(`${BLUESKY_SERVICE}/xrpc/com.atproto.server.createSession`, { @@ -95,14 +98,17 @@ async function uploadBlob( size: number; }; }> { - const response = await fetch(`${BLUESKY_SERVICE}/xrpc/com.atproto.repo.uploadBlob`, { - method: "POST", - headers: { - Authorization: `Bearer ${accessJwt}`, - "Content-Type": mimeType, + const response = await fetch( + `${BLUESKY_SERVICE}/xrpc/com.atproto.repo.uploadBlob`, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessJwt}`, + "Content-Type": mimeType, + }, + body: new Uint8Array(data), }, - body: new Uint8Array(data), - }); + ); if (!response.ok) { throw new Error(`Blob upload failed: ${response.status}`); @@ -162,7 +168,10 @@ export const blueskyProvider: SocialMediaProvider = { } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); // Get profile const profile = await bskyApiRequest<{ data: BskyProfile }>( @@ -199,7 +208,10 @@ export const blueskyProvider: SocialMediaProvider = { } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); // Build post record const record: Record = { @@ -241,7 +253,11 @@ export const blueskyProvider: SocialMediaProvider = { continue; } - const blobResponse = await uploadBlob(session.accessJwt, imageData, media.mimeType); + const blobResponse = await uploadBlob( + session.accessJwt, + imageData, + media.mimeType, + ); images.push({ alt: media.altText || "", @@ -314,7 +330,10 @@ export const blueskyProvider: SocialMediaProvider = { } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); // Extract rkey from URI const rkey = postId.split("/").pop(); @@ -346,7 +365,10 @@ export const blueskyProvider: SocialMediaProvider = { } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); const response = await bskyApiRequest<{ post: { @@ -354,7 +376,10 @@ export const blueskyProvider: SocialMediaProvider = { repostCount: number; replyCount: number; }; - }>(`app.bsky.feed.getPostThread?uri=${encodeURIComponent(postId)}`, session.accessJwt); + }>( + `app.bsky.feed.getPostThread?uri=${encodeURIComponent(postId)}`, + session.accessJwt, + ); return { platform: "bluesky", @@ -371,13 +396,18 @@ export const blueskyProvider: SocialMediaProvider = { } }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if (!credentials.handle || !credentials.appPassword) { return null; } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); const response = await bskyApiRequest( `app.bsky.actor.getProfile?actor=${session.did}`, @@ -404,7 +434,10 @@ export const blueskyProvider: SocialMediaProvider = { throw new Error("Handle and app password required"); } - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); let imageData: Buffer; if (media.data) { @@ -418,7 +451,11 @@ export const blueskyProvider: SocialMediaProvider = { throw new Error("No media data provided"); } - const blobResponse = await uploadBlob(session.accessJwt, imageData, media.mimeType); + const blobResponse = await uploadBlob( + session.accessJwt, + imageData, + media.mimeType, + ); return { mediaId: blobResponse.blob.ref.$link, @@ -431,7 +468,11 @@ export const blueskyProvider: SocialMediaProvider = { content: PostContent, options?: PlatformPostOptions, ): Promise { - return this.createPost(credentials, { ...content, replyToId: postId }, options); + return this.createPost( + credentials, + { ...content, replyToId: postId }, + options, + ); }, async likePost(credentials: SocialCredentials, postId: string) { @@ -440,7 +481,10 @@ export const blueskyProvider: SocialMediaProvider = { } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); // Get the post to get its CID const postResponse = await bskyApiRequest<{ @@ -472,7 +516,10 @@ export const blueskyProvider: SocialMediaProvider = { } }, - async repost(credentials: SocialCredentials, postId: string): Promise { + async repost( + credentials: SocialCredentials, + postId: string, + ): Promise { if (!credentials.handle || !credentials.appPassword) { return { platform: "bluesky", @@ -482,7 +529,10 @@ export const blueskyProvider: SocialMediaProvider = { } try { - const session = await createSession(credentials.handle, credentials.appPassword); + const session = await createSession( + credentials.handle, + credentials.appPassword, + ); // Get the post to get its CID const postResponse = await bskyApiRequest<{ diff --git a/packages/lib/services/social-media/providers/discord.ts b/packages/lib/services/social-media/providers/discord.ts index 958541a0e..14fb7d4f4 100644 --- a/packages/lib/services/social-media/providers/discord.ts +++ b/packages/lib/services/social-media/providers/discord.ts @@ -57,7 +57,8 @@ async function discordApiRequest( }), async (response) => { const json = await response.json(); - if (json.code) throw new Error(json.message || `Discord error ${json.code}`); + if (json.code) + throw new Error(json.message || `Discord error ${json.code}`); return json; }, { platform: "discord", maxRetries: 3 }, @@ -65,8 +66,13 @@ async function discordApiRequest( return data; } -async function webhookRequest(webhookUrl: string, payload: Record): Promise { - const url = webhookUrl.includes("?") ? `${webhookUrl}&wait=true` : `${webhookUrl}?wait=true`; +async function webhookRequest( + webhookUrl: string, + payload: Record, +): Promise { + const url = webhookUrl.includes("?") + ? `${webhookUrl}&wait=true` + : `${webhookUrl}?wait=true`; const { data } = await withRetry( () => fetch(url, { @@ -76,7 +82,8 @@ async function webhookRequest(webhookUrl: string, payload: Record { const json = await response.json(); - if (json.code) throw new Error(json.message || `Webhook error ${json.code}`); + if (json.code) + throw new Error(json.message || `Webhook error ${json.code}`); return json; }, { platform: "discord", maxRetries: 3 }, @@ -115,7 +122,10 @@ export const discordProvider: SocialMediaProvider = { } try { - const user = await discordApiRequest("/users/@me", credentials.botToken); + const user = await discordApiRequest( + "/users/@me", + credentials.botToken, + ); return { valid: true, @@ -178,7 +188,10 @@ export const discordProvider: SocialMediaProvider = { // Use webhook if provided if (credentials.webhookUrl) { - message = await webhookRequest(credentials.webhookUrl, payload); + message = await webhookRequest( + credentials.webhookUrl, + payload, + ); } else if (credentials.botToken) { // Use bot token with channel ID const channelId = options?.discord?.channelId || credentials.channelId; diff --git a/packages/lib/services/social-media/providers/linkedin.ts b/packages/lib/services/social-media/providers/linkedin.ts index d480103b1..877de65fe 100644 --- a/packages/lib/services/social-media/providers/linkedin.ts +++ b/packages/lib/services/social-media/providers/linkedin.ts @@ -47,7 +47,9 @@ async function linkedinApiRequest( accessToken: string, options: RequestInit = {}, ): Promise { - const url = endpoint.startsWith("http") ? endpoint : `${LINKEDIN_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${LINKEDIN_API_BASE}${endpoint}`; const { data } = await withRetry( () => @@ -89,7 +91,10 @@ export const linkedinProvider: SocialMediaProvider = { } try { - const profile = await linkedinApiRequest("/me", credentials.accessToken); + const profile = await linkedinApiRequest( + "/me", + credentials.accessToken, + ); return { valid: true, @@ -159,7 +164,9 @@ export const linkedinProvider: SocialMediaProvider = { // Handle link share if (content.link) { - (ugcPost.specificContent as Record)["com.linkedin.ugc.ShareContent"] = { + (ugcPost.specificContent as Record)[ + "com.linkedin.ugc.ShareContent" + ] = { shareCommentary: { text: content.text }, shareMediaCategory: "ARTICLE", media: [ @@ -167,7 +174,9 @@ export const linkedinProvider: SocialMediaProvider = { status: "READY", originalUrl: content.link, title: { text: content.linkTitle || content.link }, - description: content.linkDescription ? { text: content.linkDescription } : undefined, + description: content.linkDescription + ? { text: content.linkDescription } + : undefined, }, ], }; @@ -185,25 +194,26 @@ export const linkedinProvider: SocialMediaProvider = { if (media.url) { // For URL-based images, we need to register and upload // Register upload - const registerResponse = await linkedinApiRequest( - "/assets?action=registerUpload", - credentials.accessToken, - { - method: "POST", - body: JSON.stringify({ - registerUploadRequest: { - recipes: ["urn:li:digitalmediaRecipe:feedshare-image"], - owner: authorUrn, - serviceRelationships: [ - { - relationshipType: "OWNER", - identifier: "urn:li:userGeneratedContent", - }, - ], - }, - }), - }, - ); + const registerResponse = + await linkedinApiRequest( + "/assets?action=registerUpload", + credentials.accessToken, + { + method: "POST", + body: JSON.stringify({ + registerUploadRequest: { + recipes: ["urn:li:digitalmediaRecipe:feedshare-image"], + owner: authorUrn, + serviceRelationships: [ + { + relationshipType: "OWNER", + identifier: "urn:li:userGeneratedContent", + }, + ], + }, + }), + }, + ); const uploadUrl = registerResponse.value.uploadMechanism[ @@ -233,7 +243,9 @@ export const linkedinProvider: SocialMediaProvider = { } if (mediaAssets.length > 0) { - (ugcPost.specificContent as Record)["com.linkedin.ugc.ShareContent"] = { + (ugcPost.specificContent as Record)[ + "com.linkedin.ugc.ShareContent" + ] = { shareCommentary: { text: content.text }, shareMediaCategory: "IMAGE", media: mediaAssets, @@ -275,9 +287,13 @@ export const linkedinProvider: SocialMediaProvider = { } try { - await linkedinApiRequest(`/ugcPosts/${encodeURIComponent(postId)}`, credentials.accessToken, { - method: "DELETE", - }); + await linkedinApiRequest( + `/ugcPosts/${encodeURIComponent(postId)}`, + credentials.accessToken, + { + method: "DELETE", + }, + ); return { success: true }; } catch (error) { @@ -303,7 +319,10 @@ export const linkedinProvider: SocialMediaProvider = { likesSummary?: { totalLikes: number }; commentsSummary?: { totalComments: number }; }>; - }>(`/socialActions/${encodeURIComponent(postId)}`, credentials.accessToken); + }>( + `/socialActions/${encodeURIComponent(postId)}`, + credentials.accessToken, + ); const element = response.elements?.[0]; if (!element) return null; @@ -322,13 +341,18 @@ export const linkedinProvider: SocialMediaProvider = { } }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if (!credentials.accessToken) { return null; } try { - const profile = await linkedinApiRequest("/me", credentials.accessToken); + const profile = await linkedinApiRequest( + "/me", + credentials.accessToken, + ); // LinkedIn doesn't provide follower counts via the basic API // Would need Marketing API for that diff --git a/packages/lib/services/social-media/providers/mastodon.ts b/packages/lib/services/social-media/providers/mastodon.ts index 906bc1c63..a14f637be 100644 --- a/packages/lib/services/social-media/providers/mastodon.ts +++ b/packages/lib/services/social-media/providers/mastodon.ts @@ -55,7 +55,10 @@ interface MastodonMedia { } function getInstanceUrl(credentials: SocialCredentials): string { - const url = credentials.instanceUrl ?? credentials.webhookUrl ?? "https://mastodon.social"; + const url = + credentials.instanceUrl ?? + credentials.webhookUrl ?? + "https://mastodon.social"; return url.replace(/\/$/, ""); } @@ -108,7 +111,11 @@ async function uploadMedia( const fileBytes = Uint8Array.from(fileData); const formData = new FormData(); - formData.append("file", new Blob([fileBytes], { type: media.mimeType }), "upload"); + formData.append( + "file", + new Blob([fileBytes], { type: media.mimeType }), + "upload", + ); if (media.altText) { formData.append("description", media.altText); } @@ -175,7 +182,11 @@ export const mastodonProvider: SocialMediaProvider = { const mediaIds: string[] = []; if (content.media?.length) { for (const media of content.media) { - const uploaded = await uploadMedia(instanceUrl, credentials.accessToken, media); + const uploaded = await uploadMedia( + instanceUrl, + credentials.accessToken, + media, + ); mediaIds.push(uploaded.id); } } @@ -188,7 +199,8 @@ export const mastodonProvider: SocialMediaProvider = { if (mediaIds.length > 0) payload.media_ids = mediaIds; if (content.replyToId) payload.in_reply_to_id = content.replyToId; if (mastodonOptions?.sensitive) payload.sensitive = true; - if (mastodonOptions?.spoilerText) payload.spoiler_text = mastodonOptions.spoilerText; + if (mastodonOptions?.spoilerText) + payload.spoiler_text = mastodonOptions.spoilerText; if (mastodonOptions?.language) payload.language = mastodonOptions.language; if (mastodonOptions?.pollOptions?.length) { payload.poll = { @@ -220,9 +232,14 @@ export const mastodonProvider: SocialMediaProvider = { const instanceUrl = getInstanceUrl(credentials); - await mastodonApiRequest(instanceUrl, `/statuses/${postId}`, credentials.accessToken, { - method: "DELETE", - }); + await mastodonApiRequest( + instanceUrl, + `/statuses/${postId}`, + credentials.accessToken, + { + method: "DELETE", + }, + ); return { success: true }; }, @@ -233,7 +250,11 @@ export const mastodonProvider: SocialMediaProvider = { content: PostContent, options?: PlatformPostOptions, ): Promise { - return this.createPost(credentials, { ...content, replyToId: postId }, options); + return this.createPost( + credentials, + { ...content, replyToId: postId }, + options, + ); }, async likePost(credentials: SocialCredentials, postId: string) { @@ -253,7 +274,10 @@ export const mastodonProvider: SocialMediaProvider = { return { success: true }; }, - async repost(credentials: SocialCredentials, postId: string): Promise { + async repost( + credentials: SocialCredentials, + postId: string, + ): Promise { if (!credentials.accessToken) { return { platform: "mastodon", @@ -285,7 +309,11 @@ export const mastodonProvider: SocialMediaProvider = { } const instanceUrl = getInstanceUrl(credentials); - const uploaded = await uploadMedia(instanceUrl, credentials.accessToken, media); + const uploaded = await uploadMedia( + instanceUrl, + credentials.accessToken, + media, + ); return { mediaId: uploaded.id, @@ -319,7 +347,9 @@ export const mastodonProvider: SocialMediaProvider = { }; }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if (!credentials.accessToken) return null; const instanceUrl = getInstanceUrl(credentials); diff --git a/packages/lib/services/social-media/providers/meta.ts b/packages/lib/services/social-media/providers/meta.ts index 07208b3e9..13f610402 100644 --- a/packages/lib/services/social-media/providers/meta.ts +++ b/packages/lib/services/social-media/providers/meta.ts @@ -51,7 +51,9 @@ async function graphApiRequest( accessToken: string, options: RequestInit = {}, ): Promise { - const url = new URL(endpoint.startsWith("http") ? endpoint : `${GRAPH_API_BASE}${endpoint}`); + const url = new URL( + endpoint.startsWith("http") ? endpoint : `${GRAPH_API_BASE}${endpoint}`, + ); if (!options.method || options.method === "GET") { url.searchParams.set("access_token", accessToken); } @@ -64,7 +66,8 @@ async function graphApiRequest( }), async (response) => { const json = await response.json(); - if ((json as GraphApiError).error) throw new Error((json as GraphApiError).error!.message); + if ((json as GraphApiError).error) + throw new Error((json as GraphApiError).error!.message); return json; }, { platform: "facebook", maxRetries: 3 }, @@ -436,7 +439,10 @@ export const metaProvider: SocialMediaProvider = { id: string; like_count?: number; comments_count?: number; - }>(`/${postId}?fields=id,like_count,comments_count`, credentials.accessToken); + }>( + `/${postId}?fields=id,like_count,comments_count`, + credentials.accessToken, + ); return { platform: "instagram", @@ -453,7 +459,9 @@ export const metaProvider: SocialMediaProvider = { } }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if (!credentials.accessToken) { return null; } @@ -488,7 +496,10 @@ export const metaProvider: SocialMediaProvider = { id: string; name: string; fan_count?: number; - }>(`/${credentials.pageId}?fields=id,name,fan_count`, credentials.accessToken); + }>( + `/${credentials.pageId}?fields=id,name,fan_count`, + credentials.accessToken, + ); return { platform: "facebook", diff --git a/packages/lib/services/social-media/providers/reddit.ts b/packages/lib/services/social-media/providers/reddit.ts index 2bd847453..7b5d093a3 100644 --- a/packages/lib/services/social-media/providers/reddit.ts +++ b/packages/lib/services/social-media/providers/reddit.ts @@ -85,7 +85,9 @@ async function redditApiRequest( accessToken: string, options: RequestInit = {}, ): Promise { - const url = endpoint.startsWith("http") ? endpoint : `${REDDIT_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${REDDIT_API_BASE}${endpoint}`; const { data } = await withRetry( () => @@ -114,7 +116,9 @@ async function _redditApiRequestLegacy( accessToken: string, options: RequestInit = {}, ): Promise { - const url = endpoint.startsWith("http") ? endpoint : `${REDDIT_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${REDDIT_API_BASE}${endpoint}`; const response = await fetch(url, { ...options, @@ -158,9 +162,13 @@ export const redditProvider: SocialMediaProvider = { credentials.password, ); - const user = await redditApiRequest<{ data: RedditUser }>("/api/v1/me", accessToken, { - headers: { "Content-Type": "application/json" }, - }); + const user = await redditApiRequest<{ data: RedditUser }>( + "/api/v1/me", + accessToken, + { + headers: { "Content-Type": "application/json" }, + }, + ); return { valid: true, @@ -276,7 +284,9 @@ export const redditProvider: SocialMediaProvider = { }); if (response.json.errors?.length) { - const errorMessage = response.json.errors.map((e) => e.join(": ")).join(", "); + const errorMessage = response.json.errors + .map((e) => e.join(": ")) + .join(", "); return { platform: "reddit", success: false, @@ -391,7 +401,9 @@ export const redditProvider: SocialMediaProvider = { } }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if ( !credentials.apiKey || !credentials.apiSecret || @@ -409,15 +421,20 @@ export const redditProvider: SocialMediaProvider = { credentials.password, ); - const user = await redditApiRequest<{ data: RedditUser }>("/api/v1/me", accessToken, { - headers: { "Content-Type": "application/json" }, - }); + const user = await redditApiRequest<{ data: RedditUser }>( + "/api/v1/me", + accessToken, + { + headers: { "Content-Type": "application/json" }, + }, + ); return { platform: "reddit", accountId: user.data.id, metrics: { - totalPosts: (user.data.link_karma || 0) + (user.data.comment_karma || 0), + totalPosts: + (user.data.link_karma || 0) + (user.data.comment_karma || 0), }, fetchedAt: new Date(), }; diff --git a/packages/lib/services/social-media/providers/slack.ts b/packages/lib/services/social-media/providers/slack.ts index 66984880f..bfcb21ef6 100644 --- a/packages/lib/services/social-media/providers/slack.ts +++ b/packages/lib/services/social-media/providers/slack.ts @@ -79,7 +79,10 @@ async function slackApiRequest( return data as T; } -async function sendWebhook(webhookUrl: string, payload: Record): Promise { +async function sendWebhook( + webhookUrl: string, + payload: Record, +): Promise { const response = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -154,15 +157,18 @@ export const slackProvider: SocialMediaProvider = { } try { - const response = await slackApiRequest( - "auth.test", - credentials.botToken, - ); + const response = await slackApiRequest< + SlackResponse & { user: SlackUser } + >("auth.test", credentials.botToken); return { valid: true, - accountId: response.user?.id ?? ((response as Record).user_id as string), - username: response.user?.name ?? ((response as Record).user as string), + accountId: + response.user?.id ?? + ((response as Record).user_id as string), + username: + response.user?.name ?? + ((response as Record).user as string), displayName: response.user?.real_name, }; } catch (error) { @@ -213,7 +219,9 @@ export const slackProvider: SocialMediaProvider = { } const channelId = - options?.slack?.channelId ?? options?.discord?.channelId ?? credentials.channelId; + options?.slack?.channelId ?? + options?.discord?.channelId ?? + credentials.channelId; if (!channelId) { return { platform: "slack", @@ -260,7 +268,8 @@ export const slackProvider: SocialMediaProvider = { if (!channel) { return { success: false, - error: "Channel ID required (use channelId/ts format or set channel in credentials)", + error: + "Channel ID required (use channelId/ts format or set channel in credentials)", }; } @@ -307,7 +316,10 @@ export const slackProvider: SocialMediaProvider = { return { success: false, error: "Bot token required" }; } - const { channel, ts: timestamp } = parsePostId(postId, credentials.channelId); + const { channel, ts: timestamp } = parsePostId( + postId, + credentials.channelId, + ); if (!channel) { return { success: false, error: "Channel ID required" }; } @@ -348,7 +360,11 @@ export const slackProvider: SocialMediaProvider = { const fileBytes = Uint8Array.from(fileData); const formData = new FormData(); - formData.append("file", new Blob([fileBytes], { type: media.mimeType }), filename); + formData.append( + "file", + new Blob([fileBytes], { type: media.mimeType }), + filename, + ); formData.append("filename", filename); const response = await fetch(`${SLACK_API_BASE}/files.upload`, { diff --git a/packages/lib/services/social-media/providers/telegram.ts b/packages/lib/services/social-media/providers/telegram.ts index 47019c93e..64deb8e5d 100644 --- a/packages/lib/services/social-media/providers/telegram.ts +++ b/packages/lib/services/social-media/providers/telegram.ts @@ -55,7 +55,8 @@ async function telegramApiRequest( const payload = (await response.json()) as TelegramResponse; if (!payload.ok) { throw new Error( - payload.description ?? `Telegram API error: ${payload.error_code ?? response.status}`, + payload.description ?? + `Telegram API error: ${payload.error_code ?? response.status}`, ); } return payload.result as T; @@ -93,7 +94,10 @@ export const telegramProvider: SocialMediaProvider = { } try { - const user = await telegramApiRequest(credentials.botToken, "getMe"); + const user = await telegramApiRequest( + credentials.botToken, + "getMe", + ); return { valid: true, @@ -144,23 +148,31 @@ export const telegramProvider: SocialMediaProvider = { if (content.media.length === 1) { const media = content.media[0]; if (media.type === "video") { - message = await telegramApiRequest(credentials.botToken, "sendVideo", { - chat_id: chatId, - video: media.url, - caption: content.text, - parse_mode: options?.telegram?.parseMode || "HTML", - reply_to_message_id: options?.telegram?.replyToMessageId, - disable_notification: options?.telegram?.disableNotification, - }); + message = await telegramApiRequest( + credentials.botToken, + "sendVideo", + { + chat_id: chatId, + video: media.url, + caption: content.text, + parse_mode: options?.telegram?.parseMode || "HTML", + reply_to_message_id: options?.telegram?.replyToMessageId, + disable_notification: options?.telegram?.disableNotification, + }, + ); } else { - message = await telegramApiRequest(credentials.botToken, "sendPhoto", { - chat_id: chatId, - photo: media.url, - caption: content.text, - parse_mode: options?.telegram?.parseMode || "HTML", - reply_to_message_id: options?.telegram?.replyToMessageId, - disable_notification: options?.telegram?.disableNotification, - }); + message = await telegramApiRequest( + credentials.botToken, + "sendPhoto", + { + chat_id: chatId, + photo: media.url, + caption: content.text, + parse_mode: options?.telegram?.parseMode || "HTML", + reply_to_message_id: options?.telegram?.replyToMessageId, + disable_notification: options?.telegram?.disableNotification, + }, + ); } } else { // Multiple media - use media group @@ -219,7 +231,9 @@ export const telegramProvider: SocialMediaProvider = { } // postId should be in format "chatId/messageId" - const [chatId, messageId] = postId.includes("/") ? postId.split("/") : [null, postId]; + const [chatId, messageId] = postId.includes("/") + ? postId.split("/") + : [null, postId]; if (!chatId) { return { diff --git a/packages/lib/services/social-media/providers/tiktok.ts b/packages/lib/services/social-media/providers/tiktok.ts index 0646bdd49..295e1c44f 100644 --- a/packages/lib/services/social-media/providers/tiktok.ts +++ b/packages/lib/services/social-media/providers/tiktok.ts @@ -49,7 +49,9 @@ async function tiktokApiRequest( accessToken: string, options: RequestInit = {}, ): Promise { - const url = endpoint.startsWith("http") ? endpoint : `${TIKTOK_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${TIKTOK_API_BASE}${endpoint}`; const { data } = await withRetry<{ data: T; @@ -67,7 +69,9 @@ async function tiktokApiRequest( async (response) => { const json = await response.json(); if (json.error?.code && json.error.code !== "ok") { - throw new Error(json.error.message || `TikTok error: ${json.error.code}`); + throw new Error( + json.error.message || `TikTok error: ${json.error.code}`, + ); } return json; }, @@ -175,12 +179,14 @@ export const tiktokProvider: SocialMediaProvider = { }; if (options?.tiktok?.videoCoverTimestampMs) { - postInfo.video_cover_timestamp_ms = options.tiktok.videoCoverTimestampMs; + postInfo.video_cover_timestamp_ms = + options.tiktok.videoCoverTimestampMs; } if (options?.tiktok?.brandContentToggle) { postInfo.brand_content_toggle = true; - postInfo.brand_organic_toggle = options.tiktok.brandOrganicToggle || false; + postInfo.brand_organic_toggle = + options.tiktok.brandOrganicToggle || false; } // Initialize upload from URL (pull method) @@ -201,7 +207,10 @@ export const tiktokProvider: SocialMediaProvider = { ); // Wait for publish to complete - const status = await waitForPublish(credentials.accessToken, initResponse.publish_id); + const status = await waitForPublish( + credentials.accessToken, + initResponse.publish_id, + ); const postId = status.publicaly_available_post_id?.[0]; @@ -209,7 +218,9 @@ export const tiktokProvider: SocialMediaProvider = { platform: "tiktok", success: true, postId: postId || initResponse.publish_id, - postUrl: postId ? `https://www.tiktok.com/@me/video/${postId}` : undefined, + postUrl: postId + ? `https://www.tiktok.com/@me/video/${postId}` + : undefined, metadata: { publishId: initResponse.publish_id }, }; } @@ -231,7 +242,9 @@ export const tiktokProvider: SocialMediaProvider = { source: "FILE_UPLOAD", video_size: videoData.length, chunk_size: 10 * 1024 * 1024, // 10MB chunks - total_chunk_count: Math.ceil(videoData.length / (10 * 1024 * 1024)), + total_chunk_count: Math.ceil( + videoData.length / (10 * 1024 * 1024), + ), }, }), }, @@ -256,7 +269,10 @@ export const tiktokProvider: SocialMediaProvider = { } // Wait for publish to complete - const status = await waitForPublish(credentials.accessToken, initResponse.publish_id); + const status = await waitForPublish( + credentials.accessToken, + initResponse.publish_id, + ); const postId = status.publicaly_available_post_id?.[0]; @@ -264,7 +280,9 @@ export const tiktokProvider: SocialMediaProvider = { platform: "tiktok", success: true, postId: postId || initResponse.publish_id, - postUrl: postId ? `https://www.tiktok.com/@me/video/${postId}` : undefined, + postUrl: postId + ? `https://www.tiktok.com/@me/video/${postId}` + : undefined, }; } @@ -328,7 +346,9 @@ export const tiktokProvider: SocialMediaProvider = { } }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if (!credentials.accessToken) { return null; } diff --git a/packages/lib/services/social-media/providers/twitter.ts b/packages/lib/services/social-media/providers/twitter.ts index 6d7e5466c..2c599314a 100644 --- a/packages/lib/services/social-media/providers/twitter.ts +++ b/packages/lib/services/social-media/providers/twitter.ts @@ -19,7 +19,9 @@ async function twitterApiRequest( accessToken: string, options: RequestInit = {}, ): Promise { - const url = endpoint.startsWith("http") ? endpoint : `${TWITTER_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${TWITTER_API_BASE}${endpoint}`; const { data } = await withRetry( () => fetch(url, { @@ -36,7 +38,10 @@ async function twitterApiRequest( return data; } -async function uploadMedia(accessToken: string, media: MediaAttachment): Promise { +async function uploadMedia( + accessToken: string, + media: MediaAttachment, +): Promise { let mediaData: Buffer; if (media.data) { mediaData = media.data; @@ -81,10 +86,13 @@ async function uploadMedia(accessToken: string, media: MediaAttachment): Promise media_category: mediaType, }); - const initResponse = await fetch(`${TWITTER_UPLOAD_BASE}/media/upload.json?${initParams}`, { - method: "POST", - headers: { Authorization: `Bearer ${accessToken}` }, - }); + const initResponse = await fetch( + `${TWITTER_UPLOAD_BASE}/media/upload.json?${initParams}`, + { + method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ); if (!initResponse.ok) { throw new Error("Media upload INIT failed"); @@ -107,11 +115,14 @@ async function uploadMedia(accessToken: string, media: MediaAttachment): Promise const chunkBytes = Uint8Array.from(chunk); formData.append("media", new Blob([chunkBytes])); - const appendResponse = await fetch(`${TWITTER_UPLOAD_BASE}/media/upload.json?${appendParams}`, { - method: "POST", - headers: { Authorization: `Bearer ${accessToken}` }, - body: formData, - }); + const appendResponse = await fetch( + `${TWITTER_UPLOAD_BASE}/media/upload.json?${appendParams}`, + { + method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, + body: formData, + }, + ); if (!appendResponse.ok) { throw new Error(`Media upload APPEND failed at segment ${segmentIndex}`); @@ -158,9 +169,12 @@ async function waitForProcessing( media_id: mediaId, }); - const response = await fetch(`${TWITTER_UPLOAD_BASE}/media/upload.json?${statusParams}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); + const response = await fetch( + `${TWITTER_UPLOAD_BASE}/media/upload.json?${statusParams}`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ); const data = await response.json(); @@ -239,9 +253,12 @@ export const twitterProvider: SocialMediaProvider = { payload.media = { media_ids: mediaIds }; } - if (content.replyToId) payload.reply = { in_reply_to_tweet_id: content.replyToId }; - if (options?.twitter?.quoteTweetId) payload.quote_tweet_id = options.twitter.quoteTweetId; - if (options?.twitter?.replySettings) payload.reply_settings = options.twitter.replySettings; + if (content.replyToId) + payload.reply = { in_reply_to_tweet_id: content.replyToId }; + if (options?.twitter?.quoteTweetId) + payload.quote_tweet_id = options.twitter.quoteTweetId; + if (options?.twitter?.replySettings) + payload.reply_settings = options.twitter.replySettings; if (options?.twitter?.pollOptions?.length) { payload.poll = { options: options.twitter.pollOptions.map((opt) => ({ label: opt })), @@ -314,7 +331,10 @@ export const twitterProvider: SocialMediaProvider = { impression_count?: number; }; }; - }>(`/tweets/${postId}?tweet.fields=public_metrics`, credentials.accessToken); + }>( + `/tweets/${postId}?tweet.fields=public_metrics`, + credentials.accessToken, + ); const metrics = response.data.public_metrics; @@ -335,7 +355,9 @@ export const twitterProvider: SocialMediaProvider = { } }, - async getAccountAnalytics(credentials: SocialCredentials): Promise { + async getAccountAnalytics( + credentials: SocialCredentials, + ): Promise { if (!credentials.accessToken) { return null; } @@ -384,28 +406,40 @@ export const twitterProvider: SocialMediaProvider = { content: PostContent, options?: PlatformPostOptions, ): Promise { - return this.createPost(credentials, { ...content, replyToId: postId }, options); + return this.createPost( + credentials, + { ...content, replyToId: postId }, + options, + ); }, async likePost(credentials: SocialCredentials, postId: string) { - if (!credentials.accessToken) return { success: false, error: "Access token required" }; + if (!credentials.accessToken) + return { success: false, error: "Access token required" }; try { const userResponse = await twitterApiRequest<{ data: { id: string } }>( "/users/me", credentials.accessToken, ); - await twitterApiRequest(`/users/${userResponse.data.id}/likes`, credentials.accessToken, { - method: "POST", - body: JSON.stringify({ tweet_id: postId }), - }); + await twitterApiRequest( + `/users/${userResponse.data.id}/likes`, + credentials.accessToken, + { + method: "POST", + body: JSON.stringify({ tweet_id: postId }), + }, + ); return { success: true }; } catch (error) { return { success: false, error: extractErrorMessage(error) }; } }, - async repost(credentials: SocialCredentials, postId: string): Promise { + async repost( + credentials: SocialCredentials, + postId: string, + ): Promise { if (!credentials.accessToken) return { platform: "twitter", @@ -418,10 +452,14 @@ export const twitterProvider: SocialMediaProvider = { "/users/me", credentials.accessToken, ); - await twitterApiRequest(`/users/${userResponse.data.id}/retweets`, credentials.accessToken, { - method: "POST", - body: JSON.stringify({ tweet_id: postId }), - }); + await twitterApiRequest( + `/users/${userResponse.data.id}/retweets`, + credentials.accessToken, + { + method: "POST", + body: JSON.stringify({ tweet_id: postId }), + }, + ); return { platform: "twitter", success: true, postId }; } catch (error) { return { diff --git a/packages/lib/services/social-media/rate-limit.ts b/packages/lib/services/social-media/rate-limit.ts index 4232d0575..387ffa9ac 100644 --- a/packages/lib/services/social-media/rate-limit.ts +++ b/packages/lib/services/social-media/rate-limit.ts @@ -42,7 +42,9 @@ function parseRetryAfter(response: Response): number | undefined { const seconds = parseInt(header, 10); if (!isNaN(seconds)) return seconds * 1000; const date = new Date(header); - return isNaN(date.getTime()) ? undefined : Math.max(0, date.getTime() - Date.now()); + return isNaN(date.getTime()) + ? undefined + : Math.max(0, date.getTime() - Date.now()); } export function isRateLimitResponse(response: Response): boolean { @@ -83,12 +85,17 @@ export async function withRetry( await sleep(waitMs); continue; } - throw createRateLimitError(platform, retryAfter ? retryAfter / 1000 : undefined); + throw createRateLimitError( + platform, + retryAfter ? retryAfter / 1000 : undefined, + ); } if (!response.ok) { const errorBody = await response.text().catch(() => ""); - throw new Error(`${platform} API error ${response.status}: ${errorBody}`); + throw new Error( + `${platform} API error ${response.status}: ${errorBody}`, + ); } return { data: await parser(response) }; @@ -98,13 +105,18 @@ export async function withRetry( if (attempt < maxRetries) { const delayMs = baseDelayMs * 2 ** attempt; - logger.warn(`[${platform}] Request failed, retrying in ${delayMs}ms: ${lastError.message}`); + logger.warn( + `[${platform}] Request failed, retrying in ${delayMs}ms: ${lastError.message}`, + ); await sleep(delayMs); } } } - throw lastError || new Error(`${platform} request failed after ${maxRetries} retries`); + throw ( + lastError || + new Error(`${platform} request failed after ${maxRetries} retries`) + ); } export function getRateLimitConfig(platform: SocialPlatform) { diff --git a/packages/lib/services/social-media/token-refresh.ts b/packages/lib/services/social-media/token-refresh.ts index aa95d2687..4286453cc 100644 --- a/packages/lib/services/social-media/token-refresh.ts +++ b/packages/lib/services/social-media/token-refresh.ts @@ -1,4 +1,7 @@ -import type { SocialCredentials, SocialPlatform } from "@/lib/types/social-media"; +import type { + SocialCredentials, + SocialPlatform, +} from "@/lib/types/social-media"; import { safeJsonParse } from "@/lib/utils/json-parsing"; import { logger } from "@/lib/utils/logger"; @@ -12,7 +15,9 @@ interface RefreshResult { export function isTokenExpired(credentials: SocialCredentials): boolean { if (!credentials.tokenExpiresAt) return false; - return credentials.tokenExpiresAt.getTime() - TOKEN_EXPIRY_BUFFER_MS < Date.now(); + return ( + credentials.tokenExpiresAt.getTime() - TOKEN_EXPIRY_BUFFER_MS < Date.now() + ); } export function needsRefresh(credentials: SocialCredentials): boolean { @@ -21,7 +26,9 @@ export function needsRefresh(credentials: SocialCredentials): boolean { return isTokenExpired(credentials); } -async function refreshTwitterToken(refreshToken: string): Promise { +async function refreshTwitterToken( + refreshToken: string, +): Promise { const clientId = process.env.TWITTER_CLIENT_ID; const clientSecret = process.env.TWITTER_CLIENT_SECRET; @@ -41,7 +48,10 @@ async function refreshTwitterToken(refreshToken: string): Promise if (!response.ok) { const error = await safeJsonParse<{ error_description?: string }>(response); - throw new Error(error.error_description || `Twitter token refresh failed: ${response.status}`); + throw new Error( + error.error_description || + `Twitter token refresh failed: ${response.status}`, + ); } const data = await response.json(); @@ -56,7 +66,8 @@ async function refreshMetaToken(accessToken: string): Promise { const appId = process.env.META_APP_ID; const appSecret = process.env.META_APP_SECRET; - if (!appId || !appSecret) throw new Error("META_APP_ID or META_APP_SECRET not configured"); + if (!appId || !appSecret) + throw new Error("META_APP_ID or META_APP_SECRET not configured"); const response = await fetch( `https://graph.facebook.com/v19.0/oauth/access_token?` + @@ -69,38 +80,54 @@ async function refreshMetaToken(accessToken: string): Promise { ); if (!response.ok) { - const error = await safeJsonParse<{ error?: { message?: string } }>(response); - throw new Error(error.error?.message || `Meta token refresh failed: ${response.status}`); + const error = await safeJsonParse<{ error?: { message?: string } }>( + response, + ); + throw new Error( + error.error?.message || `Meta token refresh failed: ${response.status}`, + ); } const data = await response.json(); return { accessToken: data.access_token, - expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1000) : undefined, + expiresAt: data.expires_in + ? new Date(Date.now() + data.expires_in * 1000) + : undefined, }; } -async function refreshLinkedInToken(refreshToken: string): Promise { +async function refreshLinkedInToken( + refreshToken: string, +): Promise { const clientId = process.env.LINKEDIN_CLIENT_ID; const clientSecret = process.env.LINKEDIN_CLIENT_SECRET; if (!clientId || !clientSecret) - throw new Error("LINKEDIN_CLIENT_ID or LINKEDIN_CLIENT_SECRET not configured"); + throw new Error( + "LINKEDIN_CLIENT_ID or LINKEDIN_CLIENT_SECRET not configured", + ); - const response = await fetch("https://www.linkedin.com/oauth/v2/accessToken", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: clientId, - client_secret: clientSecret, - }), - }); + const response = await fetch( + "https://www.linkedin.com/oauth/v2/accessToken", + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: clientId, + client_secret: clientSecret, + }), + }, + ); if (!response.ok) { const error = await safeJsonParse<{ error_description?: string }>(response); - throw new Error(error.error_description || `LinkedIn token refresh failed: ${response.status}`); + throw new Error( + error.error_description || + `LinkedIn token refresh failed: ${response.status}`, + ); } const data = await response.json(); @@ -111,7 +138,9 @@ async function refreshLinkedInToken(refreshToken: string): Promise { +async function refreshTikTokToken( + refreshToken: string, +): Promise { const clientKey = process.env.TIKTOK_CLIENT_KEY; const clientSecret = process.env.TIKTOK_CLIENT_SECRET; @@ -131,7 +160,10 @@ async function refreshTikTokToken(refreshToken: string): Promise if (!response.ok) { const error = await safeJsonParse<{ error_description?: string }>(response); - throw new Error(error.error_description || `TikTok token refresh failed: ${response.status}`); + throw new Error( + error.error_description || + `TikTok token refresh failed: ${response.status}`, + ); } const data = await response.json(); @@ -150,14 +182,22 @@ export async function refreshToken( switch (platform) { case "twitter": - return credentials.refreshToken ? refreshTwitterToken(credentials.refreshToken) : null; + return credentials.refreshToken + ? refreshTwitterToken(credentials.refreshToken) + : null; case "facebook": case "instagram": - return credentials.accessToken ? refreshMetaToken(credentials.accessToken) : null; + return credentials.accessToken + ? refreshMetaToken(credentials.accessToken) + : null; case "linkedin": - return credentials.refreshToken ? refreshLinkedInToken(credentials.refreshToken) : null; + return credentials.refreshToken + ? refreshLinkedInToken(credentials.refreshToken) + : null; case "tiktok": - return credentials.refreshToken ? refreshTikTokToken(credentials.refreshToken) : null; + return credentials.refreshToken + ? refreshTikTokToken(credentials.refreshToken) + : null; default: return null; } @@ -165,17 +205,26 @@ export async function refreshToken( export function getRefreshGuidance(platform: SocialPlatform): string { const guides: Record = { - twitter: "Re-authenticate your Twitter account at /settings/connections/twitter", - bluesky: "Update your Bluesky app password at /settings/connections/bluesky", + twitter: + "Re-authenticate your Twitter account at /settings/connections/twitter", + bluesky: + "Update your Bluesky app password at /settings/connections/bluesky", discord: "Check your Discord bot token at /settings/connections/discord", - telegram: "Verify your Telegram bot token at /settings/connections/telegram", - slack: "Re-install your Slack app or update bot token at /settings/connections/slack", + telegram: + "Verify your Telegram bot token at /settings/connections/telegram", + slack: + "Re-install your Slack app or update bot token at /settings/connections/slack", reddit: "Check your Reddit app credentials at /settings/connections/reddit", - facebook: "Re-authenticate your Facebook page at /settings/connections/facebook", - instagram: "Re-authenticate your Instagram account at /settings/connections/instagram", - tiktok: "Re-authenticate your TikTok account at /settings/connections/tiktok", - linkedin: "Re-authenticate your LinkedIn account at /settings/connections/linkedin", - mastodon: "Update your Mastodon access token at /settings/connections/mastodon", + facebook: + "Re-authenticate your Facebook page at /settings/connections/facebook", + instagram: + "Re-authenticate your Instagram account at /settings/connections/instagram", + tiktok: + "Re-authenticate your TikTok account at /settings/connections/tiktok", + linkedin: + "Re-authenticate your LinkedIn account at /settings/connections/linkedin", + mastodon: + "Update your Mastodon access token at /settings/connections/mastodon", }; return guides[platform]; } diff --git a/packages/lib/services/steward-client.ts b/packages/lib/services/steward-client.ts index 99bbbacf1..a05be9484 100644 --- a/packages/lib/services/steward-client.ts +++ b/packages/lib/services/steward-client.ts @@ -95,7 +95,10 @@ function stewardHeaders(): Record { return headers; } -async function stewardFetch(path: string, options?: RequestInit): Promise { +async function stewardFetch( + path: string, + options?: RequestInit, +): Promise { const url = `${STEWARD_HOST_URL}${path}`; try { const response = await fetch(url, { @@ -126,7 +129,9 @@ async function stewardFetch(path: string, options?: RequestInit): Promise { +export async function getStewardAgent( + agentId: string, +): Promise { const data = await stewardFetch<{ id?: string; name?: string; @@ -150,7 +155,9 @@ export async function getStewardAgent(agentId: string): Promise { +export async function getStewardWalletInfo( + agentId: string, +): Promise { // Use the SDK client for balance, since it handles auth + parsing const client = getStewardClient(); @@ -177,7 +184,9 @@ export async function getStewardWalletInfo(agentId: string): Promise null)) as StewardPlatformUserResponse | null; + const payload = (await response + .json() + .catch(() => null)) as StewardPlatformUserResponse | null; if (!response.ok || !payload?.ok) { const errorMessage = diff --git a/packages/lib/services/steward-user-migration.ts b/packages/lib/services/steward-user-migration.ts index 3261fecff..312e09de0 100644 --- a/packages/lib/services/steward-user-migration.ts +++ b/packages/lib/services/steward-user-migration.ts @@ -8,7 +8,12 @@ import { logger } from "@/lib/utils/logger"; type StewardMappingUser = Pick< User, - "id" | "email" | "email_verified" | "name" | "steward_user_id" | "is_anonymous" + | "id" + | "email" + | "email_verified" + | "name" + | "steward_user_id" + | "is_anonymous" >; export interface EnsureStewardUserMappingOptions { @@ -125,11 +130,14 @@ export async function backfillStewardUserMappings( } } catch (error) { failed += 1; - logger.error("[StewardUserMigration] Failed to backfill Steward user mapping", { - userId: user.id, - email: user.email, - error: error instanceof Error ? error.message : String(error), - }); + logger.error( + "[StewardUserMigration] Failed to backfill Steward user mapping", + { + userId: user.id, + email: user.email, + error: error instanceof Error ? error.message : String(error), + }, + ); } } } diff --git a/packages/lib/services/telegram-automation/app-automation.ts b/packages/lib/services/telegram-automation/app-automation.ts index 6028180c1..31890c70e 100644 --- a/packages/lib/services/telegram-automation/app-automation.ts +++ b/packages/lib/services/telegram-automation/app-automation.ts @@ -63,7 +63,10 @@ class TelegramAppAutomationService { /** * Get app for organization, checking ownership. */ - private async getAppForOrg(organizationId: string, appId: string): Promise { + private async getAppForOrg( + organizationId: string, + appId: string, + ): Promise { const app = await appsRepository.findById(appId); if (!app || app.organization_id !== organizationId) { throw new Error("App not found"); @@ -81,9 +84,12 @@ class TelegramAppAutomationService { ): Promise { const app = await this.getAppForOrg(organizationId, appId); - const isConnected = await telegramAutomationService.isConfigured(organizationId); + const isConnected = + await telegramAutomationService.isConfigured(organizationId); if (!isConnected) { - throw new Error("Telegram bot not connected. Connect a bot in Settings first."); + throw new Error( + "Telegram bot not connected. Connect a bot in Settings first.", + ); } const currentConfig = getTelegramConfigWithDefaults( @@ -148,7 +154,8 @@ class TelegramAppAutomationService { appId: string, ): Promise { const app = await this.getAppForOrg(organizationId, appId); - const connectionStatus = await telegramAutomationService.getConnectionStatus(organizationId); + const connectionStatus = + await telegramAutomationService.getConnectionStatus(organizationId); const config = (app.telegram_automation || { enabled: false, @@ -174,7 +181,10 @@ class TelegramAppAutomationService { }; } - async generateAnnouncement(organizationId: string, app: App): Promise { + async generateAnnouncement( + organizationId: string, + app: App, + ): Promise { const deduction = await creditsService.deductCredits({ organizationId, amount: TELEGRAM_POST_COST, @@ -193,7 +203,9 @@ class TelegramAppAutomationService { let characterPrompt = ""; if (config?.agentCharacterId) { - const characterContext = await getCharacterPromptContext(config.agentCharacterId); + const characterContext = await getCharacterPromptContext( + config.agentCharacterId, + ); if (characterContext) { characterPrompt = buildCharacterSystemPrompt(characterContext); logger.info("[TelegramAppAutomation] Using character voice", { @@ -202,15 +214,21 @@ class TelegramAppAutomationService { characterName: characterContext.name, }); } else { - logger.warn("[TelegramAppAutomation] Character not found, using default", { - appId: app.id, - characterId: config.agentCharacterId, - }); + logger.warn( + "[TelegramAppAutomation] Character not found, using default", + { + appId: app.id, + characterId: config.agentCharacterId, + }, + ); } } else { - logger.info("[TelegramAppAutomation] No character selected, using default voice", { - appId: app.id, - }); + logger.info( + "[TelegramAppAutomation] No character selected, using default voice", + { + appId: app.id, + }, + ); } const systemPrompt = characterPrompt @@ -283,7 +301,9 @@ Maximum 500 characters.`; let characterPrompt = ""; if (config?.agentCharacterId) { - const characterContext = await getCharacterPromptContext(config.agentCharacterId); + const characterContext = await getCharacterPromptContext( + config.agentCharacterId, + ); if (characterContext) { characterPrompt = buildCharacterSystemPrompt(characterContext); } @@ -380,9 +400,11 @@ Maximum 300 characters.`; return { success: false, error: "No channel or group configured" }; } - const messageText = text || (await this.generateAnnouncement(organizationId, app)); + const messageText = + text || (await this.generateAnnouncement(organizationId, app)); - const botToken = await telegramAutomationService.getBotToken(organizationId); + const botToken = + await telegramAutomationService.getBotToken(organizationId); if (!botToken) { return { success: false, error: "Bot not connected" }; } @@ -401,7 +423,9 @@ Maximum 300 characters.`; if (promotionalImageUrl) { // Telegram photo captions are limited to 1024 characters const caption = - messageText.length > 1024 ? messageText.substring(0, 1021) + "..." : messageText; + messageText.length > 1024 + ? messageText.substring(0, 1021) + "..." + : messageText; const replyMarkup = buttonUrl ? createInlineKeyboard([{ text: "🚀 Try It Now", url: buttonUrl }]) @@ -428,13 +452,18 @@ Maximum 300 characters.`; }); } else { // No image - send text message as before - const chunks = splitMessage(messageText, TELEGRAM_RATE_LIMITS.MAX_MESSAGE_LENGTH); + const chunks = splitMessage( + messageText, + TELEGRAM_RATE_LIMITS.MAX_MESSAGE_LENGTH, + ); for (const chunk of chunks) { const isLastChunk = chunk === chunks[chunks.length - 1]; const replyMarkup = isLastChunk && buttonUrl - ? createInlineKeyboard([{ text: "🚀 Try It Now", url: buttonUrl }]) + ? createInlineKeyboard([ + { text: "🚀 Try It Now", url: buttonUrl }, + ]) : undefined; const result = await Promise.race([ @@ -443,7 +472,10 @@ Maximum 300 characters.`; reply_markup: replyMarkup, }), new Promise((_, reject) => - setTimeout(() => reject(new Error("Telegram API timeout")), 25_000), + setTimeout( + () => reject(new Error("Telegram API timeout")), + 25_000, + ), ), ]); @@ -451,7 +483,8 @@ Maximum 300 characters.`; } } } catch (error) { - lastError = error instanceof Error ? error.message : "Failed to send message"; + lastError = + error instanceof Error ? error.message : "Failed to send message"; logger.error("[TelegramAppAutomation] Failed to post announcement", { appId, chatId, @@ -511,12 +544,18 @@ Maximum 300 characters.`; return { success: false, error: "Auto-reply not enabled" }; } - const botToken = await telegramAutomationService.getBotToken(organizationId); + const botToken = + await telegramAutomationService.getBotToken(organizationId); if (!botToken) { return { success: false, error: "Bot not connected" }; } - const replyText = await this.generateReply(organizationId, app, message.text, message.userName); + const replyText = await this.generateReply( + organizationId, + app, + message.text, + message.userName, + ); const bot = new Telegraf(botToken); @@ -551,7 +590,8 @@ Maximum 300 characters.`; chatId: message.chatId, }; } catch (error) { - const errorMsg = error instanceof Error ? error.message : "Failed to send reply"; + const errorMsg = + error instanceof Error ? error.message : "Failed to send reply"; logger.error("[TelegramAppAutomation] Failed to handle message", { appId, chatId: message.chatId, @@ -582,14 +622,18 @@ Maximum 300 characters.`; const lastAnnouncement = new Date(config.lastAnnouncementAt); const now = new Date(); - const minutesSince = (now.getTime() - lastAnnouncement.getTime()) / (1000 * 60); + const minutesSince = + (now.getTime() - lastAnnouncement.getTime()) / (1000 * 60); // Use a random interval between min and max for natural timing const minInterval = - config.announceIntervalMin || TELEGRAM_AUTOMATION_DEFAULTS.announceIntervalMin; + config.announceIntervalMin || + TELEGRAM_AUTOMATION_DEFAULTS.announceIntervalMin; const maxInterval = - config.announceIntervalMax || TELEGRAM_AUTOMATION_DEFAULTS.announceIntervalMax; - const targetInterval = minInterval + Math.random() * (maxInterval - minInterval); + config.announceIntervalMax || + TELEGRAM_AUTOMATION_DEFAULTS.announceIntervalMax; + const targetInterval = + minInterval + Math.random() * (maxInterval - minInterval); return minutesSince >= targetInterval; } diff --git a/packages/lib/services/telegram-automation/index.ts b/packages/lib/services/telegram-automation/index.ts index de55cea99..d43d19646 100644 --- a/packages/lib/services/telegram-automation/index.ts +++ b/packages/lib/services/telegram-automation/index.ts @@ -11,7 +11,9 @@ import { logger } from "@/lib/utils/logger"; // Use ELIZA_API_URL (ngrok) for local dev webhooks, otherwise NEXT_PUBLIC_APP_URL const WEBHOOK_BASE_URL = - process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + process.env.ELIZA_API_URL || + process.env.NEXT_PUBLIC_APP_URL || + "https://www.elizacloud.ai"; // Cache TTL for connection status (5 minutes) const STATUS_CACHE_TTL_MS = 5 * 60 * 1000; @@ -167,7 +169,10 @@ class TelegramAutomationService { /** * Remove bot credentials (disconnect). */ - async removeCredentials(organizationId: string, userId: string): Promise { + async removeCredentials( + organizationId: string, + userId: string, + ): Promise { const audit = { actorType: "user" as const, actorId: userId, @@ -264,10 +269,13 @@ class TelegramAutomationService { this.statusCache.set(organizationId, { status, cachedAt: Date.now() }); return status; } catch (error) { - logger.warn("[TelegramAutomation] Token validation failed during status check", { - organizationId, - error: error instanceof Error ? error.message : "Unknown error", - }); + logger.warn( + "[TelegramAutomation] Token validation failed during status check", + { + organizationId, + error: error instanceof Error ? error.message : "Unknown error", + }, + ); // Return stored data even if validation fails (don't cache error state) const status: TelegramConnectionStatus = { @@ -290,7 +298,9 @@ class TelegramAutomationService { * Set webhook for receiving updates from Telegram. * Requires HTTPS URL - use ELIZA_API_URL with ngrok for local development. */ - async setWebhook(organizationId: string): Promise<{ success: boolean; error?: string }> { + async setWebhook( + organizationId: string, + ): Promise<{ success: boolean; error?: string }> { const botToken = await this.getBotToken(organizationId); if (!botToken) { return { success: false, error: "Bot token not found" }; @@ -304,7 +314,8 @@ class TelegramAutomationService { }); return { success: true, // Don't fail the connection, just skip webhook - error: "Webhook skipped - HTTPS required. Set ELIZA_API_URL with ngrok URL for local dev.", + error: + "Webhook skipped - HTTPS required. Set ELIZA_API_URL with ngrok URL for local dev.", }; } @@ -316,7 +327,12 @@ class TelegramAutomationService { const webhookSecret = await this.getWebhookSecret(organizationId); await bot.telegram.setWebhook(webhookUrl, { - allowed_updates: ["message", "callback_query", "channel_post", "my_chat_member"], + allowed_updates: [ + "message", + "callback_query", + "channel_post", + "my_chat_member", + ], drop_pending_updates: true, secret_token: webhookSecret || undefined, }); @@ -378,7 +394,10 @@ class TelegramAutomationService { parseMode?: "MarkdownV2" | "HTML"; replyMarkup?: { inline_keyboard: Array< - Array<{ text: string; url: string } | { text: string; callback_data: string }> + Array< + | { text: string; url: string } + | { text: string; callback_data: string } + > >; }; disableWebPagePreview?: boolean; @@ -393,7 +412,9 @@ class TelegramAutomationService { const result = await bot.telegram.sendMessage(chatId, text, { parse_mode: options?.parseMode, reply_markup: options?.replyMarkup, - link_preview_options: options?.disableWebPagePreview ? { is_disabled: true } : undefined, + link_preview_options: options?.disableWebPagePreview + ? { is_disabled: true } + : undefined, }); return { success: true, messageId: result.message_id }; diff --git a/packages/lib/services/token-redemption-secure.ts b/packages/lib/services/token-redemption-secure.ts index d305838a5..31c1a5607 100644 --- a/packages/lib/services/token-redemption-secure.ts +++ b/packages/lib/services/token-redemption-secure.ts @@ -33,7 +33,10 @@ import { } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { dbRead, dbWrite } from "@/db/client"; -import { redeemableEarnings, redeemableEarningsLedger } from "@/db/schemas/redeemable-earnings"; +import { + redeemableEarnings, + redeemableEarningsLedger, +} from "@/db/schemas/redeemable-earnings"; import { redemptionLimits, type TokenRedemption, @@ -46,9 +49,16 @@ import { getWalletRecommendation, } from "@/lib/config/redemption-addresses"; import { ARBITRAGE_PROTECTION } from "@/lib/config/redemption-security"; -import { ELIZA_DECIMALS, ERC20_ABI, EVM_CHAINS } from "@/lib/config/token-constants"; +import { + ELIZA_DECIMALS, + ERC20_ABI, + EVM_CHAINS, +} from "@/lib/config/token-constants"; import { logger } from "@/lib/utils/logger"; -import { ELIZA_TOKEN_ADDRESSES, type SupportedNetwork } from "./eliza-token-price"; +import { + ELIZA_TOKEN_ADDRESSES, + type SupportedNetwork, +} from "./eliza-token-price"; import { redeemableEarningsService } from "./redeemable-earnings"; import { twapPriceOracle } from "./twap-price-oracle"; @@ -204,7 +214,9 @@ export class SecureTokenRedemptionService { * - Supports idempotency key (Fix #10) * - Atomic balance checks with constraints (Fix #1, #5) */ - async createRedemption(request: SecureRedemptionRequest): Promise { + async createRedemption( + request: SecureRedemptionRequest, + ): Promise { const { userId, appId, @@ -263,7 +275,12 @@ export class SecureTokenRedemptionService { // If signature provided, verify it (for EVM chains) if (signature && network !== "solana" && nonce) { - const sigValid = await this.verifyAddressSignature(payoutAddress, signature, nonce, network); + const sigValid = await this.verifyAddressSignature( + payoutAddress, + signature, + nonce, + network, + ); if (!sigValid) { return { success: false, @@ -273,7 +290,10 @@ export class SecureTokenRedemptionService { } // Fix #12: Validate address with contract + exchange check - const addressValidation = await this.validateAddressSecure(payoutAddress, network); + const addressValidation = await this.validateAddressSecure( + payoutAddress, + network, + ); if (!addressValidation.valid) { // Include wallet recommendation in error return { @@ -284,7 +304,12 @@ export class SecureTokenRedemptionService { } // Check for fraud patterns (fast earn-to-redeem, high ratio, shared address) - const fraudCheck = await this.checkFraudPatterns(userId, appId, pointsAmount, payoutAddress); + const fraudCheck = await this.checkFraudPatterns( + userId, + appId, + pointsAmount, + payoutAddress, + ); if (fraudCheck.flagged) { warnings.push(fraudCheck.warning!); // Continue but flag for admin review @@ -295,7 +320,8 @@ export class SecureTokenRedemptionService { if (existingInFlight) { return { success: false, - error: "You have an in-flight redemption. Please wait for it to complete or be rejected.", + error: + "You have an in-flight redemption. Please wait for it to complete or be rejected.", }; } @@ -313,10 +339,14 @@ export class SecureTokenRedemptionService { // SECURITY: Check IP-based rate limits (anti-sybil protection) if (metadata?.ipAddress) { - const ipCheck = await this.checkIPRateLimits(metadata.ipAddress, pointsAmount); + const ipCheck = await this.checkIPRateLimits( + metadata.ipAddress, + pointsAmount, + ); if (!ipCheck.valid) { logger.warn("[SecureRedemption] IP rate limit exceeded", { - ipAddress: metadata.ipAddress.split(".").slice(0, 2).join(".") + ".x.x", // Partially mask + ipAddress: + metadata.ipAddress.split(".").slice(0, 2).join(".") + ".x.x", // Partially mask reason: ipCheck.error, }); return { success: false, error: ipCheck.error }; @@ -349,7 +379,11 @@ export class SecureTokenRedemptionService { // PRICING PHASE (Fix #8, #14: Use TWAP exclusively) // ======================================== - const quoteResult = await twapPriceOracle.getRedemptionQuote(network, pointsAmount, userId); + const quoteResult = await twapPriceOracle.getRedemptionQuote( + network, + pointsAmount, + userId, + ); if (!quoteResult.success) { return { @@ -370,7 +404,10 @@ export class SecureTokenRedemptionService { // ======================================== // Check hot wallet has enough tokens - const tokenCheck = await this.checkTokenAvailability(network, elizaAmount.toNumber()); + const tokenCheck = await this.checkTokenAvailability( + network, + elizaAmount.toNumber(), + ); if (!tokenCheck.available) { logger.warn("[SecureRedemption] Insufficient hot wallet balance", { @@ -391,7 +428,8 @@ export class SecureTokenRedemptionService { // ATOMIC TRANSACTION PHASE (Fix #1, #5) // ======================================== - const requiresReview = usdValue.toNumber() >= SECURE_CONFIG.ADMIN_APPROVAL_THRESHOLD_USD; + const requiresReview = + usdValue.toNumber() >= SECURE_CONFIG.ADMIN_APPROVAL_THRESHOLD_USD; // Generate idempotency key if not provided const finalIdempotencyKey = idempotencyKey || randomUUID(); @@ -510,7 +548,9 @@ export class SecureTokenRedemptionService { status: requiresReview ? "pending" : "approved", requires_review: requiresReview, metadata: { - user_agent: metadata?.userAgent ? sanitizeForLog(metadata.userAgent) : undefined, + user_agent: metadata?.userAgent + ? sanitizeForLog(metadata.userAgent) + : undefined, ip_address: metadata?.ipAddress, price_source: "twap", idempotency_key: finalIdempotencyKey, @@ -715,7 +755,9 @@ export class SecureTokenRedemptionService { const timeSince = Date.now() - lastRedemption.created_at.getTime(); if (timeSince < SECURE_CONFIG.COOLDOWN_MS) { - const waitSeconds = Math.ceil((SECURE_CONFIG.COOLDOWN_MS - timeSince) / 1000); + const waitSeconds = Math.ceil( + (SECURE_CONFIG.COOLDOWN_MS - timeSince) / 1000, + ); return { valid: false, error: `Please wait ${waitSeconds} seconds before your next redemption.`, @@ -737,7 +779,10 @@ export class SecureTokenRedemptionService { todayUTC.setUTCHours(0, 0, 0, 0); const limits = await dbRead.query.redemptionLimits.findFirst({ - where: and(eq(redemptionLimits.user_id, userId), gte(redemptionLimits.date, todayUTC)), + where: and( + eq(redemptionLimits.user_id, userId), + gte(redemptionLimits.date, todayUTC), + ), }); const usdValue = pointsAmount / 100; @@ -786,7 +831,9 @@ export class SecureTokenRedemptionService { AND status NOT IN ('rejected', 'expired') `); - const hourlyRedemptions = Number((hourlyCount.rows[0] as { count: string })?.count || 0); + const hourlyRedemptions = Number( + (hourlyCount.rows[0] as { count: string })?.count || 0, + ); if (hourlyRedemptions >= IP_RATE_LIMITS.MAX_REDEMPTIONS_PER_IP_HOURLY) { return { @@ -806,8 +853,12 @@ export class SecureTokenRedemptionService { AND status NOT IN ('rejected', 'expired') `); - const dailyRedemptions = Number((dailyStats.rows[0] as { count: string })?.count || 0); - const dailyUsd = Number((dailyStats.rows[0] as { total_usd: string })?.total_usd || 0); + const dailyRedemptions = Number( + (dailyStats.rows[0] as { count: string })?.count || 0, + ); + const dailyUsd = Number( + (dailyStats.rows[0] as { total_usd: string })?.total_usd || 0, + ); if (dailyRedemptions >= IP_RATE_LIMITS.MAX_REDEMPTIONS_PER_IP_DAILY) { return { @@ -856,7 +907,9 @@ export class SecureTokenRedemptionService { // ======================================== // Fix #10: Find by idempotency key // ======================================== - private async findByIdempotencyKey(key: string): Promise { + private async findByIdempotencyKey( + key: string, + ): Promise { const redemption = await dbRead.query.tokenRedemptions.findFirst({ where: sql`${tokenRedemptions.metadata}->>'idempotency_key' = ${key}`, }); @@ -887,7 +940,8 @@ export class SecureTokenRedemptionService { if (!evmAddress) { // Derive from private key (matches payout-processor.ts logic) - const evmKey = process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY; + const evmKey = + process.env.EVM_PAYOUT_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY; if (evmKey) { const formattedKey = evmKey.startsWith("0x") ? (evmKey as `0x${string}`) @@ -956,7 +1010,8 @@ export class SecureTokenRedemptionService { walletAddress: string, requiredAmount: number, ): Promise<{ available: boolean; balance: number; error?: string }> { - const solanaRpc = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; + const solanaRpc = + process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; const { Connection, PublicKey } = require("@solana/web3.js") as typeof import("@solana/web3.js"); const { getAssociatedTokenAddress, getAccount } = @@ -1003,7 +1058,12 @@ export class SecureTokenRedemptionService { const [redemption] = await tx .select() .from(tokenRedemptions) - .where(and(eq(tokenRedemptions.id, redemptionId), eq(tokenRedemptions.status, "pending"))) + .where( + and( + eq(tokenRedemptions.id, redemptionId), + eq(tokenRedemptions.status, "pending"), + ), + ) .for("update"); if (!redemption) { @@ -1093,7 +1153,9 @@ export class SecureTokenRedemptionService { payoutAddress?: string, ): Promise<{ flagged: boolean; warning?: string; requiresReview?: boolean }> { // Check 1: Fast earn-to-redeem (earned within last hour) - const oneHourAgo = new Date(Date.now() - FRAUD_THRESHOLDS.FAST_REDEEM_HOURS * 60 * 60 * 1000); + const oneHourAgo = new Date( + Date.now() - FRAUD_THRESHOLDS.FAST_REDEEM_HOURS * 60 * 60 * 1000, + ); // Look for recent earnings transactions if (appId) { @@ -1105,8 +1167,12 @@ export class SecureTokenRedemptionService { AND created_at >= ${oneHourAgo} `); - const recentCount = Number((recentEarnings.rows[0] as { count: string })?.count || 0); - const recentTotal = Number((recentEarnings.rows[0] as { total: string })?.total || 0); + const recentCount = Number( + (recentEarnings.rows[0] as { count: string })?.count || 0, + ); + const recentTotal = Number( + (recentEarnings.rows[0] as { total: string })?.total || 0, + ); if (recentCount > 0 && recentTotal > (pointsAmount / 100) * 0.5) { return { @@ -1132,8 +1198,12 @@ export class SecureTokenRedemptionService { AND status IN ('completed', 'approved', 'processing') `); - const earned = Number((totalEarned.rows[0] as { total: string })?.total || 0); - const redeemed = Number((totalRedeemed.rows[0] as { total: string })?.total || 0); + const earned = Number( + (totalEarned.rows[0] as { total: string })?.total || 0, + ); + const redeemed = Number( + (totalRedeemed.rows[0] as { total: string })?.total || 0, + ); if (earned > 0) { const redemptionRatio = (redeemed + pointsAmount / 100) / earned; @@ -1177,7 +1247,10 @@ export class SecureTokenRedemptionService { // Other methods (get, list, approve) unchanged // ======================================== - async getRedemption(redemptionId: string, userId?: string): Promise { + async getRedemption( + redemptionId: string, + userId?: string, + ): Promise { const conditions = [eq(tokenRedemptions.id, redemptionId)]; if (userId) { conditions.push(eq(tokenRedemptions.user_id, userId)); @@ -1190,7 +1263,10 @@ export class SecureTokenRedemptionService { return redemption ?? null; } - async listUserRedemptions(userId: string, limit = 20): Promise { + async listUserRedemptions( + userId: string, + limit = 20, + ): Promise { return await dbRead.query.tokenRedemptions.findMany({ where: eq(tokenRedemptions.user_id, userId), orderBy: (redemptions, { desc }) => [desc(redemptions.created_at)], @@ -1212,7 +1288,12 @@ export class SecureTokenRedemptionService { review_notes: notes ? sanitizeForLog(notes) : undefined, updated_at: new Date(), }) - .where(and(eq(tokenRedemptions.id, redemptionId), eq(tokenRedemptions.status, "pending"))) + .where( + and( + eq(tokenRedemptions.id, redemptionId), + eq(tokenRedemptions.status, "pending"), + ), + ) .returning(); if (!updated) { diff --git a/packages/lib/services/topup-handler.ts b/packages/lib/services/topup-handler.ts index f0ebea91e..9c908b112 100644 --- a/packages/lib/services/topup-handler.ts +++ b/packages/lib/services/topup-handler.ts @@ -26,7 +26,12 @@ interface TopupRecipient { */ async function getTopupRecipient( request: NextRequest, - body: { walletAddress?: string; ref?: string; referral_code?: string; appOwnerId?: string }, + body: { + walletAddress?: string; + ref?: string; + referral_code?: string; + appOwnerId?: string; + }, ): Promise { const hasWalletSig = !!request.headers.get("X-Wallet-Address") && @@ -39,12 +44,15 @@ async function getTopupRecipient( return { user: walletUser, organizationId: walletUser.organization_id!, - walletAddress: walletUser.wallet_address ?? request.headers.get("X-Wallet-Address")!, + walletAddress: + walletUser.wallet_address ?? request.headers.get("X-Wallet-Address")!, }; } if (!body?.walletAddress?.trim()) { - throw new Error("walletAddress is required (body or wallet signature headers)"); + throw new Error( + "walletAddress is required (body or wallet signature headers)", + ); } const { user } = await findOrCreateUserByWalletAddress(body.walletAddress, { @@ -75,14 +83,22 @@ export function createTopupHandler(options: CreateTopupHandlerOptions) { return async function handler(req: NextRequest): Promise { const body = (await req.json().catch(() => ({}))) as TopupBody; - if (!body?.walletAddress?.trim() && !req.headers.get("X-Wallet-Signature")) { + if ( + !body?.walletAddress?.trim() && + !req.headers.get("X-Wallet-Signature") + ) { return NextResponse.json( - { error: "walletAddress is required (body or wallet signature headers)" }, + { + error: "walletAddress is required (body or wallet signature headers)", + }, { status: 400 }, ); } if (body?.walletAddress && !isAddress(body.walletAddress)) { - return NextResponse.json({ error: "Valid EVM walletAddress is required" }, { status: 400 }); + return NextResponse.json( + { error: "Valid EVM walletAddress is required" }, + { status: 400 }, + ); } let recipient; @@ -102,14 +118,22 @@ export function createTopupHandler(options: CreateTopupHandlerOptions) { req.nextUrl.searchParams.get("referral_code") || body.ref || body.referral_code; - const appOwnerId = req.nextUrl.searchParams.get("appOwnerId") || body.appOwnerId; + const appOwnerId = + req.nextUrl.searchParams.get("appOwnerId") || body.appOwnerId; if (ref && user) { - const result = await referralsService.applyReferralCode(user.id, organizationId, ref, { - appOwnerId: appOwnerId || undefined, - }); + const result = await referralsService.applyReferralCode( + user.id, + organizationId, + ref, + { + appOwnerId: appOwnerId || undefined, + }, + ); if (result.success) { - logger.info(`[x402] Successfully applied referral code ${ref} to user ${user.id}`); + logger.info( + `[x402] Successfully applied referral code ${ref} to user ${user.id}`, + ); } } @@ -117,15 +141,22 @@ export function createTopupHandler(options: CreateTopupHandlerOptions) { logger.info(`Topped up ${walletAddress} with $${amount} via x402`); if (user) { - const { splits } = await referralsService.calculateRevenueSplits(user.id, amount); + const { splits } = await referralsService.calculateRevenueSplits( + user.id, + amount, + ); if (splits.length > 0) { - logger.info(`[x402] Processing revenue splits for $${amount} purchase by user ${user.id}`); + logger.info( + `[x402] Processing revenue splits for $${amount} purchase by user ${user.id}`, + ); const paymentId = req.headers.get("X-PAYMENT") ?? crypto.randomUUID(); const sourceIdBase = getSourceId(walletAddress, paymentId); for (const split of splits) { if (split.amount <= 0) continue; const source = - split.role === "app_owner" ? "app_owner_revenue_share" : "creator_revenue_share"; + split.role === "app_owner" + ? "app_owner_revenue_share" + : "creator_revenue_share"; await redeemableEarningsService.addEarnings({ userId: split.userId, amount: split.amount, @@ -157,11 +188,16 @@ export function createTopupHandler(options: CreateTopupHandlerOptions) { }; } -const FALLBACK_PAYTO = "0x0000000000000000000000000000000000000001" as `0x${string}`; +const FALLBACK_PAYTO = + "0x0000000000000000000000000000000000000001" as `0x${string}`; export function getPayToAddress(): `0x${string}` { const raw = process.env.X402_RECIPIENT_ADDRESS?.trim(); - if (raw && raw !== "0x0000000000000000000000000000000000000000" && isAddress(raw)) { + if ( + raw && + raw !== "0x0000000000000000000000000000000000000000" && + isAddress(raw) + ) { return raw as `0x${string}`; } return FALLBACK_PAYTO; @@ -193,7 +229,10 @@ export function createTopupRoute(amount: number) { const handler = createTopupHandler({ amount, getSourceId: (walletAddress: string, paymentId: string) => - crypto.createHash("sha256").update(`${walletAddress}-${amount}-${paymentId}`).digest("hex"), + crypto + .createHash("sha256") + .update(`${walletAddress}-${amount}-${paymentId}`) + .digest("hex"), }); const payTo = getPayToAddress(); const wrappedHandler = createWrappedHandler(handler, payTo); diff --git a/packages/lib/services/twap-price-oracle.ts b/packages/lib/services/twap-price-oracle.ts index 9b829b49c..0a158af72 100644 --- a/packages/lib/services/twap-price-oracle.ts +++ b/packages/lib/services/twap-price-oracle.ts @@ -146,7 +146,11 @@ export class TWAPPriceOracle { * Record a new price sample for TWAP calculation. * Should be called periodically by a cron job. */ - async recordPriceSample(network: SupportedNetwork, price: number, source: string): Promise { + async recordPriceSample( + network: SupportedNetwork, + price: number, + source: string, + ): Promise { const now = new Date(); await dbWrite.insert(elizaTokenPrices).values({ @@ -183,7 +187,10 @@ export class TWAPPriceOracle { }) .from(elizaTokenPrices) .where( - and(eq(elizaTokenPrices.network, network), gte(elizaTokenPrices.fetched_at, windowStart)), + and( + eq(elizaTokenPrices.network, network), + gte(elizaTokenPrices.fetched_at, windowStart), + ), ) .orderBy(desc(elizaTokenPrices.fetched_at)); @@ -208,7 +215,8 @@ export class TWAPPriceOracle { // Calculate volatility (standard deviation / mean) const mean = twapPrice; - const variance = prices.reduce((sum, p) => sum + (p - mean) ** 2, 0) / prices.length; + const variance = + prices.reduce((sum, p) => sum + (p - mean) ** 2, 0) / prices.length; const stdDev = Math.sqrt(variance); const volatility = stdDev / mean; @@ -292,7 +300,8 @@ export class TWAPPriceOracle { const elizaAmount = usdValue / twap.twapPrice; // 7. Check if large redemption requires delay - const requiresDelay = usdValue >= SYSTEM_LIMITS.LARGE_REDEMPTION_THRESHOLD_USD; + const requiresDelay = + usdValue >= SYSTEM_LIMITS.LARGE_REDEMPTION_THRESHOLD_USD; const delayUntil = requiresDelay ? new Date(Date.now() + SYSTEM_LIMITS.LARGE_REDEMPTION_DELAY_MS) : undefined; @@ -304,8 +313,10 @@ export class TWAPPriceOracle { } // 8. Check system-wide limits - const hourlyRemaining = SYSTEM_LIMITS.MAX_HOURLY_REDEMPTION_USD - systemHealth.hourlyVolumeUsd; - const dailyRemaining = SYSTEM_LIMITS.MAX_DAILY_REDEMPTION_USD - systemHealth.dailyVolumeUsd; + const hourlyRemaining = + SYSTEM_LIMITS.MAX_HOURLY_REDEMPTION_USD - systemHealth.hourlyVolumeUsd; + const dailyRemaining = + SYSTEM_LIMITS.MAX_DAILY_REDEMPTION_USD - systemHealth.dailyVolumeUsd; if (usdValue > hourlyRemaining) { return { @@ -322,7 +333,9 @@ export class TWAPPriceOracle { } if (hourlyRemaining < SYSTEM_LIMITS.MAX_HOURLY_REDEMPTION_USD * 0.2) { - warnings.push(`Only $${hourlyRemaining.toFixed(0)} remaining in hourly limit`); + warnings.push( + `Only $${hourlyRemaining.toFixed(0)} remaining in hourly limit`, + ); } const expiresAt = new Date(Date.now() + TWAP_CONFIG.QUOTE_VALIDITY_MS); @@ -365,7 +378,9 @@ export class TWAPPriceOracle { const now = new Date(); const hourAgo = new Date(now.getTime() - 60 * 60 * 1000); const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - const velocityWindow = new Date(now.getTime() - SYSTEM_LIMITS.VELOCITY_LIMIT_WINDOW_MS); + const velocityWindow = new Date( + now.getTime() - SYSTEM_LIMITS.VELOCITY_LIMIT_WINDOW_MS, + ); // Get hourly volume from token_redemptions table const hourlyResult = await dbRead.execute(sql` @@ -389,9 +404,15 @@ export class TWAPPriceOracle { AND created_at >= ${velocityWindow} `); - const hourlyVolumeUsd = Number((hourlyResult.rows[0] as { total: string })?.total || 0); - const dailyVolumeUsd = Number((dailyResult.rows[0] as { total: string })?.total || 0); - const recentRedemptionCount = Number((velocityResult.rows[0] as { count: string })?.count || 0); + const hourlyVolumeUsd = Number( + (hourlyResult.rows[0] as { total: string })?.total || 0, + ); + const dailyVolumeUsd = Number( + (dailyResult.rows[0] as { total: string })?.total || 0, + ); + const recentRedemptionCount = Number( + (velocityResult.rows[0] as { count: string })?.count || 0, + ); let canProcessRedemptions = true; let pauseReason: string | undefined; diff --git a/packages/lib/services/twilio-automation/index.ts b/packages/lib/services/twilio-automation/index.ts index 5eb0f6c98..00b83eda5 100644 --- a/packages/lib/services/twilio-automation/index.ts +++ b/packages/lib/services/twilio-automation/index.ts @@ -16,7 +16,9 @@ import { // Use ELIZA_API_URL (ngrok) for local dev webhooks, otherwise NEXT_PUBLIC_APP_URL const WEBHOOK_BASE_URL = - process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + process.env.ELIZA_API_URL || + process.env.NEXT_PUBLIC_APP_URL || + "https://www.elizacloud.ai"; // Cache TTL for connection status (5 minutes) const STATUS_CACHE_TTL_MS = 5 * 60 * 1000; @@ -128,7 +130,12 @@ class TwilioAutomationService { const existingSecrets = await secretsService.list(organizationId); const existingSecret = existingSecrets.find((s) => s.name === name); if (existingSecret) { - await secretsService.rotate(existingSecret.id, organizationId, value, audit); + await secretsService.rotate( + existingSecret.id, + organizationId, + value, + audit, + ); } else { throw err; // Re-throw if we can't find it } @@ -154,14 +161,21 @@ class TwilioAutomationService { /** * Remove Twilio credentials (disconnect). */ - async removeCredentials(organizationId: string, userId: string): Promise { + async removeCredentials( + organizationId: string, + userId: string, + ): Promise { const audit = { actorType: "user" as const, actorId: userId, source: "twilio-automation", }; - const secretNames = ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_PHONE_NUMBER"]; + const secretNames = [ + "TWILIO_ACCOUNT_SID", + "TWILIO_AUTH_TOKEN", + "TWILIO_PHONE_NUMBER", + ]; // Get all secrets once (not inside the loop) for efficiency const existingSecrets = await secretsService.list(organizationId); @@ -177,12 +191,19 @@ class TwilioAutomationService { organizationId, }); } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (message.includes("Secret not found") || message.includes("Failed to delete secret")) { - logger.debug("[TwilioAutomation] Secret already removed during disconnect", { - name, - organizationId, - }); + const message = + error instanceof Error ? error.message : String(error); + if ( + message.includes("Secret not found") || + message.includes("Failed to delete secret") + ) { + logger.debug( + "[TwilioAutomation] Secret already removed during disconnect", + { + name, + organizationId, + }, + ); continue; } throw error; @@ -266,7 +287,8 @@ class TwilioAutomationService { connected: false, configured: true, phoneNumber: phoneNumber || undefined, - error: validation.error || "Credentials may be invalid. Try reconnecting.", + error: + validation.error || "Credentials may be invalid. Try reconnecting.", }; // Cache with shorter TTL for error state (1 minute) this.statusCache.set(organizationId, { diff --git a/packages/lib/services/twitter-automation/app-automation.ts b/packages/lib/services/twitter-automation/app-automation.ts index 25135c896..f42099c84 100644 --- a/packages/lib/services/twitter-automation/app-automation.ts +++ b/packages/lib/services/twitter-automation/app-automation.ts @@ -39,7 +39,10 @@ export interface GeneratedTweet { } class TwitterAppAutomationService { - private async getAppForOrg(organizationId: string, appId: string): Promise { + private async getAppForOrg( + organizationId: string, + appId: string, + ): Promise { const app = await appsRepository.findById(appId); if (!app || app.organization_id !== organizationId) { throw new Error("App not found"); @@ -56,22 +59,28 @@ class TwitterAppAutomationService { const hasTwitter = await this.isTwitterConnected(organizationId); if (!hasTwitter) { - throw new Error("Twitter account must be connected before enabling automation"); + throw new Error( + "Twitter account must be connected before enabling automation", + ); } - const currentConfig = (app.twitter_automation || {}) as TwitterAutomationConfig; + const currentConfig = (app.twitter_automation || + {}) as TwitterAutomationConfig; const updatedConfig: TwitterAutomationConfig = { enabled: true, autoPost: config.autoPost ?? currentConfig.autoPost ?? true, autoReply: config.autoReply ?? currentConfig.autoReply ?? true, autoEngage: config.autoEngage ?? currentConfig.autoEngage ?? false, discovery: config.discovery ?? currentConfig.discovery ?? false, - postIntervalMin: config.postIntervalMin ?? currentConfig.postIntervalMin ?? 90, - postIntervalMax: config.postIntervalMax ?? currentConfig.postIntervalMax ?? 150, + postIntervalMin: + config.postIntervalMin ?? currentConfig.postIntervalMin ?? 90, + postIntervalMax: + config.postIntervalMax ?? currentConfig.postIntervalMax ?? 150, vibeStyle: config.vibeStyle ?? currentConfig.vibeStyle, topics: config.topics ?? currentConfig.topics, totalPosts: currentConfig.totalPosts ?? 0, - agentCharacterId: config.agentCharacterId ?? currentConfig.agentCharacterId, + agentCharacterId: + config.agentCharacterId ?? currentConfig.agentCharacterId, }; const updated = await appsRepository.update(appId, { @@ -94,7 +103,8 @@ class TwitterAppAutomationService { async disableAutomation(organizationId: string, appId: string): Promise { const app = await this.getAppForOrg(organizationId, appId); - const currentConfig = (app.twitter_automation || {}) as TwitterAutomationConfig; + const currentConfig = (app.twitter_automation || + {}) as TwitterAutomationConfig; const updatedConfig: TwitterAutomationConfig = { ...currentConfig, enabled: false, @@ -143,7 +153,11 @@ class TwitterAppAutomationService { async generateAppTweet( organizationId: string, app: App, - type: "promotional" | "engagement" | "educational" | "announcement" = "promotional", + type: + | "promotional" + | "engagement" + | "educational" + | "announcement" = "promotional", ): Promise { const deduction = await creditsService.deductCredits({ organizationId, @@ -164,7 +178,9 @@ class TwitterAppAutomationService { let characterPrompt = ""; if (config?.agentCharacterId) { - const characterContext = await getCharacterPromptContext(config.agentCharacterId); + const characterContext = await getCharacterPromptContext( + config.agentCharacterId, + ); if (characterContext) { characterPrompt = buildCharacterSystemPrompt(characterContext); logger.info("[TwitterAppAutomation] Using character voice", { @@ -173,15 +189,21 @@ class TwitterAppAutomationService { characterName: characterContext.name, }); } else { - logger.warn("[TwitterAppAutomation] Character not found, using default", { - appId: app.id, - characterId: config.agentCharacterId, - }); + logger.warn( + "[TwitterAppAutomation] Character not found, using default", + { + appId: app.id, + characterId: config.agentCharacterId, + }, + ); } } else { - logger.info("[TwitterAppAutomation] No character selected, using default voice", { - appId: app.id, - }); + logger.info( + "[TwitterAppAutomation] No character selected, using default voice", + { + appId: app.id, + }, + ); } const prompt = characterPrompt @@ -284,7 +306,11 @@ Return ONLY the tweet text, nothing else.`; let text = tweetText; if (!text) { - const generated = await this.generateAppTweet(organizationId, app, "promotional"); + const generated = await this.generateAppTweet( + organizationId, + app, + "promotional", + ); text = generated.text; } @@ -293,7 +319,10 @@ Return ONLY the tweet text, nothing else.`; tweetResult = await Promise.race([ client.v2.tweet(text), new Promise((_, reject) => - setTimeout(() => reject(new Error("Twitter API timeout")), TWITTER_API_TIMEOUT_MS), + setTimeout( + () => reject(new Error("Twitter API timeout")), + TWITTER_API_TIMEOUT_MS, + ), ), ]); } catch (error) { @@ -303,7 +332,8 @@ Return ONLY the tweet text, nothing else.`; }); return { success: false, - error: error instanceof Error ? error.message : "Failed to post to Twitter", + error: + error instanceof Error ? error.message : "Failed to post to Twitter", }; } @@ -329,11 +359,16 @@ Return ONLY the tweet text, nothing else.`; } private async isTwitterConnected(organizationId: string): Promise { - const accessToken = await secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN"); + const accessToken = await secretsService.get( + organizationId, + "TWITTER_ACCESS_TOKEN", + ); return !!accessToken; } - private async getTwitterClient(organizationId: string): Promise { + private async getTwitterClient( + organizationId: string, + ): Promise { const [accessToken, accessTokenSecret] = await Promise.all([ secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN"), secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN_SECRET"), diff --git a/packages/lib/services/twitter-automation/index.ts b/packages/lib/services/twitter-automation/index.ts index a530ef546..d38364765 100644 --- a/packages/lib/services/twitter-automation/index.ts +++ b/packages/lib/services/twitter-automation/index.ts @@ -195,7 +195,10 @@ class TwitterAutomationService { /** * Remove Twitter credentials (disconnect) */ - async removeCredentials(organizationId: string, userId: string): Promise { + async removeCredentials( + organizationId: string, + userId: string, + ): Promise { const audit = { actorType: "user" as const, actorId: userId, @@ -223,13 +226,16 @@ class TwitterAutomationService { /** * Check if Twitter is connected for an organization */ - async getConnectionStatus(organizationId: string): Promise { - const [accessToken, accessSecret, username, twitterUserId] = await Promise.all([ - secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN"), - secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN_SECRET"), - secretsService.get(organizationId, "TWITTER_USERNAME"), - secretsService.get(organizationId, "TWITTER_USER_ID"), - ]); + async getConnectionStatus( + organizationId: string, + ): Promise { + const [accessToken, accessSecret, username, twitterUserId] = + await Promise.all([ + secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN"), + secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN_SECRET"), + secretsService.get(organizationId, "TWITTER_USERNAME"), + secretsService.get(organizationId, "TWITTER_USER_ID"), + ]); if (!accessToken || !accessSecret) { return { connected: false }; @@ -274,7 +280,9 @@ class TwitterAutomationService { * Get credentials for injecting into character settings * Used by agent-loader when Twitter is enabled */ - async getCredentialsForAgent(organizationId: string): Promise | null> { + async getCredentialsForAgent( + organizationId: string, + ): Promise | null> { const [accessToken, accessSecret] = await Promise.all([ secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN"), secretsService.get(organizationId, "TWITTER_ACCESS_TOKEN_SECRET"), diff --git a/packages/lib/services/usage-quotas.ts b/packages/lib/services/usage-quotas.ts index 5a1f90d9f..7d51ff99c 100644 --- a/packages/lib/services/usage-quotas.ts +++ b/packages/lib/services/usage-quotas.ts @@ -39,7 +39,9 @@ class UsageQuotasService { return await usageQuotasRepository.findByOrganization(organizationId); } - async getActiveQuotasByOrganization(organizationId: string): Promise { + async getActiveQuotasByOrganization( + organizationId: string, + ): Promise { return await usageQuotasRepository.findActiveByOrganization(organizationId); } @@ -146,7 +148,11 @@ class UsageQuotasService { }; } - async trackUsage(organizationId: string, amount: number, modelName?: string): Promise { + async trackUsage( + organizationId: string, + amount: number, + modelName?: string, + ): Promise { if (modelName) { const modelQuota = await usageQuotasRepository.findByOrganizationAndType( organizationId, @@ -172,9 +178,13 @@ class UsageQuotasService { async getCurrentUsage(organizationId: string): Promise<{ global: { used: number; limit: number | null; periodEnd: string | null }; - modelSpecific: Record; + modelSpecific: Record< + string, + { used: number; limit: number; periodEnd: string } + >; }> { - const quotas = await usageQuotasRepository.findActiveByOrganization(organizationId); + const quotas = + await usageQuotasRepository.findActiveByOrganization(organizationId); const result = { global: { @@ -182,7 +192,10 @@ class UsageQuotasService { limit: null as number | null, periodEnd: null as string | null, }, - modelSpecific: {} as Record, + modelSpecific: {} as Record< + string, + { used: number; limit: number; periodEnd: string } + >, }; for (const quota of quotas) { diff --git a/packages/lib/services/usage.ts b/packages/lib/services/usage.ts index 41b0c1a71..a1d693cf6 100644 --- a/packages/lib/services/usage.ts +++ b/packages/lib/services/usage.ts @@ -17,8 +17,14 @@ export class UsageService { return await usageRecordsRepository.findById(id); } - async listByOrganization(organizationId: string, limit?: number): Promise { - return await usageRecordsRepository.listByOrganization(organizationId, limit); + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { + return await usageRecordsRepository.listByOrganization( + organizationId, + limit, + ); } async listByOrganizationAndDateRange( @@ -38,7 +44,11 @@ export class UsageService { startDate?: Date, endDate?: Date, ): Promise { - return await usageRecordsRepository.getStatsByOrganization(organizationId, startDate, endDate); + return await usageRecordsRepository.getStatsByOrganization( + organizationId, + startDate, + endDate, + ); } async create(data: NewUsageRecord): Promise { @@ -61,7 +71,11 @@ export class UsageService { totalCost: number; }> > { - return await usageRecordsRepository.getByModel(organizationId, startDate, endDate); + return await usageRecordsRepository.getByModel( + organizationId, + startDate, + endDate, + ); } /** diff --git a/packages/lib/services/user-database.ts b/packages/lib/services/user-database.ts index f60f04a34..c6e876d25 100644 --- a/packages/lib/services/user-database.ts +++ b/packages/lib/services/user-database.ts @@ -94,7 +94,9 @@ export class UserDatabaseService { // Already has a database? if (app.user_database_status === "ready" && app.user_database_uri) { logger.info("App already has database", { appId }); - const decryptedUri = await fieldEncryption.decryptIfNeeded(app.user_database_uri); + const decryptedUri = await fieldEncryption.decryptIfNeeded( + app.user_database_uri, + ); return { success: true, connectionUri: decryptedUri || undefined, @@ -106,17 +108,25 @@ export class UserDatabaseService { // Atomically try to set status to "provisioning" // This prevents race conditions - only one request can win - const updatedApp = await appsRepository.trySetDatabaseProvisioning(appId, region); + const updatedApp = await appsRepository.trySetDatabaseProvisioning( + appId, + region, + ); if (!updatedApp) { // Another request won the race, or status was already "provisioning" or "ready" // Re-fetch to get current state const currentApp = await appsRepository.findById(appId); - if (currentApp?.user_database_status === "ready" && currentApp.user_database_uri) { + if ( + currentApp?.user_database_status === "ready" && + currentApp.user_database_uri + ) { logger.info("Database was provisioned by concurrent request", { appId, }); - const decryptedUri = await fieldEncryption.decryptIfNeeded(currentApp.user_database_uri); + const decryptedUri = await fieldEncryption.decryptIfNeeded( + currentApp.user_database_uri, + ); return { success: true, connectionUri: decryptedUri || undefined, @@ -146,7 +156,10 @@ export class UserDatabaseService { } // Encrypt the connection URI before storing - const encryptedUri = await fieldEncryption.encrypt(app.organization_id, sharedDbUrl); + const encryptedUri = await fieldEncryption.encrypt( + app.organization_id, + sharedDbUrl, + ); await appsRepository.update(appId, { user_database_uri: encryptedUri, @@ -162,7 +175,8 @@ export class UserDatabaseService { region, }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("Database provisioning failed", { appId, @@ -222,7 +236,11 @@ export class UserDatabaseService { async getConnectionUri(appId: string): Promise { const app = await appsRepository.findById(appId); - if (!app || app.user_database_status !== "ready" || !app.user_database_uri) { + if ( + !app || + app.user_database_status !== "ready" || + !app.user_database_uri + ) { return null; } @@ -255,7 +273,8 @@ export class UserDatabaseService { if (includeUri && app.user_database_uri) { status.connectionUri = - (await fieldEncryption.decryptIfNeeded(app.user_database_uri)) || undefined; + (await fieldEncryption.decryptIfNeeded(app.user_database_uri)) || + undefined; } return status; @@ -287,7 +306,9 @@ export class UserDatabaseService { // Only retry if in error state if (app.user_database_status !== "error") { if (app.user_database_status === "ready") { - const decryptedUri = await fieldEncryption.decryptIfNeeded(app.user_database_uri); + const decryptedUri = await fieldEncryption.decryptIfNeeded( + app.user_database_uri, + ); return { success: true, connectionUri: decryptedUri || undefined, @@ -330,7 +351,9 @@ export const userDatabaseService = new UserDatabaseService(); * @param app - The app object with user_database_uri field * @returns Decrypted connection URI or null if no database */ -export async function getDecryptedDatabaseUri(app: App): Promise { +export async function getDecryptedDatabaseUri( + app: App, +): Promise { if (!app.user_database_uri) { return null; } diff --git a/packages/lib/services/user-mcps.ts b/packages/lib/services/user-mcps.ts index cc8022a57..c89ec2f36 100644 --- a/packages/lib/services/user-mcps.ts +++ b/packages/lib/services/user-mcps.ts @@ -6,7 +6,11 @@ */ import crypto from "crypto"; -import { mcpUsageRepository, type UserMcp, userMcpsRepository } from "@/db/repositories"; +import { + mcpUsageRepository, + type UserMcp, + userMcpsRepository, +} from "@/db/repositories"; import { cache } from "@/lib/cache/client"; import { CacheKeys, CacheTTL } from "@/lib/cache/keys"; import { assertSafeOutboundUrl } from "@/lib/security/outbound-url"; @@ -131,7 +135,10 @@ class UserMcpsService { async create(params: CreateMcpParams): Promise { // Validate container exists if using container endpoint if (params.endpointType === "container" && params.containerId) { - const container = await containersService.getById(params.containerId, params.organizationId); + const container = await containersService.getById( + params.containerId, + params.organizationId, + ); if (!container) { throw new Error("Container not found"); } @@ -145,7 +152,10 @@ class UserMcpsService { } // Check slug uniqueness - const existing = await userMcpsRepository.getBySlug(params.slug, params.organizationId); + const existing = await userMcpsRepository.getBySlug( + params.slug, + params.organizationId, + ); if (existing) { throw new Error(`MCP with slug "${params.slug}" already exists`); } @@ -167,8 +177,11 @@ class UserMcpsService { credits_per_request: params.creditsPerRequest?.toString() ?? "1.0000", x402_price_usd: params.x402PriceUsd?.toString() ?? "0.000100", x402_enabled: params.x402Enabled ?? false, - creator_share_percentage: params.creatorSharePercentage?.toString() ?? "80.00", - platform_share_percentage: (100 - (params.creatorSharePercentage ?? 80)).toString(), + creator_share_percentage: + params.creatorSharePercentage?.toString() ?? "80.00", + platform_share_percentage: ( + 100 - (params.creatorSharePercentage ?? 80) + ).toString(), documentation_url: params.documentationUrl, source_code_url: params.sourceCodeUrl, support_email: params.supportEmail, @@ -206,7 +219,10 @@ class UserMcpsService { /** * Get MCP by slug and organization */ - async getBySlug(slug: string, organizationId: string): Promise { + async getBySlug( + slug: string, + organizationId: string, + ): Promise { const cacheKey = CacheKeys.mcp.bySlug(organizationId, slug); const cached = await cache.get(cacheKey); if (cached) return cached; @@ -247,7 +263,11 @@ class UserMcpsService { /** * Update an MCP */ - async update(id: string, organizationId: string, params: UpdateMcpParams): Promise { + async update( + id: string, + organizationId: string, + params: UpdateMcpParams, + ): Promise { const mcp = await userMcpsRepository.getById(id); if (!mcp) { throw new Error("MCP not found"); @@ -259,26 +279,36 @@ class UserMcpsService { const updateData: Partial = {}; if (params.name !== undefined) updateData.name = params.name; - if (params.description !== undefined) updateData.description = params.description; + if (params.description !== undefined) + updateData.description = params.description; if (params.version !== undefined) updateData.version = params.version; if (params.category !== undefined) updateData.category = params.category; - if (params.endpointPath !== undefined) updateData.endpoint_path = params.endpointPath; - if (params.transportType !== undefined) updateData.transport_type = params.transportType; + if (params.endpointPath !== undefined) + updateData.endpoint_path = params.endpointPath; + if (params.transportType !== undefined) + updateData.transport_type = params.transportType; if (params.tools !== undefined) updateData.tools = params.tools; - if (params.pricingType !== undefined) updateData.pricing_type = params.pricingType; + if (params.pricingType !== undefined) + updateData.pricing_type = params.pricingType; if (params.creditsPerRequest !== undefined) updateData.credits_per_request = params.creditsPerRequest.toString(); if (params.x402PriceUsd !== undefined) updateData.x402_price_usd = params.x402PriceUsd.toString(); - if (params.x402Enabled !== undefined) updateData.x402_enabled = params.x402Enabled; + if (params.x402Enabled !== undefined) + updateData.x402_enabled = params.x402Enabled; if (params.creatorSharePercentage !== undefined) { - updateData.creator_share_percentage = params.creatorSharePercentage.toString(); - updateData.platform_share_percentage = (100 - params.creatorSharePercentage).toString(); + updateData.creator_share_percentage = + params.creatorSharePercentage.toString(); + updateData.platform_share_percentage = ( + 100 - params.creatorSharePercentage + ).toString(); } if (params.documentationUrl !== undefined) updateData.documentation_url = params.documentationUrl; - if (params.sourceCodeUrl !== undefined) updateData.source_code_url = params.sourceCodeUrl; - if (params.supportEmail !== undefined) updateData.support_email = params.supportEmail; + if (params.sourceCodeUrl !== undefined) + updateData.source_code_url = params.sourceCodeUrl; + if (params.supportEmail !== undefined) + updateData.support_email = params.supportEmail; if (params.tags !== undefined) updateData.tags = params.tags; if (params.icon !== undefined) updateData.icon = params.icon; if (params.color !== undefined) updateData.color = params.color; @@ -427,17 +457,22 @@ class UserMcpsService { platformFeeCredits = creditsCharged * (platformPercent / 100); } } catch (e) { - logger.error(`[UserMcps] Error fetching referrer for ${params.userId}`, e); + logger.error( + `[UserMcps] Error fetching referrer for ${params.userId}`, + e, + ); } } - const totalCreditsToDeduct = creditsCharged + affiliateFeeCredits + platformFeeCredits; + const totalCreditsToDeduct = + creditsCharged + affiliateFeeCredits + platformFeeCredits; const creatorSharePct = Number(mcp.creator_share_percentage) / 100; const platformSharePct = Number(mcp.platform_share_percentage) / 100; const creatorEarnings = creditsCharged * creatorSharePct; - const platformEarnings = creditsCharged * platformSharePct + platformFeeCredits; + const platformEarnings = + creditsCharged * platformSharePct + platformFeeCredits; // Charge the consumer if (params.paymentType === "credits" && totalCreditsToDeduct > 0) { @@ -536,7 +571,11 @@ class UserMcpsService { }); // Update MCP stats - await userMcpsRepository.incrementUsage(params.mcpId, creatorEarnings, x402AmountUsd); + await userMcpsRepository.incrementUsage( + params.mcpId, + creatorEarnings, + x402AmountUsd, + ); logger.info("[UserMcps] Recorded usage", { mcpId: params.mcpId, @@ -561,7 +600,9 @@ class UserMcpsService { * Use this when credits have already been deducted by the caller. * This only handles revenue distribution and usage tracking. */ - async recordUsageWithoutDeduction(params: UseMcpWithoutDeductionParams): Promise { + async recordUsageWithoutDeduction( + params: UseMcpWithoutDeductionParams, + ): Promise { const mcp = await userMcpsRepository.getById(params.mcpId); if (!mcp) { throw new Error("MCP not found"); @@ -574,11 +615,16 @@ class UserMcpsService { const platformSharePct = Number(mcp.platform_share_percentage) / 100; const creatorEarnings = creditsCharged * creatorSharePct; - const platformEarnings = creditsCharged * platformSharePct + platformFeeCredits; + const platformEarnings = + creditsCharged * platformSharePct + platformFeeCredits; const CREDITS_PER_DOLLAR = 100; // 1 cent = 1 credit - if (affiliateFeeCredits > 0 && params.affiliateOwnerId && params.affiliateCodeId) { + if ( + affiliateFeeCredits > 0 && + params.affiliateOwnerId && + params.affiliateCodeId + ) { const sourceSuffix = typeof params.metadata?.preChargeTransactionId === "string" ? params.metadata.preChargeTransactionId @@ -594,7 +640,8 @@ class UserMcpsService { buyer_user_id: params.userId, buyer_org_id: params.organizationId, mcp_id: mcp.id, - total_credits_charged: creditsCharged + affiliateFeeCredits + platformFeeCredits, + total_credits_charged: + creditsCharged + affiliateFeeCredits + platformFeeCredits, }, }); diff --git a/packages/lib/services/user-metrics.ts b/packages/lib/services/user-metrics.ts index e22d7ae06..be4399c06 100644 --- a/packages/lib/services/user-metrics.ts +++ b/packages/lib/services/user-metrics.ts @@ -16,12 +16,30 @@ * admin engagement dashboard. */ -import { and, count, countDistinct, eq, gte, inArray, isNull, lt, ne, sql } from "drizzle-orm"; +import { + and, + count, + countDistinct, + eq, + gte, + inArray, + isNull, + lt, + ne, + sql, +} from "drizzle-orm"; import { dbRead, dbWrite } from "@/db/client"; -import { type DailyMetric, dailyMetrics, type MetricsPlatform } from "@/db/schemas/daily-metrics"; +import { + type DailyMetric, + dailyMetrics, + type MetricsPlatform, +} from "@/db/schemas/daily-metrics"; import { memoryTable, roomTable } from "@/db/schemas/eliza"; import { platformCredentials } from "@/db/schemas/platform-credentials"; -import { type RetentionCohort, retentionCohorts } from "@/db/schemas/retention-cohorts"; +import { + type RetentionCohort, + retentionCohorts, +} from "@/db/schemas/retention-cohorts"; import { userIdentities } from "@/db/schemas/user-identities"; import { users } from "@/db/schemas/users"; import { cache } from "@/lib/cache/client"; @@ -92,7 +110,9 @@ class UserMetricsService { /** * Count unique active users across all platforms in the given window. */ - async getActiveUsers(timeRange: "day" | "7d" | "30d"): Promise { + async getActiveUsers( + timeRange: "day" | "7d" | "30d", + ): Promise { const cacheKey = CacheKeys.userMetrics.activeUsers(timeRange); const cached = await cache.getWithSWR( cacheKey, @@ -103,7 +123,9 @@ class UserMetricsService { return cached ?? this._queryActiveUsers(timeRange); } - private async _queryActiveUsers(timeRange: "day" | "7d" | "30d"): Promise { + private async _queryActiveUsers( + timeRange: "day" | "7d" | "30d", + ): Promise { const since = this._rangeSince(timeRange); // Count distinct message *senders* (memoryTable.entityId) rather than @@ -226,7 +248,10 @@ class UserMetricsService { // PRE-COMPUTED READS // ========================================================================= - async getDailyMetrics(startDate: Date, endDate: Date): Promise { + async getDailyMetrics( + startDate: Date, + endDate: Date, + ): Promise { const key = CacheKeys.userMetrics.daily( startDate.toISOString().split("T")[0], endDate.toISOString().split("T")[0], @@ -237,14 +262,19 @@ class UserMetricsService { const rows = await dbRead .select() .from(dailyMetrics) - .where(and(gte(dailyMetrics.date, startDate), lt(dailyMetrics.date, endDate))) + .where( + and(gte(dailyMetrics.date, startDate), lt(dailyMetrics.date, endDate)), + ) .orderBy(dailyMetrics.date); await cache.set(key, rows, CacheTTL.userMetrics.daily); return rows; } - async getRetentionCohorts(startDate: Date, endDate: Date): Promise { + async getRetentionCohorts( + startDate: Date, + endDate: Date, + ): Promise { const key = CacheKeys.userMetrics.retention( startDate.toISOString().split("T")[0], endDate.toISOString().split("T")[0], @@ -312,7 +342,9 @@ class UserMetricsService { // Weighted average: totalMessages / totalDAU avoids the averaging-averages // problem where low-DAU days get the same weight as high-DAU days. - const recentAll = dailyTrend.filter((d) => d.platform === null && d.dau > 0); + const recentAll = dailyTrend.filter( + (d) => d.platform === null && d.dau > 0, + ); const totalMessages = recentAll.reduce((s, d) => s + d.total_messages, 0); const totalDau = recentAll.reduce((s, d) => s + d.dau, 0); const avgMessagesPerUser = totalDau > 0 ? totalMessages / totalDau : 0; @@ -348,13 +380,27 @@ class UserMetricsService { date: dayStart.toISOString(), }); - const perPlatform: Array = ["web", "telegram", "discord", "imessage", "sms"]; + const perPlatform: Array = [ + "web", + "telegram", + "discord", + "imessage", + "sms", + ]; // Compute per-platform rows in parallel (independent queries). await Promise.all( perPlatform.map(async (platform) => { - const { dau, totalMessages } = await this._countDayActivity(dayStart, dayEnd, platform); - const newSignups = await this._countNewSignups(dayStart, dayEnd, platform); + const { dau, totalMessages } = await this._countDayActivity( + dayStart, + dayEnd, + platform, + ); + const newSignups = await this._countNewSignups( + dayStart, + dayEnd, + platform, + ); const messagesPerUser = dau > 0 ? totalMessages / dau : 0; await dbWrite @@ -381,7 +427,11 @@ class UserMetricsService { // Compute aggregate (NULL platform) row last to avoid race with the // read-then-write pattern if parallelized. - const { dau, totalMessages } = await this._countDayActivity(dayStart, dayEnd, null); + const { dau, totalMessages } = await this._countDayActivity( + dayStart, + dayEnd, + null, + ); const newSignups = await this._countNewSignups(dayStart, dayEnd, null); const messagesPerUser = dau > 0 ? totalMessages / dau : 0; const values = { @@ -394,13 +444,20 @@ class UserMetricsService { const existing = await dbRead .select() .from(dailyMetrics) - .where(and(eq(dailyMetrics.date, dayStart), isNull(dailyMetrics.platform))) + .where( + and(eq(dailyMetrics.date, dayStart), isNull(dailyMetrics.platform)), + ) .limit(1); if (existing.length > 0) { - await dbWrite.update(dailyMetrics).set(values).where(eq(dailyMetrics.id, existing[0].id)); + await dbWrite + .update(dailyMetrics) + .set(values) + .where(eq(dailyMetrics.id, existing[0].id)); } else { - await dbWrite.insert(dailyMetrics).values({ date: dayStart, platform: null, ...values }); + await dbWrite + .insert(dailyMetrics) + .values({ date: dayStart, platform: null, ...values }); } } @@ -428,7 +485,9 @@ class UserMetricsService { // Each window targets a different cohort_date row, so they're independent. await Promise.all( - windows.map(({ field, daysAgo }) => this._computeRetentionWindow(field, daysAgo, dayStart)), + windows.map(({ field, daysAgo }) => + this._computeRetentionWindow(field, daysAgo, dayStart), + ), ); } @@ -471,7 +530,12 @@ class UserMetricsService { const existing = await dbRead .select() .from(retentionCohorts) - .where(and(eq(retentionCohorts.cohort_date, cohortDate), isNull(retentionCohorts.platform))) + .where( + and( + eq(retentionCohorts.cohort_date, cohortDate), + isNull(retentionCohorts.platform), + ), + ) .limit(1); if (existing.length > 0) { @@ -518,7 +582,8 @@ class UserMetricsService { dayEnd: Date, platform: MetricsPlatform | null, ): Promise<{ dau: number; totalMessages: number }> { - const sources = platform === null ? ALL_SOURCES : PLATFORM_TO_SOURCES[platform]; + const sources = + platform === null ? ALL_SOURCES : PLATFORM_TO_SOURCES[platform]; if (sources.length === 0) { return { dau: 0, totalMessages: 0 }; diff --git a/packages/lib/services/user-sessions.ts b/packages/lib/services/user-sessions.ts index 8d2dda587..3cc328c64 100644 --- a/packages/lib/services/user-sessions.ts +++ b/packages/lib/services/user-sessions.ts @@ -14,7 +14,11 @@ import type { NewUserSession, UserSession } from "@/db/schemas/user-sessions"; * @returns A 32-character SHA-256 hash */ function hashSessionToken(token: string): string { - return crypto.createHash("sha256").update(token).digest("hex").substring(0, 32); + return crypto + .createHash("sha256") + .update(token) + .digest("hex") + .substring(0, 32); } /** @@ -68,7 +72,9 @@ class UserSessionsService { return await userSessionsRepository.findById(id); } - async getActiveByToken(sessionToken: string): Promise { + async getActiveByToken( + sessionToken: string, + ): Promise { const hashedToken = normalizeToken(sessionToken); return await userSessionsRepository.findActiveByToken(hashedToken); } @@ -77,8 +83,14 @@ class UserSessionsService { return await userSessionsRepository.listActiveByUser(userId); } - async listByOrganization(organizationId: string, limit?: number): Promise { - return await userSessionsRepository.listByOrganization(organizationId, limit); + async listByOrganization( + organizationId: string, + limit?: number, + ): Promise { + return await userSessionsRepository.listByOrganization( + organizationId, + limit, + ); } async create(params: CreateSessionParams): Promise { @@ -101,7 +113,8 @@ class UserSessionsService { } async trackUsage(params: TrackUsageParams): Promise { - const { session_token, credits_used, requests_made, tokens_consumed } = params; + const { session_token, credits_used, requests_made, tokens_consumed } = + params; const hashedToken = normalizeToken(session_token); return await userSessionsRepository.incrementMetrics(hashedToken, { diff --git a/packages/lib/services/users.ts b/packages/lib/services/users.ts index fb14bbace..c5b5f29d6 100644 --- a/packages/lib/services/users.ts +++ b/packages/lib/services/users.ts @@ -43,7 +43,9 @@ export class UsersService { const stewardUserId = user.steward_user_id; if (typeof stewardUserId === "string") { promises.push(cache.del(CacheKeys.user.byStewardId(stewardUserId))); - promises.push(cache.del(CacheKeys.user.byStewardIdWithOrg(stewardUserId))); + promises.push( + cache.del(CacheKeys.user.byStewardIdWithOrg(stewardUserId)), + ); } const privyUserId = user.privy_user_id; if (typeof privyUserId === "string") { @@ -53,7 +55,9 @@ export class UsersService { const walletAddress = user.wallet_address; if (typeof walletAddress === "string") { promises.push(cache.del(CacheKeys.user.byWalletAddress(walletAddress))); - promises.push(cache.del(CacheKeys.user.byWalletAddressWithOrg(walletAddress))); + promises.push( + cache.del(CacheKeys.user.byWalletAddressWithOrg(walletAddress)), + ); } await Promise.all(promises); logger.debug("[UsersService] Invalidated cache for user:", user.id); @@ -89,7 +93,9 @@ export class UsersService { return user; } - async getByStewardId(stewardUserId: string): Promise { + async getByStewardId( + stewardUserId: string, + ): Promise { const cacheKey = CacheKeys.user.byStewardId(stewardUserId); const cached = await cache.get(cacheKey); if (cached) { @@ -98,7 +104,8 @@ export class UsersService { } try { - const user = await usersRepository.findByStewardIdWithOrganization(stewardUserId); + const user = + await usersRepository.findByStewardIdWithOrganization(stewardUserId); if (user) { await cache.set(cacheKey, user, CacheTTL.user.byStewardId); logger.debug("[UsersService] Cached user data by stewardId"); @@ -107,10 +114,13 @@ export class UsersService { } catch (error) { const errorDetails = getErrorDetails(error); - logger.warn("[UsersService] Read-path Steward lookup failed, retrying on primary", { - stewardUserId, - ...errorDetails, - }); + logger.warn( + "[UsersService] Read-path Steward lookup failed, retrying on primary", + { + stewardUserId, + ...errorDetails, + }, + ); try { return await this.getByStewardIdForWrite(stewardUserId); @@ -125,7 +135,9 @@ export class UsersService { } } - async getByPrivyId(privyUserId: string): Promise { + async getByPrivyId( + privyUserId: string, + ): Promise { const cacheKey = CacheKeys.user.byPrivyId(privyUserId); const cached = await cache.get(cacheKey); if (cached) { @@ -134,7 +146,8 @@ export class UsersService { } try { - const user = await usersRepository.findByPrivyIdWithOrganization(privyUserId); + const user = + await usersRepository.findByPrivyIdWithOrganization(privyUserId); if (user) { await cache.set(cacheKey, user, CacheTTL.user.byPrivyId); logger.debug("[UsersService] Cached user data by privyId"); @@ -143,10 +156,13 @@ export class UsersService { } catch (error) { const errorDetails = getErrorDetails(error); - logger.warn("[UsersService] Read-path Privy lookup failed, retrying on primary", { - privyUserId, - ...errorDetails, - }); + logger.warn( + "[UsersService] Read-path Privy lookup failed, retrying on primary", + { + privyUserId, + ...errorDetails, + }, + ); try { return await this.getByPrivyIdForWrite(privyUserId); @@ -161,11 +177,18 @@ export class UsersService { } } - async getByPrivyIdForWrite(privyUserId: string): Promise { - const user = await usersRepository.findByPrivyIdWithOrganizationForWrite(privyUserId); + async getByPrivyIdForWrite( + privyUserId: string, + ): Promise { + const user = + await usersRepository.findByPrivyIdWithOrganizationForWrite(privyUserId); if (user) { await Promise.all([ - cache.set(CacheKeys.user.byPrivyId(privyUserId), user, CacheTTL.user.byPrivyId), + cache.set( + CacheKeys.user.byPrivyId(privyUserId), + user, + CacheTTL.user.byPrivyId, + ), cache.set( CacheKeys.user.byPrivyIdWithOrg(privyUserId), user, @@ -183,11 +206,20 @@ export class UsersService { return await usersRepository.findIdentityByPrivyIdForWrite(privyUserId); } - async getByStewardIdForWrite(stewardUserId: string): Promise { - const user = await usersRepository.findByStewardIdWithOrganizationForWrite(stewardUserId); + async getByStewardIdForWrite( + stewardUserId: string, + ): Promise { + const user = + await usersRepository.findByStewardIdWithOrganizationForWrite( + stewardUserId, + ); if (user) { await Promise.all([ - cache.set(CacheKeys.user.byStewardId(stewardUserId), user, CacheTTL.user.byStewardId), + cache.set( + CacheKeys.user.byStewardId(stewardUserId), + user, + CacheTTL.user.byStewardId, + ), cache.set( CacheKeys.user.byStewardIdWithOrg(stewardUserId), user, @@ -205,7 +237,9 @@ export class UsersService { return await usersRepository.findIdentityByStewardIdForWrite(stewardUserId); } - async getWithOrganization(userId: string): Promise { + async getWithOrganization( + userId: string, + ): Promise { const cacheKey = CacheKeys.user.withOrg(userId); const cached = await cache.get(cacheKey); if (cached) { @@ -220,7 +254,9 @@ export class UsersService { return user; } - async getByEmailWithOrganization(email: string): Promise { + async getByEmailWithOrganization( + email: string, + ): Promise { const cacheKey = CacheKeys.user.byEmailWithOrg(email); const cached = await cache.get(cacheKey); if (cached) { @@ -259,7 +295,8 @@ export class UsersService { logger.debug("[UsersService] Cache hit for user byWalletAddressWithOrg"); return cached; } - const user = await usersRepository.findByWalletAddressWithOrganization(walletAddress); + const user = + await usersRepository.findByWalletAddressWithOrganization(walletAddress); if (user) { await cache.set(cacheKey, user, CacheTTL.user.byWalletAddressWithOrg); logger.debug("[UsersService] Cached user data byWalletAddressWithOrg"); @@ -287,8 +324,12 @@ export class UsersService { return result; } - async upsertPrivyIdentity(userId: string, privyUserId: string): Promise { - const existingIdentity = await usersRepository.findIdentityByUserIdForWrite(userId); + async upsertPrivyIdentity( + userId: string, + privyUserId: string, + ): Promise { + const existingIdentity = + await usersRepository.findIdentityByUserIdForWrite(userId); // Existing authenticated users hit this path on every Privy-backed request. // Once the projection already points at the canonical Privy ID, avoid the @@ -312,18 +353,27 @@ export class UsersService { cache.del(CacheKeys.user.byPrivyIdWithOrg(privyUserId)), ]; - if (existingIdentity?.privy_user_id && existingIdentity.privy_user_id !== privyUserId) { + if ( + existingIdentity?.privy_user_id && + existingIdentity.privy_user_id !== privyUserId + ) { cacheDeletes.push( cache.del(CacheKeys.user.byPrivyId(existingIdentity.privy_user_id)), - cache.del(CacheKeys.user.byPrivyIdWithOrg(existingIdentity.privy_user_id)), + cache.del( + CacheKeys.user.byPrivyIdWithOrg(existingIdentity.privy_user_id), + ), ); } await Promise.all(cacheDeletes); } - async upsertStewardIdentity(userId: string, stewardUserId: string): Promise { - const existingIdentity = await usersRepository.findIdentityByUserIdForWrite(userId); + async upsertStewardIdentity( + userId: string, + stewardUserId: string, + ): Promise { + const existingIdentity = + await usersRepository.findIdentityByUserIdForWrite(userId); if (existingIdentity?.steward_user_id === stewardUserId) { await Promise.all([ @@ -340,10 +390,15 @@ export class UsersService { cache.del(CacheKeys.user.byStewardIdWithOrg(stewardUserId)), ]; - if (existingIdentity?.steward_user_id && existingIdentity.steward_user_id !== stewardUserId) { + if ( + existingIdentity?.steward_user_id && + existingIdentity.steward_user_id !== stewardUserId + ) { cacheDeletes.push( cache.del(CacheKeys.user.byStewardId(existingIdentity.steward_user_id)), - cache.del(CacheKeys.user.byStewardIdWithOrg(existingIdentity.steward_user_id)), + cache.del( + CacheKeys.user.byStewardIdWithOrg(existingIdentity.steward_user_id), + ), ); } @@ -364,7 +419,8 @@ export class UsersService { // Check if this was the last user in the organization if (organizationId) { - const remainingUsers = await usersRepository.listByOrganization(organizationId); + const remainingUsers = + await usersRepository.listByOrganization(organizationId); // If no users remain, delete the organization if (remainingUsers.length === 0) { diff --git a/packages/lib/services/vercel-deployments.ts b/packages/lib/services/vercel-deployments.ts index d3462742b..9e973f829 100644 --- a/packages/lib/services/vercel-deployments.ts +++ b/packages/lib/services/vercel-deployments.ts @@ -77,7 +77,10 @@ interface SubdomainResult { /** * Make authenticated request to Vercel API */ -async function vercelFetch(path: string, options: RequestInit = {}): Promise { +async function vercelFetch( + path: string, + options: RequestInit = {}, +): Promise { if (!VERCEL_TOKEN) { throw new Error("VERCEL_TOKEN is not configured"); } @@ -202,7 +205,8 @@ async function getOrCreateVercelProject( }, { key: "NEXT_PUBLIC_ELIZA_API_URL", - value: process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai", + value: + process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai", target: ["production", "preview", "development"], type: "plain", }, @@ -321,7 +325,10 @@ export async function assignSubdomain( /** * Add domain to Vercel project */ -async function addDomainToProject(projectId: string, domain: string): Promise { +async function addDomainToProject( + projectId: string, + domain: string, +): Promise { try { await vercelFetch(`/v10/projects/${projectId}/domains`, { method: "POST", @@ -356,7 +363,8 @@ export async function createDeployment( if (!VERCEL_TOKEN || !VERCEL_TEAM_ID) { return { success: false, - error: "Vercel deployment is not configured. Set VERCEL_TOKEN and VERCEL_TEAM_ID.", + error: + "Vercel deployment is not configured. Set VERCEL_TOKEN and VERCEL_TEAM_ID.", }; } @@ -438,23 +446,28 @@ export async function createDeployment( try { // Parse GitHub repo - const [org, repo] = githubRepo.includes("/") ? githubRepo.split("/") : [GITHUB_ORG, githubRepo]; + const [org, repo] = githubRepo.includes("/") + ? githubRepo.split("/") + : [GITHUB_ORG, githubRepo]; // Create deployment via Vercel API - const deploymentResponse = await vercelFetch("/v13/deployments", { - method: "POST", - body: JSON.stringify({ - name: project.projectName, - project: project.projectId, - gitSource: { - type: "github", - org, - repo, - ref: commitSha || branch, - }, - target, - }), - }); + const deploymentResponse = await vercelFetch( + "/v13/deployments", + { + method: "POST", + body: JSON.stringify({ + name: project.projectName, + project: project.projectId, + gitSource: { + type: "github", + org, + repo, + ref: commitSha || branch, + }, + target, + }), + }, + ); logger.info("[Vercel Deployments] Deployment created", { appId, @@ -471,7 +484,8 @@ export async function createDeployment( productionUrl, }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; logger.error("[Vercel Deployments] Failed to create deployment", { appId, projectId: project.projectId, @@ -604,7 +618,9 @@ export async function getProductionUrl(appId: string): Promise { /** * Get the Vercel project ID for an app */ -export async function getVercelProjectId(appId: string): Promise { +export async function getVercelProjectId( + appId: string, +): Promise { const domain = await dbRead.query.appDomains.findFirst({ where: eq(appDomains.app_id, appId), }); diff --git a/packages/lib/services/vercel-domains.ts b/packages/lib/services/vercel-domains.ts index 92920638c..15a04a7fb 100644 --- a/packages/lib/services/vercel-domains.ts +++ b/packages/lib/services/vercel-domains.ts @@ -16,7 +16,10 @@ import { and, eq, ne } from "drizzle-orm"; import { dbRead, dbWrite } from "@/db/client"; -import { appDomains, type DomainVerificationRecord } from "@/db/schemas/app-domains"; +import { + appDomains, + type DomainVerificationRecord, +} from "@/db/schemas/app-domains"; import { extractErrorMessage } from "@/lib/utils/error-handling"; import { logger } from "@/lib/utils/logger"; import { vercelApiRequest } from "@/lib/utils/vercel-api"; @@ -150,7 +153,10 @@ interface AddDomainResult { /** * Make authenticated request to Vercel API */ -async function vercelFetch(path: string, options: RequestInit = {}): Promise { +async function vercelFetch( + path: string, + options: RequestInit = {}, +): Promise { if (!VERCEL_TOKEN) { throw new Error("VERCEL_TOKEN is not configured"); } @@ -190,7 +196,10 @@ export async function isDomainInUse( const existing = await dbRead.query.appDomains.findFirst({ where: excludeAppId - ? and(eq(appDomains.custom_domain, normalizedDomain), ne(appDomains.app_id, excludeAppId)) + ? and( + eq(appDomains.custom_domain, normalizedDomain), + ne(appDomains.app_id, excludeAppId), + ) : eq(appDomains.custom_domain, normalizedDomain), }); @@ -203,7 +212,10 @@ export async function isDomainInUse( /** * Add a custom domain to the app's Vercel project */ -export async function addDomain(appId: string, domain: string): Promise { +export async function addDomain( + appId: string, + domain: string, +): Promise { // Get the app's Vercel project ID const projectId = await getAppProjectId(appId); if (!projectId) { @@ -250,19 +262,22 @@ export async function addDomain(appId: string, domain: string): Promise(`/v10/projects/${projectId}/domains`, { - method: "POST", - body: JSON.stringify({ name: normalizedDomain }), - }); - - const verificationRecords: DomainVerificationRecord[] = (response.verification || []).map( - (v) => ({ - type: v.type as "TXT" | "CNAME" | "A", - name: v.domain, - value: v.value, - }), + const response = await vercelFetch( + `/v10/projects/${projectId}/domains`, + { + method: "POST", + body: JSON.stringify({ name: normalizedDomain }), + }, ); + const verificationRecords: DomainVerificationRecord[] = ( + response.verification || [] + ).map((v) => ({ + type: v.type as "TXT" | "CNAME" | "A", + name: v.domain, + value: v.value, + })); + // Store in database const existingDomain = await dbRead.query.appDomains.findFirst({ where: eq(appDomains.app_id, appId), @@ -282,9 +297,12 @@ export async function addDomain(appId: string, domain: string): Promise { +export async function getDomainStatus( + appId: string, + domain: string, +): Promise { // Get the app's Vercel project ID const projectId = await getAppProjectId(appId); if (!projectId) { @@ -341,24 +362,24 @@ export async function getDomainStatus(appId: string, domain: string): Promise(`/v6/domains/${normalizedDomain}/config`).catch( - (error) => { - logger.debug("[VercelDomains] Failed to fetch domain config", { - domain: normalizedDomain, - error: extractErrorMessage(error), - }); - return null; - }, - ), - vercelFetch(`/v7/certs?domain=${normalizedDomain}`).catch( - (error) => { - logger.debug("[VercelDomains] Failed to fetch certificates", { - domain: normalizedDomain, - error: extractErrorMessage(error), - }); - return [] as VercelCertificateResponse[]; - }, - ), + vercelFetch( + `/v6/domains/${normalizedDomain}/config`, + ).catch((error) => { + logger.debug("[VercelDomains] Failed to fetch domain config", { + domain: normalizedDomain, + error: extractErrorMessage(error), + }); + return null; + }), + vercelFetch( + `/v7/certs?domain=${normalizedDomain}`, + ).catch((error) => { + logger.debug("[VercelDomains] Failed to fetch certificates", { + domain: normalizedDomain, + error: extractErrorMessage(error), + }); + return [] as VercelCertificateResponse[]; + }), ]); if (!domainInfo) { @@ -375,7 +396,9 @@ export async function getDomainStatus(appId: string, domain: string): Promise ({ + const records: DomainVerificationRecord[] = ( + domainInfo.verification || [] + ).map((v) => ({ type: v.type as "TXT" | "CNAME" | "A", name: v.domain, value: v.value, @@ -566,7 +589,8 @@ export function validateSubdomain(subdomain: string): { if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(normalized)) { return { valid: false, - error: "Subdomain can only contain lowercase letters, numbers, and hyphens", + error: + "Subdomain can only contain lowercase letters, numbers, and hyphens", }; } diff --git a/packages/lib/services/vercel-sandbox-provider.ts b/packages/lib/services/vercel-sandbox-provider.ts index d0516fb96..857d9a620 100644 --- a/packages/lib/services/vercel-sandbox-provider.ts +++ b/packages/lib/services/vercel-sandbox-provider.ts @@ -6,7 +6,11 @@ */ import { logger } from "@/lib/utils/logger"; -import type { SandboxCreateConfig, SandboxHandle, SandboxProvider } from "./sandbox-provider"; +import type { + SandboxCreateConfig, + SandboxHandle, + SandboxProvider, +} from "./sandbox-provider"; // Set MILADY_AGENT_TEMPLATE_URL to override the default template repo. const CLOUD_AGENT_TEMPLATE_URL = @@ -53,7 +57,8 @@ export class VercelSandboxProvider implements SandboxProvider { type SB = { sandboxId?: string; domain: (port: number) => string }; const sb = (await Sandbox.create(opts)) as SB; - const sandboxId = sb.sandboxId ?? `sandbox-${crypto.randomUUID().slice(0, 8)}`; + const sandboxId = + sb.sandboxId ?? `sandbox-${crypto.randomUUID().slice(0, 8)}`; // Write .env.local as a fallback — some SDK versions ignore the env create option const envContent = Object.entries(env) @@ -69,7 +74,10 @@ export class VercelSandboxProvider implements SandboxProvider { if (typeof sbWithShell.runCommand === "function") { await sbWithShell.runCommand({ cmd: "sh", - args: ["-c", `cat > /app/.env.local << 'ENVEOF'\n${envContent}\nENVEOF`], + args: [ + "-c", + `cat > /app/.env.local << 'ENVEOF'\n${envContent}\nENVEOF`, + ], }); } @@ -127,6 +135,12 @@ export class VercelSandboxProvider implements SandboxProvider { VERCEL_PROJECT_ID: projectId, VERCEL_TOKEN: token, } = process.env; - return { hasOIDC, hasAccessToken: !!(teamId && projectId && token), teamId, projectId, token }; + return { + hasOIDC, + hasAccessToken: !!(teamId && projectId && token), + teamId, + projectId, + token, + }; } } diff --git a/packages/lib/services/vertex-model-registry.ts b/packages/lib/services/vertex-model-registry.ts index 671b1aaec..c77e93839 100644 --- a/packages/lib/services/vertex-model-registry.ts +++ b/packages/lib/services/vertex-model-registry.ts @@ -8,8 +8,14 @@ import { vertexTunedModels, vertexTuningJobs, } from "@/db/schemas"; -import type { ModelPreferenceKey, ModelPreferences } from "@/lib/eliza/model-preferences"; -import { mergeModelPreferences, normalizeModelPreferences } from "@/lib/eliza/model-preferences"; +import type { + ModelPreferenceKey, + ModelPreferences, +} from "@/lib/eliza/model-preferences"; +import { + mergeModelPreferences, + normalizeModelPreferences, +} from "@/lib/eliza/model-preferences"; import { logger } from "@/lib/utils/logger"; import { isValidUUID } from "@/lib/utils/validation"; import { @@ -37,7 +43,8 @@ type NormalizedScopeOwner = { userId?: string; }; -export interface RecordSubmittedVertexTuningJobInput extends AssignmentScopeOwner { +export interface RecordSubmittedVertexTuningJobInput + extends AssignmentScopeOwner { vertexJobName: string; projectId: string; region: string; @@ -78,7 +85,9 @@ function isTerminalJobState(state: TuningJob["state"]): boolean { ); } -function normalizeScopeOwner(input: AssignmentScopeOwner): NormalizedScopeOwner { +function normalizeScopeOwner( + input: AssignmentScopeOwner, +): NormalizedScopeOwner { const organizationId = input.organizationId?.trim(); const userId = input.userId?.trim(); @@ -87,12 +96,16 @@ function normalizeScopeOwner(input: AssignmentScopeOwner): NormalizedScopeOwner return { scope: "global", organizationId: undefined, userId: undefined }; case "organization": if (!organizationId || !isValidUUID(organizationId)) { - throw new Error("organizationId is required for organization-scoped tuned models"); + throw new Error( + "organizationId is required for organization-scoped tuned models", + ); } return { scope: "organization", organizationId, userId: undefined }; case "user": if (!organizationId || !isValidUUID(organizationId)) { - throw new Error("organizationId is required for user-scoped tuned models"); + throw new Error( + "organizationId is required for user-scoped tuned models", + ); } if (!userId || !isValidUUID(userId)) { throw new Error("userId is required for user-scoped tuned models"); @@ -115,7 +128,10 @@ function buildVisibilityCondition(viewer: ViewerScope) { if (viewer.userId && isValidUUID(viewer.userId)) { clauses.push( - and(eq(vertexTuningJobs.scope, "user"), eq(vertexTuningJobs.user_id, viewer.userId))!, + and( + eq(vertexTuningJobs.scope, "user"), + eq(vertexTuningJobs.user_id, viewer.userId), + )!, ); } @@ -181,7 +197,10 @@ function getScopePriority(scope: VertexTuningScope): number { } } -function getRecommendedModelId(remoteJob: TuningJob, fallbackDisplayName: string): string { +function getRecommendedModelId( + remoteJob: TuningJob, + fallbackDisplayName: string, +): string { return ( remoteJob.tunedModelEndpointName?.trim() || remoteJob.tunedModelDisplayName?.trim() || @@ -203,7 +222,9 @@ export class VertexModelRegistryService { scope: owner.scope, ownerId: owner.userId ?? owner.organizationId, }); - const completedAt = isTerminalJobState(input.remoteJob.state) ? new Date() : null; + const completedAt = isTerminalJobState(input.remoteJob.state) + ? new Date() + : null; const [job] = await dbWrite .insert(vertexTuningJobs) @@ -231,8 +252,14 @@ export class VertexModelRegistryService { status: input.remoteJob.state, error_code: input.remoteJob.error?.code, error_message: input.remoteJob.error?.message, - model_preference_patch: patch.modelPreferences as Record, - last_remote_payload: input.remoteJob as unknown as Record, + model_preference_patch: patch.modelPreferences as Record< + string, + string + >, + last_remote_payload: input.remoteJob as unknown as Record< + string, + unknown + >, metadata: input.metadata ?? {}, completed_at: completedAt, created_at: new Date(input.remoteJob.createTime), @@ -263,8 +290,14 @@ export class VertexModelRegistryService { status: input.remoteJob.state, error_code: input.remoteJob.error?.code, error_message: input.remoteJob.error?.message, - model_preference_patch: patch.modelPreferences as Record, - last_remote_payload: input.remoteJob as unknown as Record, + model_preference_patch: patch.modelPreferences as Record< + string, + string + >, + last_remote_payload: input.remoteJob as unknown as Record< + string, + unknown + >, metadata: input.metadata ?? {}, completed_at: completedAt, updated_at: new Date(input.remoteJob.updateTime), @@ -299,7 +332,10 @@ export class VertexModelRegistryService { } const remoteJob = await getTuningJobStatus(job.vertex_job_name); - const recommendedModelId = getRecommendedModelId(remoteJob, job.display_name); + const recommendedModelId = getRecommendedModelId( + remoteJob, + job.display_name, + ); const [updatedJob] = await dbWrite .update(vertexTuningJobs) .set({ @@ -359,7 +395,13 @@ export class VertexModelRegistryService { slot?: VertexTuningSlot; limit?: number; } = {}, - ): Promise> { + ): Promise< + Array< + VertexTunedModelRecord & { + activeAssignments: VertexModelAssignmentRecord[]; + } + > + > { const conditions = [buildModelVisibilityCondition(viewer)]; if (filters.scope) { @@ -382,7 +424,10 @@ export class VertexModelRegistryService { limit: filters.limit ? filters.limit * 4 : undefined, }); - const assignmentsByModelId = new Map(); + const assignmentsByModelId = new Map< + string, + VertexModelAssignmentRecord[] + >(); for (const item of visibleAssignments) { const existing = assignmentsByModelId.get(item.tunedModel.id) ?? []; existing.push(item.assignment); @@ -422,7 +467,10 @@ export class VertexModelRegistryService { tunedModel: vertexTunedModels, }) .from(vertexModelAssignments) - .innerJoin(vertexTunedModels, eq(vertexModelAssignments.tuned_model_id, vertexTunedModels.id)) + .innerJoin( + vertexTunedModels, + eq(vertexModelAssignments.tuned_model_id, vertexTunedModels.id), + ) .where(and(...conditions)) .orderBy(desc(vertexModelAssignments.activated_at)) .limit(filters.limit ?? 100); @@ -559,25 +607,34 @@ export class VertexModelRegistryService { return result.length; } - async resolveModelPreferences(viewer: ViewerScope): Promise { + async resolveModelPreferences( + viewer: ViewerScope, + ): Promise { const assignments = await this.listVisibleAssignments(viewer, { activeOnly: true, limit: 32, }); const sortedAssignments = assignments.sort((a, b) => { - const scopeDiff = getScopePriority(a.assignment.scope) - getScopePriority(b.assignment.scope); + const scopeDiff = + getScopePriority(a.assignment.scope) - + getScopePriority(b.assignment.scope); if (scopeDiff !== 0) { return scopeDiff; } - return a.assignment.activated_at.getTime() - b.assignment.activated_at.getTime(); + return ( + a.assignment.activated_at.getTime() - + b.assignment.activated_at.getTime() + ); }); const sources: ResolvedModelPreferences["sources"] = {}; let merged: ModelPreferences | undefined; for (const assignment of sortedAssignments) { - const normalized = normalizeModelPreferences(assignment.tunedModel.model_preferences); + const normalized = normalizeModelPreferences( + assignment.tunedModel.model_preferences, + ); if (!normalized) { continue; } @@ -603,7 +660,10 @@ export class VertexModelRegistryService { job: VertexTuningJobRecord, remoteJob: TuningJob, ): Promise { - const recommendedModelId = getRecommendedModelId(remoteJob, job.display_name); + const recommendedModelId = getRecommendedModelId( + remoteJob, + job.display_name, + ); const modelPreferences = buildVertexModelPreferencePatch({ slot: job.slot, diff --git a/packages/lib/services/vertex-tuning.ts b/packages/lib/services/vertex-tuning.ts index 46920ed5f..cbeaff4d0 100644 --- a/packages/lib/services/vertex-tuning.ts +++ b/packages/lib/services/vertex-tuning.ts @@ -89,7 +89,10 @@ async function getAccessToken(providedToken?: string): Promise { } try { - const { stdout } = await execFileAsync("gcloud", ["auth", "print-access-token"]); + const { stdout } = await execFileAsync("gcloud", [ + "auth", + "print-access-token", + ]); const token = stdout.trim(); if (token) return token; } catch { @@ -180,13 +183,17 @@ export async function uploadToGCS( }); if (!response.ok) { - throw new Error(`GCS upload failed: ${response.status} ${await response.text()}`); + throw new Error( + `GCS upload failed: ${response.status} ${await response.text()}`, + ); } return `gs://${bucket}/${objectName}`; } -export async function createTuningJob(config: VertexTuningConfig): Promise { +export async function createTuningJob( + config: VertexTuningConfig, +): Promise { const region = config.region ?? "us-central1"; const accessToken = await getAccessToken(config.accessToken); const timestamp = Date.now(); @@ -226,7 +233,9 @@ export async function createTuningJob(config: VertexTuningConfig): Promise { const token = await getAccessToken(accessToken); - const response = await fetch(`https://aiplatform.googleapis.com/v1/${jobName}`, { - headers: { authorization: `Bearer ${token}` }, - }); + const response = await fetch( + `https://aiplatform.googleapis.com/v1/${jobName}`, + { + headers: { authorization: `Bearer ${token}` }, + }, + ); if (!response.ok) { - throw new Error(`Failed to get tuning job status: ${response.status} ${await response.text()}`); + throw new Error( + `Failed to get tuning job status: ${response.status} ${await response.text()}`, + ); } return (await response.json()) as TuningJob; @@ -303,7 +319,9 @@ export async function orchestrateVertexTuning( const job = createdJob.job; const recommendedModelId = - job.tunedModelEndpointName?.trim() || job.tunedModelDisplayName?.trim() || config.displayName; + job.tunedModelEndpointName?.trim() || + job.tunedModelDisplayName?.trim() || + config.displayName; return { job, diff --git a/packages/lib/services/voice-cloning.ts b/packages/lib/services/voice-cloning.ts index c16be32c7..e27a794fd 100644 --- a/packages/lib/services/voice-cloning.ts +++ b/packages/lib/services/voice-cloning.ts @@ -2,7 +2,11 @@ import { put } from "@vercel/blob"; import { and, desc, eq } from "drizzle-orm"; import { dbRead, dbWrite } from "@/db/client"; import type { NewUserVoice, VoiceCloningJob } from "@/db/schemas/user-voices"; -import { userVoices, voiceCloningJobs, voiceSamples } from "@/db/schemas/user-voices"; +import { + userVoices, + voiceCloningJobs, + voiceSamples, +} from "@/db/schemas/user-voices"; import { logger } from "@/lib/utils/logger"; import { calculateVoiceCloneCostFromCatalog } from "./ai-pricing"; import { getElevenLabsService } from "./elevenlabs"; @@ -51,8 +55,18 @@ export class VoiceCloningService { /** * Create a voice clone (instant or professional) */ - async createVoiceClone(params: CreateVoiceCloneParams): Promise { - const { organizationId, userId, name, description, cloneType, files, settings = {} } = params; + async createVoiceClone( + params: CreateVoiceCloneParams, + ): Promise { + const { + organizationId, + userId, + name, + description, + cloneType, + files, + settings = {}, + } = params; logger.info(`[VoiceCloning] Starting ${cloneType} voice clone: ${name}`, { organizationId, @@ -128,9 +142,12 @@ export class VoiceCloningService { }), ); } else { - logger.info("[VoiceCloning] Skipping blob storage (no token configured)", { - jobId: job.id, - }); + logger.info( + "[VoiceCloning] Skipping blob storage (no token configured)", + { + jobId: job.id, + }, + ); } logger.info("[VoiceCloning] Creating voice in ElevenLabs", { @@ -213,7 +230,8 @@ export class VoiceCloningService { return { userVoice, job: updatedJob }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; // Only update job status if job was created if (job) { @@ -232,9 +250,12 @@ export class VoiceCloningService { }) .where(eq(voiceCloningJobs.id, job.id)); } else { - logger.error("[VoiceCloning] Error creating voice clone (before job creation)", { - error: errorMessage, - }); + logger.error( + "[VoiceCloning] Error creating voice clone (before job creation)", + { + error: errorMessage, + }, + ); } throw error; @@ -278,7 +299,12 @@ export class VoiceCloningService { const [voice] = await dbRead .select() .from(userVoices) - .where(and(eq(userVoices.id, voiceId), eq(userVoices.organizationId, organizationId))); + .where( + and( + eq(userVoices.id, voiceId), + eq(userVoices.organizationId, organizationId), + ), + ); if (!voice) { return null; @@ -313,7 +339,12 @@ export class VoiceCloningService { ...updates, updatedAt: new Date(), }) - .where(and(eq(userVoices.id, voiceId), eq(userVoices.organizationId, organizationId))) + .where( + and( + eq(userVoices.id, voiceId), + eq(userVoices.organizationId, organizationId), + ), + ) .returning(); if (!updatedVoice) { @@ -371,7 +402,10 @@ export class VoiceCloningService { */ async incrementUsageCount(voiceId: string): Promise { // Get current voice - const [voice] = await dbRead.select().from(userVoices).where(eq(userVoices.id, voiceId)); + const [voice] = await dbRead + .select() + .from(userVoices) + .where(eq(userVoices.id, voiceId)); if (voice) { await dbWrite @@ -393,7 +427,10 @@ export class VoiceCloningService { .select() .from(voiceCloningJobs) .where( - and(eq(voiceCloningJobs.id, jobId), eq(voiceCloningJobs.organizationId, organizationId)), + and( + eq(voiceCloningJobs.id, jobId), + eq(voiceCloningJobs.organizationId, organizationId), + ), ); return job || null; @@ -435,7 +472,9 @@ export class VoiceCloningService { // Check file type - allow any audio/* or specific types const isValidType = file.type.startsWith("audio/") || - this.ALLOWED_TYPES.some((type) => file.type.includes(type.split(";")[0])); + this.ALLOWED_TYPES.some((type) => + file.type.includes(type.split(";")[0]), + ); if (!isValidType) { throw new Error( @@ -454,7 +493,9 @@ export class VoiceCloningService { const maxTotalSize = this.MAX_FILE_SIZE * 10; // Max 100MB total if (totalSize > maxTotalSize) { - throw new Error(`Total file size exceeds maximum of ${maxTotalSize / 1024 / 1024}MB`); + throw new Error( + `Total file size exceeds maximum of ${maxTotalSize / 1024 / 1024}MB`, + ); } } } diff --git a/packages/lib/services/wallet-provider.ts b/packages/lib/services/wallet-provider.ts index fa78527fd..178aba895 100644 --- a/packages/lib/services/wallet-provider.ts +++ b/packages/lib/services/wallet-provider.ts @@ -46,7 +46,10 @@ export interface TransactionRequest { export interface WalletProvider { /** Create a new wallet for an agent. */ - createWallet(agentId: string, options?: CreateWalletOptions): Promise; + createWallet( + agentId: string, + options?: CreateWalletOptions, + ): Promise; /** Look up an existing wallet for an agent. Returns null if none exists. */ getWallet(agentId: string): Promise; diff --git a/packages/lib/services/wallet-signup.ts b/packages/lib/services/wallet-signup.ts index 2e2fac5b2..53c8a7bbc 100644 --- a/packages/lib/services/wallet-signup.ts +++ b/packages/lib/services/wallet-signup.ts @@ -38,14 +38,16 @@ export async function findOrCreateUserByWalletAddress( const normalized = address.toLowerCase(); const grantInitialCredits = options?.grantInitialCredits !== false; - const existing = await usersService.getByWalletAddressWithOrganization(address); + const existing = + await usersService.getByWalletAddressWithOrganization(address); if (existing) { return { user: existing, isNewAccount: false }; } /* WHY slug wallet-${normalized}: consistent with topup and SIWE; lowercase for unique indexing. */ const slug = `wallet-${normalized}`; - let org: Organization | null = (await organizationsRepository.findBySlug(slug)) ?? null; + let org: Organization | null = + (await organizationsRepository.findBySlug(slug)) ?? null; if (!org) { try { org = await organizationsService.create({ @@ -72,14 +74,17 @@ export async function findOrCreateUserByWalletAddress( } catch (e) { // Note: Handle race condition where two concurrent requests try to create the same org const isUniqueViolation = - e instanceof Error && (e.message.includes("unique") || e.message.includes("duplicate")); + e instanceof Error && + (e.message.includes("unique") || e.message.includes("duplicate")); if (!isUniqueViolation) throw e; // Important: For race conditions, retry finding the org that won the race org = (await organizationsRepository.findBySlug(slug)) ?? null; if (!org) { // If we still can't find it, something else went wrong - throw new Error("Organization creation failed and could not find existing org"); + throw new Error( + "Organization creation failed and could not find existing org", + ); } // Note: Skip initial credits for raced org - first creator already granted them } @@ -97,9 +102,11 @@ export async function findOrCreateUserByWalletAddress( } catch (e) { /* WHY handle unique violation: two concurrent signups for same wallet; second should see the first's user. */ const isUniqueViolation = - e instanceof Error && (e.message.includes("unique") || e.message.includes("duplicate")); + e instanceof Error && + (e.message.includes("unique") || e.message.includes("duplicate")); if (!isUniqueViolation) throw e; - const raced = await usersService.getByWalletAddressWithOrganization(address); + const raced = + await usersService.getByWalletAddressWithOrganization(address); if (!raced) throw e; return { user: raced, isNewAccount: false }; } diff --git a/packages/lib/services/whatsapp-automation/index.ts b/packages/lib/services/whatsapp-automation/index.ts index b7b42f4eb..b6fd4ccc2 100644 --- a/packages/lib/services/whatsapp-automation/index.ts +++ b/packages/lib/services/whatsapp-automation/index.ts @@ -28,7 +28,9 @@ import { // Use ELIZA_API_URL (ngrok) for local dev webhooks, otherwise NEXT_PUBLIC_APP_URL const WEBHOOK_BASE_URL = - process.env.ELIZA_API_URL || process.env.NEXT_PUBLIC_APP_URL || "https://www.elizacloud.ai"; + process.env.ELIZA_API_URL || + process.env.NEXT_PUBLIC_APP_URL || + "https://www.elizacloud.ai"; const META_REQUEST_TIMEOUT_MS = 10_000; const STATUS_CACHE_TTL_SECONDS = 5 * 60; @@ -108,7 +110,10 @@ class WhatsAppAutomationService { status: response.status, error: errorText.slice(0, 200), }); - return { valid: false, error: "Validation failed. Please check your credentials." }; + return { + valid: false, + error: "Validation failed. Please check your credentials.", + }; } const data = await response.json(); @@ -125,8 +130,13 @@ class WhatsAppAutomationService { }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; - logger.warn("[WhatsAppAutomation] Token validation error", { error: message }); - return { valid: false, error: "Validation failed due to network error. Please try again." }; + logger.warn("[WhatsAppAutomation] Token validation error", { + error: message, + }); + return { + valid: false, + error: "Validation failed due to network error. Please try again.", + }; } } @@ -175,7 +185,12 @@ class WhatsAppAutomationService { const existingSecrets = await secretsService.list(organizationId); const existingSecret = existingSecrets.find((s) => s.name === name); if (existingSecret) { - await secretsService.rotate(existingSecret.id, organizationId, value, audit); + await secretsService.rotate( + existingSecret.id, + organizationId, + value, + audit, + ); } else { throw err; } @@ -200,10 +215,13 @@ class WhatsAppAutomationService { await createOrUpdateSecret(name, value); } } catch (error) { - logger.error("[WhatsAppAutomation] Failed to store credentials, rolling back", { - organizationId, - error: error instanceof Error ? error.message : String(error), - }); + logger.error( + "[WhatsAppAutomation] Failed to store credentials, rolling back", + { + organizationId, + error: error instanceof Error ? error.message : String(error), + }, + ); await Promise.allSettled( secretEntries.map(async ([name]) => { @@ -232,7 +250,10 @@ class WhatsAppAutomationService { /** * Remove WhatsApp credentials (disconnect). */ - async removeCredentials(organizationId: string, userId: string): Promise { + async removeCredentials( + organizationId: string, + userId: string, + ): Promise { const audit = { actorType: "user" as const, actorId: userId, @@ -272,7 +293,10 @@ class WhatsAppAutomationService { * No env fallback in production to prevent multi-tenancy violation. */ async getAccessToken(organizationId: string): Promise { - const fromSecrets = await secretsService.get(organizationId, WHATSAPP_ACCESS_TOKEN); + const fromSecrets = await secretsService.get( + organizationId, + WHATSAPP_ACCESS_TOKEN, + ); if (fromSecrets) return fromSecrets; if (process.env.NODE_ENV !== "production") { return process.env.WHATSAPP_ACCESS_TOKEN || null; @@ -284,7 +308,10 @@ class WhatsAppAutomationService { * Get phone number ID for an organization. */ async getPhoneNumberId(organizationId: string): Promise { - const fromSecrets = await secretsService.get(organizationId, WHATSAPP_PHONE_NUMBER_ID); + const fromSecrets = await secretsService.get( + organizationId, + WHATSAPP_PHONE_NUMBER_ID, + ); if (fromSecrets) return fromSecrets; if (process.env.NODE_ENV !== "production") { return process.env.WHATSAPP_PHONE_NUMBER_ID || null; @@ -296,7 +323,10 @@ class WhatsAppAutomationService { * Get app secret for an organization. */ async getAppSecret(organizationId: string): Promise { - const fromSecrets = await secretsService.get(organizationId, WHATSAPP_APP_SECRET); + const fromSecrets = await secretsService.get( + organizationId, + WHATSAPP_APP_SECRET, + ); if (fromSecrets) return fromSecrets; if (process.env.NODE_ENV !== "production") { return process.env.WHATSAPP_APP_SECRET || null; @@ -328,7 +358,9 @@ class WhatsAppAutomationService { ): Promise { const appSecret = await this.getAppSecret(organizationId); if (!appSecret) { - logger.warn("[WhatsAppAutomation] No app secret configured", { organizationId }); + logger.warn("[WhatsAppAutomation] No app secret configured", { + organizationId, + }); return false; } return verifyWhatsAppSignature(appSecret, signatureHeader, rawBody); @@ -385,12 +417,19 @@ class WhatsAppAutomationService { connected: false, configured: false, }; - await cache.set(this.getStatusCacheKey(organizationId), status, STATUS_CACHE_TTL_SECONDS); + await cache.set( + this.getStatusCacheKey(organizationId), + status, + STATUS_CACHE_TTL_SECONDS, + ); return status; } // Validate the access token is still working - const validation = await this.validateAccessToken(accessToken, phoneNumberId); + const validation = await this.validateAccessToken( + accessToken, + phoneNumberId, + ); if (validation.valid) { const status: WhatsAppConnectionStatus = { @@ -398,7 +437,11 @@ class WhatsAppAutomationService { configured: true, businessPhone: businessPhone || validation.phoneDisplay || undefined, }; - await cache.set(this.getStatusCacheKey(organizationId), status, STATUS_CACHE_TTL_SECONDS); + await cache.set( + this.getStatusCacheKey(organizationId), + status, + STATUS_CACHE_TTL_SECONDS, + ); return status; } @@ -407,9 +450,14 @@ class WhatsAppAutomationService { connected: false, configured: true, businessPhone: businessPhone || undefined, - error: validation.error || "Access token may be invalid. Try reconnecting.", + error: + validation.error || "Access token may be invalid. Try reconnecting.", }; - await cache.set(this.getStatusCacheKey(organizationId), status, STATUS_ERROR_CACHE_TTL_SECONDS); + await cache.set( + this.getStatusCacheKey(organizationId), + status, + STATUS_ERROR_CACHE_TTL_SECONDS, + ); return status; } @@ -442,7 +490,12 @@ class WhatsAppAutomationService { } try { - const response = await sendWhatsAppMessage(accessToken, phoneNumberId, to, text); + const response = await sendWhatsAppMessage( + accessToken, + phoneNumberId, + to, + text, + ); const messageId = response.messages?.[0]?.id; logger.info("[WhatsAppAutomation] Message sent", { diff --git a/packages/lib/services/x402-facilitator.ts b/packages/lib/services/x402-facilitator.ts index 837634c6d..1885fdb0a 100644 --- a/packages/lib/services/x402-facilitator.ts +++ b/packages/lib/services/x402-facilitator.ts @@ -15,9 +15,22 @@ * (encrypted with KMS) — never stored as a plain environment variable in production. */ -import { type Chain, createPublicClient, type Hex, http, type PublicClient } from "viem"; +import { + type Chain, + createPublicClient, + type Hex, + http, + type PublicClient, +} from "viem"; import { type PrivateKeyAccount, privateKeyToAccount } from "viem/accounts"; -import { base, baseSepolia, bsc, bscTestnet, mainnet, sepolia } from "viem/chains"; +import { + base, + baseSepolia, + bsc, + bscTestnet, + mainnet, + sepolia, +} from "viem/chains"; import { logger } from "@/lib/utils/logger"; // Types @@ -216,13 +229,17 @@ class X402FacilitatorService { const privateKey = await this.loadFacilitatorKey(); if (!privateKey) { - logger.warn("[x402-facilitator] No facilitator private key configured. Service disabled."); + logger.warn( + "[x402-facilitator] No facilitator private key configured. Service disabled.", + ); return; } try { this.account = privateKeyToAccount(privateKey as Hex); - logger.info(`[x402-facilitator] Signer initialized: ${this.account.address}`); + logger.info( + `[x402-facilitator] Signer initialized: ${this.account.address}`, + ); } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger.error(`[x402-facilitator] Failed to initialize signer: ${msg}`); @@ -233,7 +250,9 @@ class X402FacilitatorService { this.networks = buildNetworkRegistry(); const enabledStr = - process.env.X402_NETWORKS ?? process.env.EVM_NETWORKS ?? "base-sepolia,base,bsc,bsc-testnet"; + process.env.X402_NETWORKS ?? + process.env.EVM_NETWORKS ?? + "base-sepolia,base,bsc,bsc-testnet"; const enabledNames = enabledStr.split(",").map((n) => n.trim()); for (const [caip2, config] of Object.entries(this.networks)) { @@ -249,7 +268,9 @@ class X402FacilitatorService { } } - logger.info(`[x402-facilitator] Enabled networks: ${this.enabledNetworks.join(", ")}`); + logger.info( + `[x402-facilitator] Enabled networks: ${this.enabledNetworks.join(", ")}`, + ); this.initialized = true; } @@ -340,7 +361,9 @@ class X402FacilitatorService { } // 4. Validate payTo - if (accepted.payTo.toLowerCase() !== paymentRequirements.payTo.toLowerCase()) { + if ( + accepted.payTo.toLowerCase() !== paymentRequirements.payTo.toLowerCase() + ) { return { isValid: false, invalidReason: "payto_mismatch", @@ -524,7 +547,9 @@ class X402FacilitatorService { }); // Parse v, r, s from the compact signature - const sigHex = signature.startsWith("0x") ? signature.slice(2) : signature; + const sigHex = signature.startsWith("0x") + ? signature.slice(2) + : signature; const r = `0x${sigHex.slice(0, 64)}` as Hex; const s = `0x${sigHex.slice(64, 128)}` as Hex; const v = parseInt(sigHex.slice(128, 130), 16); @@ -586,7 +611,11 @@ class X402FacilitatorService { address: networkConfig.usdcAddress, abi: transferFromAbi, functionName: "transferFrom", - args: [authorization.from as Hex, authorization.to as Hex, BigInt(authorization.value)], + args: [ + authorization.from as Hex, + authorization.to as Hex, + BigInt(authorization.value), + ], }); } else { // EIP-3009: transferWithAuthorization() @@ -679,9 +708,13 @@ class X402FacilitatorService { const { secretsService } = await import("@/lib/services/secrets"); if (secretsService) { // Try org-level facilitator key - const key = await secretsService.get("system", "FACILITATOR_PRIVATE_KEY").catch(() => null); + const key = await secretsService + .get("system", "FACILITATOR_PRIVATE_KEY") + .catch(() => null); if (key) { - logger.info("[x402-facilitator] Loaded key from secrets service (encrypted)"); + logger.info( + "[x402-facilitator] Loaded key from secrets service (encrypted)", + ); return key; } } @@ -690,7 +723,9 @@ class X402FacilitatorService { } // Fallback to environment variable (development) - const envKey = process.env.FACILITATOR_PRIVATE_KEY ?? process.env.X402_FACILITATOR_PRIVATE_KEY; + const envKey = + process.env.FACILITATOR_PRIVATE_KEY ?? + process.env.X402_FACILITATOR_PRIVATE_KEY; if (envKey) { if (process.env.NODE_ENV === "production") { diff --git a/packages/lib/session/session.ts b/packages/lib/session/session.ts index 1c4e3ae9c..86791f6bf 100644 --- a/packages/lib/session/session.ts +++ b/packages/lib/session/session.ts @@ -179,21 +179,29 @@ export async function getOrCreateSessionUser( null; if (providedToken) { - logger.debug(`${logPrefix} Checking provided token:`, providedToken.slice(0, 8) + "..."); + logger.debug( + `${logPrefix} Checking provided token:`, + providedToken.slice(0, 8) + "...", + ); const session = await anonymousSessionsService.getByToken(providedToken); if (session) { - const sessionUser = await usersService.getWithOrganization(session.user_id); + const sessionUser = await usersService.getWithOrganization( + session.user_id, + ); if (sessionUser) { // Check identity table for anonymous status const identity = await db.query.userIdentities.findFirst({ where: eq(userIdentities.user_id, sessionUser.id), }); if (identity?.is_anonymous) { - logger.info(`${logPrefix} Valid anonymous session from provided token`, { - userId: sessionUser.id, - messageCount: session.message_count, - }); + logger.info( + `${logPrefix} Valid anonymous session from provided token`, + { + userId: sessionUser.id, + messageCount: session.message_count, + }, + ); return buildAnonymousSessionUser(sessionUser, session, providedToken); } @@ -203,13 +211,19 @@ export async function getOrCreateSessionUser( } // Step 3: Try session cookie - const cookieToken = tokenSources?.cookie || (await getSessionTokenFromCookie()); + const cookieToken = + tokenSources?.cookie || (await getSessionTokenFromCookie()); if (cookieToken) { - logger.debug(`${logPrefix} Checking cookie token:`, cookieToken.slice(0, 8) + "..."); + logger.debug( + `${logPrefix} Checking cookie token:`, + cookieToken.slice(0, 8) + "...", + ); const session = await anonymousSessionsService.getByToken(cookieToken); if (session) { - const sessionUser = await usersService.getWithOrganization(session.user_id); + const sessionUser = await usersService.getWithOrganization( + session.user_id, + ); if (sessionUser) { const identity = await db.query.userIdentities.findFirst({ where: eq(userIdentities.user_id, sessionUser.id), @@ -255,7 +269,10 @@ async function buildAnonymousSessionUser( sessionToken: token, messageCount: session.message_count, messagesLimit: session.messages_limit, - messagesRemaining: Math.max(0, session.messages_limit - session.message_count), + messagesRemaining: Math.max( + 0, + session.messages_limit - session.message_count, + ), metadata: { ipAddress: session.ip_address || undefined, userAgent: session.user_agent || undefined, @@ -270,7 +287,9 @@ async function buildAnonymousSessionUser( async function createNewAnonymousSession(): Promise { const sessionToken = nanoid(32); - const expiresAt = new Date(Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000); + const expiresAt = new Date( + Date.now() + ANON_SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000, + ); const clientInfo = await getClientInfo(); // NOTE: IP-based anonymous-session abuse checks intentionally removed. @@ -337,7 +356,9 @@ async function createNewAnonymousSession(): Promise { /** * Increment message count for a session user */ -export async function incrementSessionMessageCount(sessionUser: SessionUser): Promise<{ +export async function incrementSessionMessageCount( + sessionUser: SessionUser, +): Promise<{ allowed: boolean; newCount: number; remaining: number; @@ -362,7 +383,9 @@ export async function incrementSessionMessageCount(sessionUser: SessionUser): Pr }; } - const rateLimitResult = await anonymousSessionsService.checkRateLimit(session.id); + const rateLimitResult = await anonymousSessionsService.checkRateLimit( + session.id, + ); if (!rateLimitResult.allowed) { return { allowed: false, @@ -372,7 +395,9 @@ export async function incrementSessionMessageCount(sessionUser: SessionUser): Pr }; } - const updatedSession = await anonymousSessionsService.incrementMessageCount(session.id); + const updatedSession = await anonymousSessionsService.incrementMessageCount( + session.id, + ); logger.debug("[Session] Incremented message count", { sessionId: session.id, @@ -383,7 +408,10 @@ export async function incrementSessionMessageCount(sessionUser: SessionUser): Pr return { allowed: true, newCount: updatedSession.message_count, - remaining: Math.max(0, session.messages_limit - updatedSession.message_count), + remaining: Math.max( + 0, + session.messages_limit - updatedSession.message_count, + ), }; } @@ -427,7 +455,12 @@ export async function migrateAnonymousSession( .select() .from(users) .innerJoin(userIdentities, eq(users.id, userIdentities.user_id)) - .where(and(eq(users.id, anonymousUserId), eq(userIdentities.is_anonymous, true))) + .where( + and( + eq(users.id, anonymousUserId), + eq(userIdentities.is_anonymous, true), + ), + ) .limit(1) .then((rows) => rows.map((r) => r.users)); @@ -544,7 +577,9 @@ export async function migrateAnonymousSession( mergedData.charactersTransferred = charResult.length; } else { if (!realUser.organization_id) { - throw new Error(`Cannot migrate to user ${realUser.id} without organization`); + throw new Error( + `Cannot migrate to user ${realUser.id} without organization`, + ); } targetUserId = realUser.id; @@ -672,7 +707,8 @@ export function shouldPromptSignup(sessionUser: SessionUser): { if (sessionUser.metadata.expiresAt) { const hoursRemaining = - (sessionUser.metadata.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60); + (sessionUser.metadata.expiresAt.getTime() - Date.now()) / + (1000 * 60 * 60); if (hoursRemaining < 24) { return { shouldPrompt: true, reason: "session_expiring" }; } @@ -684,7 +720,9 @@ export function shouldPromptSignup(sessionUser: SessionUser): { /** * Get session summary for debugging */ -export function getSessionDebugInfo(sessionUser: SessionUser): Record { +export function getSessionDebugInfo( + sessionUser: SessionUser, +): Record { return { userId: sessionUser.userId, isAnonymous: sessionUser.isAnonymous, diff --git a/packages/lib/steward-sync.ts b/packages/lib/steward-sync.ts index cd5ee3f56..a7fdfad04 100644 --- a/packages/lib/steward-sync.ts +++ b/packages/lib/steward-sync.ts @@ -36,7 +36,8 @@ const getInitialCredits = (): number => { return DEFAULT_INITIAL_CREDITS; }; -const STEWARD_IDENTITY_UNIQUE_CONSTRAINT = "user_identities_steward_user_id_unique"; +const STEWARD_IDENTITY_UNIQUE_CONSTRAINT = + "user_identities_steward_user_id_unique"; function extractErrorMetadata(candidate: unknown): { code?: string; @@ -56,12 +57,20 @@ function extractErrorMetadata(candidate: unknown): { }; return { - code: typeof typedCandidate.code === "string" ? typedCandidate.code : undefined, + code: + typeof typedCandidate.code === "string" ? typedCandidate.code : undefined, constraint: - typeof typedCandidate.constraint === "string" ? typedCandidate.constraint : undefined, - detail: typeof typedCandidate.detail === "string" ? typedCandidate.detail : undefined, + typeof typedCandidate.constraint === "string" + ? typedCandidate.constraint + : undefined, + detail: + typeof typedCandidate.detail === "string" + ? typedCandidate.detail + : undefined, message: - typeof typedCandidate.message === "string" ? typedCandidate.message : String(candidate), + typeof typedCandidate.message === "string" + ? typedCandidate.message + : String(candidate), }; } @@ -71,8 +80,10 @@ function isRecoverableStewardProjectionConflict(error: unknown): boolean { } const errorMetadata = extractErrorMetadata(error); - const causeMetadata = "cause" in error ? extractErrorMetadata(error.cause) : { message: "" }; - const isUniqueViolation = errorMetadata.code === "23505" || causeMetadata.code === "23505"; + const causeMetadata = + "cause" in error ? extractErrorMetadata(error.cause) : { message: "" }; + const isUniqueViolation = + errorMetadata.code === "23505" || causeMetadata.code === "23505"; const hasExactStewardConstraint = errorMetadata.constraint === STEWARD_IDENTITY_UNIQUE_CONSTRAINT || causeMetadata.constraint === STEWARD_IDENTITY_UNIQUE_CONSTRAINT; @@ -90,7 +101,8 @@ async function recoverCanonicalStewardUser( return false; } - const projection = await usersService.getStewardIdentityForWrite(stewardUserId); + const projection = + await usersService.getStewardIdentityForWrite(stewardUserId); if (!projection || projection.user_id !== expectedUserId) { return false; } @@ -100,12 +112,15 @@ async function recoverCanonicalStewardUser( return false; } - logger.warn("[StewardSync] Recovered from stale Steward identity projection conflict", { - context, - expectedUserId, - stewardUserId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[StewardSync] Recovered from stale Steward identity projection conflict", + { + context, + expectedUserId, + stewardUserId, + error: error instanceof Error ? error.message : String(error), + }, + ); return true; } @@ -121,8 +136,14 @@ async function rollbackCreatedUserSafely( logger.error("[StewardSync] Failed to roll back newly created user", { context, userId, - originalError: originalError instanceof Error ? originalError.message : String(originalError), - rollbackError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), + originalError: + originalError instanceof Error + ? originalError.message + : String(originalError), + rollbackError: + rollbackError instanceof Error + ? rollbackError.message + : String(rollbackError), }); } } @@ -141,8 +162,14 @@ async function restorePreviousStewardUserIdSafely( logger.error("[StewardSync] Failed to restore previous Steward user ID", { userId, previousStewardUserId, - originalError: originalError instanceof Error ? originalError.message : String(originalError), - rollbackError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), + originalError: + originalError instanceof Error + ? originalError.message + : String(originalError), + rollbackError: + rollbackError instanceof Error + ? rollbackError.message + : String(rollbackError), }); } } @@ -213,11 +240,14 @@ export async function syncUserFromSteward( try { await usersService.upsertStewardIdentity(user.id, stewardUserId); } catch (error) { - logger.warn("[StewardSync] Failed to repair Steward identity projection for existing user", { - userId: user.id, - stewardUserId, - error: error instanceof Error ? error.message : String(error), - }); + logger.warn( + "[StewardSync] Failed to repair Steward identity projection for existing user", + { + userId: user.id, + stewardUserId, + error: error instanceof Error ? error.message : String(error), + }, + ); } // Update user fields if anything changed @@ -269,7 +299,12 @@ export async function syncUserFromSteward( } catch (error) { const recovered = newUser && - (await recoverCanonicalStewardUser(newUser.id, stewardUserId, "invite", error)); + (await recoverCanonicalStewardUser( + newUser.id, + stewardUserId, + "invite", + error, + )); if (newUser && !recovered) { await rollbackCreatedUserSafely(newUser.id, "invite", error); @@ -279,7 +314,8 @@ export async function syncUserFromSteward( } } - const userWithOrg = await usersService.getByStewardIdForWrite(stewardUserId); + const userWithOrg = + await usersService.getByStewardIdForWrite(stewardUserId); if (!userWithOrg) { throw new Error( @@ -287,7 +323,10 @@ export async function syncUserFromSteward( ); } - await organizationInvitesRepository.markAsAccepted(pendingInvite.id, userWithOrg.id); + await organizationInvitesRepository.markAsAccepted( + pendingInvite.id, + userWithOrg.id, + ); // Log to Discord (fire-and-forget) discordService @@ -312,7 +351,8 @@ export async function syncUserFromSteward( // ── 3. Email already taken (account linking) ───────────────────────── if (email) { - const existingByEmail = await usersService.getByEmailWithOrganization(email); + const existingByEmail = + await usersService.getByEmailWithOrganization(email); if (existingByEmail && existingByEmail.steward_user_id !== stewardUserId) { logger.info( @@ -326,15 +366,25 @@ export async function syncUserFromSteward( }); try { - await usersService.upsertStewardIdentity(existingByEmail.id, stewardUserId); + await usersService.upsertStewardIdentity( + existingByEmail.id, + stewardUserId, + ); } catch (error) { - await restorePreviousStewardUserIdSafely(existingByEmail.id, previousStewardUserId, error); + await restorePreviousStewardUserIdSafely( + existingByEmail.id, + previousStewardUserId, + error, + ); throw error; } - const linkedUser = await usersService.getByStewardIdForWrite(stewardUserId); + const linkedUser = + await usersService.getByStewardIdForWrite(stewardUserId); if (!linkedUser) { - throw new Error(`Failed to fetch user after Steward account linking for ${email}`); + throw new Error( + `Failed to fetch user after Steward account linking for ${email}`, + ); } return linkedUser; } @@ -354,7 +404,9 @@ export async function syncUserFromSteward( const timestamp = Date.now().toString(36).slice(-4); orgSlug = `${sanitized}-${timestamp}${random}`; } else { - throw new Error(`Cannot generate organization slug for Steward user ${stewardUserId}`); + throw new Error( + `Cannot generate organization slug for Steward user ${stewardUserId}`, + ); } // Ensure slug uniqueness @@ -366,7 +418,9 @@ export async function syncUserFromSteward( `Failed to generate unique organization slug for Steward user ${stewardUserId}`, ); } - orgSlug = email ? generateSlugFromEmail(email) : generateSlugFromWallet(walletAddress!); + orgSlug = email + ? generateSlugFromEmail(email) + : generateSlugFromWallet(walletAddress!); } // Create organization with zero balance initially @@ -431,7 +485,9 @@ export async function syncUserFromSteward( for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { - await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** (attempt - 1))); + await new Promise((resolve) => + setTimeout(resolve, 50 * 2 ** (attempt - 1)), + ); } existingUser = await usersService.getByStewardIdForWrite(stewardUserId); @@ -452,7 +508,10 @@ export async function syncUserFromSteward( updated_at: new Date(), }); try { - await usersService.upsertStewardIdentity(existingUser.id, stewardUserId); + await usersService.upsertStewardIdentity( + existingUser.id, + stewardUserId, + ); } catch (upsertError) { await usersService.update(existingUser.id, { steward_user_id: previousStewardUserId, @@ -461,9 +520,12 @@ export async function syncUserFromSteward( throw upsertError; } await organizationsService.delete(organization.id); - const linkedUser = await usersService.getByStewardIdForWrite(stewardUserId); + const linkedUser = + await usersService.getByStewardIdForWrite(stewardUserId); if (!linkedUser) { - throw new Error(`Failed to fetch user after Steward account linking for ${email}`); + throw new Error( + `Failed to fetch user after Steward account linking for ${email}`, + ); } return linkedUser; } @@ -515,7 +577,9 @@ export async function syncUserFromSteward( const userWithOrg = await usersService.getByStewardIdForWrite(stewardUserId); if (!userWithOrg) { - throw new Error(`Failed to fetch newly created Steward user ${stewardUserId}`); + throw new Error( + `Failed to fetch newly created Steward user ${stewardUserId}`, + ); } // Send welcome email (fire-and-forget) @@ -558,7 +622,10 @@ export async function syncUserFromSteward( void ensureUserHasApiKey(userWithOrg.id, userWithOrg.organization?.id || ""); // Auto-create default Eliza character (fire-and-forget) - void ensureDefaultCharacter(userWithOrg.id, userWithOrg.organization?.id || ""); + void ensureDefaultCharacter( + userWithOrg.id, + userWithOrg.organization?.id || "", + ); return userWithOrg; } @@ -566,14 +633,20 @@ export async function syncUserFromSteward( /** * Ensures a user has a default API key for programmatic access. */ -async function ensureUserHasApiKey(userId: string, organizationId: string): Promise { +async function ensureUserHasApiKey( + userId: string, + organizationId: string, +): Promise { if (!userId?.trim() || !organizationId?.trim()) { - logger.warn("[StewardSync] Invalid userId or organizationId, skipping API key creation"); + logger.warn( + "[StewardSync] Invalid userId or organizationId, skipping API key creation", + ); return; } try { - const existingKeys = await apiKeysService.listByOrganization(organizationId); + const existingKeys = + await apiKeysService.listByOrganization(organizationId); if (existingKeys.some((key) => key.user_id === userId)) { return; } @@ -595,9 +668,14 @@ async function ensureUserHasApiKey(userId: string, organizationId: string): Prom /** * Ensures a new account starts with a default Eliza character. */ -async function ensureDefaultCharacter(userId: string, organizationId: string): Promise { +async function ensureDefaultCharacter( + userId: string, + organizationId: string, +): Promise { if (!userId?.trim() || !organizationId?.trim()) { - logger.warn("[StewardSync] Invalid userId or organizationId, skipping default character"); + logger.warn( + "[StewardSync] Invalid userId or organizationId, skipping default character", + ); return; } @@ -614,7 +692,9 @@ async function ensureDefaultCharacter(userId: string, organizationId: string): P organization_id: organizationId, }); - logger.info(`[StewardSync] Created default Eliza character for user ${userId}`); + logger.info( + `[StewardSync] Created default Eliza character for user ${userId}`, + ); } catch (error) { logger.error("[StewardSync] Error creating default character", { userId, diff --git a/packages/lib/stores/chat-store.ts b/packages/lib/stores/chat-store.ts index ca6c87bb8..18e9513c0 100644 --- a/packages/lib/stores/chat-store.ts +++ b/packages/lib/stores/chat-store.ts @@ -52,7 +52,9 @@ function computeViewerState( } // Check if user owns the selected character - const selectedChar = availableCharacters.find((c) => c.id === selectedCharacterId); + const selectedChar = availableCharacters.find( + (c) => c.id === selectedCharacterId, + ); // Only treat as owner if ownerId explicitly matches - missing ownerId means unknown ownership if (selectedChar?.ownerId === currentUserId) { return "owner"; @@ -131,7 +133,8 @@ export const useChatStore = create((set, get) => ({ } }, setIsLoadingRooms: (isLoading) => set({ isLoadingRooms: isLoading }), - setAvailableCharacters: (characters) => set({ availableCharacters: characters }), + setAvailableCharacters: (characters) => + set({ availableCharacters: characters }), setSelectedCharacterId: (characterId) => { const { isAuthenticated, currentUserId, availableCharacters } = get(); @@ -165,7 +168,12 @@ export const useChatStore = create((set, get) => ({ // Atomically initialize auth state, characters, and selection together // Prevents race conditions when all three need to be set during page initialization - initializeState: ({ isAuthenticated, userId, characters, selectedCharacterId }) => { + initializeState: ({ + isAuthenticated, + userId, + characters, + selectedCharacterId, + }) => { // Compute viewer state with all the new values at once const viewerState = computeViewerState( isAuthenticated, @@ -186,7 +194,9 @@ export const useChatStore = create((set, get) => ({ // Update an existing room's properties (for instant UI updates) updateRoom: (roomId: string, updates: Partial>) => { const { rooms } = get(); - const updatedRooms = rooms.map((room) => (room.id === roomId ? { ...room, ...updates } : room)); + const updatedRooms = rooms.map((room) => + room.id === roomId ? { ...room, ...updates } : room, + ); set({ rooms: updatedRooms }); }, @@ -231,19 +241,23 @@ export const useChatStore = create((set, get) => ({ const data = await res.json(); if (Array.isArray(data.rooms)) { - const roomItems: RoomItem[] = data.rooms.slice(0, 20).map((r: RoomPreview) => ({ - id: r.id, - characterId: r.characterId, - characterName: r.characterName, - lastText: r.lastText, - lastTime: r.lastTime, - title: r.title, - isLocked: r.isLocked, - isBuildRoom: r.isBuildRoom, - })); + const roomItems: RoomItem[] = data.rooms + .slice(0, 20) + .map((r: RoomPreview) => ({ + id: r.id, + characterId: r.characterId, + characterName: r.characterName, + lastText: r.lastText, + lastTime: r.lastTime, + title: r.title, + isLocked: r.isLocked, + isBuildRoom: r.isBuildRoom, + })); const currentState = get(); - const existingCharacterIds = new Set(currentState.availableCharacters.map((c) => c.id)); + const existingCharacterIds = new Set( + currentState.availableCharacters.map((c) => c.id), + ); const charactersFromRooms: Character[] = []; for (const room of data.rooms as RoomPreview[]) { @@ -261,7 +275,10 @@ export const useChatStore = create((set, get) => ({ } } - const mergedCharacters = [...currentState.availableCharacters, ...charactersFromRooms]; + const mergedCharacters = [ + ...currentState.availableCharacters, + ...charactersFromRooms, + ]; let newSelectedCharacterId = currentState.selectedCharacterId; if ( @@ -340,7 +357,9 @@ export const useChatStore = create((set, get) => ({ const newRoomId = data.roomId; if (!newRoomId) { - throw new Error("Room creation succeeded but returned empty ID. Check server logs."); + throw new Error( + "Room creation succeeded but returned empty ID. Check server logs.", + ); } // Automatically switch to the new room @@ -350,7 +369,9 @@ export const useChatStore = create((set, get) => ({ return newRoomId; } else { const errorData = await response.json(); - throw new Error(errorData.error || `Failed to create room: ${response.status}`); + throw new Error( + errorData.error || `Failed to create room: ${response.status}`, + ); } }, @@ -387,7 +408,9 @@ export const useChatStore = create((set, get) => ({ if (!response.ok) { // Log error but don't rollback - the server delete often succeeds // even when returning errors due to cascade operations - console.warn("[ChatStore] Delete API returned error, but room may have been deleted"); + console.warn( + "[ChatStore] Delete API returned error, but room may have been deleted", + ); } // Always clean up the deleted set after a delay setTimeout(() => { diff --git a/packages/lib/stripe.ts b/packages/lib/stripe.ts index f99b2050a..045257a19 100644 --- a/packages/lib/stripe.ts +++ b/packages/lib/stripe.ts @@ -39,7 +39,9 @@ function initStripe(): Stripe | null { const secretKey = process.env.STRIPE_SECRET_KEY?.trim(); if (!secretKey) { - stripeInitError = new Error("STRIPE_SECRET_KEY is not set in environment variables"); + stripeInitError = new Error( + "STRIPE_SECRET_KEY is not set in environment variables", + ); return null; } @@ -68,7 +70,10 @@ function initStripe(): Stripe | null { export function getStripe(): Stripe { const instance = initStripe(); if (!instance) { - throw stripeInitError || new Error("STRIPE_SECRET_KEY is not set in environment variables"); + throw ( + stripeInitError || + new Error("STRIPE_SECRET_KEY is not set in environment variables") + ); } return instance; } diff --git a/packages/lib/stubs/walletconnect-logger.ts b/packages/lib/stubs/walletconnect-logger.ts index 649459faa..f83a14e6b 100644 --- a/packages/lib/stubs/walletconnect-logger.ts +++ b/packages/lib/stubs/walletconnect-logger.ts @@ -28,7 +28,10 @@ const LOG_LEVELS: Record = { fatal: 60, }; -function createLogger(level: LogLevel = "info", _bindings?: Record): Logger { +function createLogger( + level: LogLevel = "info", + _bindings?: Record, +): Logger { const minLevel = LOG_LEVELS[level] || LOG_LEVELS.info; const log = @@ -46,7 +49,8 @@ function createLogger(level: LogLevel = "info", _bindings?: Record) => createLogger(level, bindings), + child: (bindings?: Record) => + createLogger(level, bindings), }; } diff --git a/packages/lib/swagger/endpoint-discovery.ts b/packages/lib/swagger/endpoint-discovery.ts index bc7ffbe43..0061b5ec1 100644 --- a/packages/lib/swagger/endpoint-discovery.ts +++ b/packages/lib/swagger/endpoint-discovery.ts @@ -95,7 +95,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "POST", category: "Image Generation", name: "Generate Image", - description: "Generate images from text prompts using AI models (supports API key auth)", + description: + "Generate images from text prompts using AI models (supports API key auth)", requiresAuth: true, pricing: { cost: 0.01, @@ -177,7 +178,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "POST", category: "AI Completions", name: "Chat Completion", - description: "Generate text completions using Vercel AI SDK format (supports API key auth)", + description: + "Generate text completions using Vercel AI SDK format (supports API key auth)", requiresAuth: true, pricing: { cost: 0.0025, @@ -192,9 +194,12 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ name: "messages", type: "array", required: true, - description: "Array of UIMessage objects (Vercel AI SDK format with role and parts)", - defaultValue: '[{"role":"user","parts":[{"type":"text","text":"Hello, how are you?"}]}]', - example: '[{"role":"user","parts":[{"type":"text","text":"Explain quantum computing"}]}]', + description: + "Array of UIMessage objects (Vercel AI SDK format with role and parts)", + defaultValue: + '[{"role":"user","parts":[{"type":"text","text":"Hello, how are you?"}]}]', + example: + '[{"role":"user","parts":[{"type":"text","text":"Explain quantum computing"}]}]', }, { name: "id", @@ -222,7 +227,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "POST", category: "AI Completions", name: "Character Assistant", - description: "AI assistant for creating character definitions (session auth only)", + description: + "AI assistant for creating character definitions (session auth only)", requiresAuth: true, pricing: { cost: 0.0025, @@ -240,7 +246,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ description: "Conversation messages (Vercel AI SDK UIMessage format)", defaultValue: '[{"role":"user","parts":[{"type":"text","text":"Help me create a character"}]}]', - example: '[{"role":"user","parts":[{"type":"text","text":"Create a sci-fi character"}]}]', + example: + '[{"role":"user","parts":[{"type":"text","text":"Create a sci-fi character"}]}]', }, { name: "characterData", @@ -268,7 +275,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "POST", category: "AI Completions", name: "Generate Prompts", - description: "Generate creative prompts for image/video generation (session auth only)", + description: + "Generate creative prompts for image/video generation (session auth only)", requiresAuth: true, pricing: { cost: 0, @@ -281,7 +289,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ name: "seed", type: "number", required: false, - description: "Seed for prompt generation (optional, auto-generated if not provided)", + description: + "Seed for prompt generation (optional, auto-generated if not provided)", example: 1234567890, }, ], @@ -326,7 +335,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "GET", category: "Gallery", name: "List Generations", - description: "List all media generations (images and videos) (supports API key auth)", + description: + "List all media generations (images and videos) (supports API key auth)", requiresAuth: true, pricing: { cost: 0, @@ -402,7 +412,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "PATCH", category: "User Management", name: "Update User Profile", - description: "Update user profile information (session auth only - won't work with API key)", + description: + "Update user profile information (session auth only - won't work with API key)", requiresAuth: true, pricing: { cost: 0, @@ -468,7 +479,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "POST", category: "API Keys", name: "Create API Key", - description: "Create a new API key (session auth only - won't work with API key)", + description: + "Create a new API key (session auth only - won't work with API key)", requiresAuth: true, pricing: { cost: 0, @@ -526,7 +538,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "DELETE", category: "API Keys", name: "Delete API Key", - description: "Delete an API key (session auth only - won't work with API key)", + description: + "Delete an API key (session auth only - won't work with API key)", requiresAuth: true, pricing: { cost: 0, @@ -560,7 +573,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ method: "PATCH", category: "API Keys", name: "Update API Key", - description: "Update API key properties (session auth only - won't work with API key)", + description: + "Update API key properties (session auth only - won't work with API key)", requiresAuth: true, pricing: { cost: 0, @@ -670,7 +684,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ name: "voiceId", type: "string", required: false, - description: "ElevenLabs voice ID (use your custom cloned voice ID or default voice)", + description: + "ElevenLabs voice ID (use your custom cloned voice ID or default voice)", example: "21m00Tcm4TlvDq8ikWAM", }, { @@ -692,7 +707,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ responses: [ { statusCode: 200, - description: "Audio stream generated successfully (Content-Type: audio/mpeg)", + description: + "Audio stream generated successfully (Content-Type: audio/mpeg)", }, { statusCode: 400, @@ -741,7 +757,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ name: "languageCode", type: "string", required: false, - description: "ISO 639-1 language code for transcription (auto-detect if not provided)", + description: + "ISO 639-1 language code for transcription (auto-detect if not provided)", example: "en", }, ], @@ -875,7 +892,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ name: "file1", type: "string", required: false, - description: "Additional audio sample (optional, multipart/form-data)", + description: + "Additional audio sample (optional, multipart/form-data)", format: "binary", example: "sample2.mp3", }, @@ -914,7 +932,8 @@ export const API_ENDPOINTS: ApiEndpoint[] = [ }, { statusCode: 400, - description: "Invalid request (missing fields, too many files, file too large)", + description: + "Invalid request (missing fields, too many files, file too large)", }, { statusCode: 402, diff --git a/packages/lib/swagger/openapi-generator.ts b/packages/lib/swagger/openapi-generator.ts index 7f11849ad..9f4606a41 100644 --- a/packages/lib/swagger/openapi-generator.ts +++ b/packages/lib/swagger/openapi-generator.ts @@ -182,7 +182,9 @@ function convertParametersToOpenAPI( * @param params - Array of endpoint parameters. * @returns OpenAPI request body definition. */ -function createRequestBodyFromParameters(params: EndpointParameter[]): OpenAPIRequestBody { +function createRequestBodyFromParameters( + params: EndpointParameter[], +): OpenAPIRequestBody { const properties: Record = {}; const required: string[] = []; @@ -214,7 +216,9 @@ function createRequestBodyFromParameters(params: EndpointParameter[]): OpenAPIRe * @param responses - Array of endpoint responses. * @returns Record of status codes to OpenAPI responses. */ -function convertResponsesToOpenAPI(responses: EndpointResponse[]): Record { +function convertResponsesToOpenAPI( + responses: EndpointResponse[], +): Record { const result: Record = {}; for (const response of responses) { @@ -259,15 +263,21 @@ function convertEndpointToOperation(endpoint: ApiEndpoint): OpenAPIOperation { const parameters: OpenAPIParameter[] = []; if (endpoint.parameters?.path) { - parameters.push(...convertParametersToOpenAPI(endpoint.parameters.path, "path")); + parameters.push( + ...convertParametersToOpenAPI(endpoint.parameters.path, "path"), + ); } if (endpoint.parameters?.query) { - parameters.push(...convertParametersToOpenAPI(endpoint.parameters.query, "query")); + parameters.push( + ...convertParametersToOpenAPI(endpoint.parameters.query, "query"), + ); } if (endpoint.parameters?.headers) { - parameters.push(...convertParametersToOpenAPI(endpoint.parameters.headers, "header")); + parameters.push( + ...convertParametersToOpenAPI(endpoint.parameters.headers, "header"), + ); } if (parameters.length > 0) { @@ -275,7 +285,9 @@ function convertEndpointToOperation(endpoint: ApiEndpoint): OpenAPIOperation { } if (endpoint.parameters?.body) { - operation.requestBody = createRequestBodyFromParameters(endpoint.parameters.body); + operation.requestBody = createRequestBodyFromParameters( + endpoint.parameters.body, + ); } return operation; @@ -378,7 +390,12 @@ function convertToYAML(obj: unknown, indent = 0): string { if (Array.isArray(obj)) { if (obj.length === 0) return "[]"; - return "\n" + obj.map((item) => `${spaces}- ${convertToYAML(item, indent + 1)}`).join("\n"); + return ( + "\n" + + obj + .map((item) => `${spaces}- ${convertToYAML(item, indent + 1)}`) + .join("\n") + ); } if (typeof obj === "object") { @@ -390,7 +407,10 @@ function convertToYAML(obj: unknown, indent = 0): string { entries .map(([key, value]) => { const yamlValue = convertToYAML(value, indent + 1); - if (yamlValue.startsWith("\n") || (typeof value === "object" && value !== null)) { + if ( + yamlValue.startsWith("\n") || + (typeof value === "object" && value !== null) + ) { return `${spaces}${key}:${yamlValue}`; } return `${spaces}${key}: ${yamlValue}`; @@ -403,7 +423,10 @@ function convertToYAML(obj: unknown, indent = 0): string { } export function downloadOpenAPISpec(format: "json" | "yaml", baseUrl?: string) { - const content = format === "json" ? generateOpenAPIJSON(baseUrl) : generateOpenAPIYAML(baseUrl); + const content = + format === "json" + ? generateOpenAPIJSON(baseUrl) + : generateOpenAPIYAML(baseUrl); const blob = new Blob([content], { type: format === "json" ? "application/json" : "text/yaml", diff --git a/packages/lib/types.ts b/packages/lib/types.ts index bfeb9ac9b..db6217e42 100644 --- a/packages/lib/types.ts +++ b/packages/lib/types.ts @@ -109,7 +109,9 @@ export interface UsageMetadata { /** * Template type for dynamic content generation. */ -export type TemplateType = string | ((options: { state: Record }) => string); +export type TemplateType = + | string + | ((options: { state: Record }) => string); /** * Character definition for Eliza AI agents. @@ -139,7 +141,10 @@ export interface ElizaCharacter { knowledge?: (string | { path: string; shared?: boolean })[]; plugins?: string[]; avatarUrl?: string; - settings?: Record>; + settings?: Record< + string, + string | boolean | number | Record + >; secrets?: Record; style?: { all?: string[]; diff --git a/packages/lib/types/a2a.ts b/packages/lib/types/a2a.ts index 31844a4e9..2b01beb15 100644 --- a/packages/lib/types/a2a.ts +++ b/packages/lib/types/a2a.ts @@ -204,7 +204,9 @@ export interface JSONRPCErrorResponse { id: string | number | null; } -export type JSONRPCResponse = JSONRPCSuccessResponse | JSONRPCErrorResponse; +export type JSONRPCResponse = + | JSONRPCSuccessResponse + | JSONRPCErrorResponse; // ===== A2A Error Codes ===== @@ -230,11 +232,17 @@ export const A2AErrorCodes = { // ===== Helper Functions ===== -export function createTextPart(text: string, metadata?: Record): TextPart { +export function createTextPart( + text: string, + metadata?: Record, +): TextPart { return { type: "text", text, metadata }; } -export function createDataPart(data: object, metadata?: Record): DataPart { +export function createDataPart( + data: object, + metadata?: Record, +): DataPart { return { type: "data", data, metadata }; } @@ -272,7 +280,10 @@ export function createTask( }; } -export function createTaskStatus(state: TaskState, message?: Message): TaskStatus { +export function createTaskStatus( + state: TaskState, + message?: Message, +): TaskStatus { return { state, message, diff --git a/packages/lib/types/message-content.ts b/packages/lib/types/message-content.ts index b23a665d3..e077a4f22 100644 --- a/packages/lib/types/message-content.ts +++ b/packages/lib/types/message-content.ts @@ -36,7 +36,9 @@ export function hasAttachments( /** * Type guard to check if content has text */ -export function hasText(content: unknown): content is MessageContent & { text: string } { +export function hasText( + content: unknown, +): content is MessageContent & { text: string } { return ( typeof content === "object" && content !== null && @@ -146,7 +148,9 @@ export interface DialogueMetadata extends BaseMetadata { /** * Type guard to check if metadata is DialogueMetadata format */ -export function isDialogueMetadata(metadata: unknown): metadata is DialogueMetadata { +export function isDialogueMetadata( + metadata: unknown, +): metadata is DialogueMetadata { if (!metadata || typeof metadata !== "object") return false; const meta = metadata as Record; return ( @@ -159,7 +163,10 @@ export function isDialogueMetadata(metadata: unknown): metadata is DialogueMetad /** * Helper to check if a message should be visible in conversation logs. */ -export function isVisibleDialogueMessage(metadata: unknown, content?: unknown): boolean { +export function isVisibleDialogueMessage( + metadata: unknown, + content?: unknown, +): boolean { if (content && typeof content === "object") { const c = content as Record; if (c.type === "action_result") { @@ -168,7 +175,10 @@ export function isVisibleDialogueMessage(metadata: unknown, content?: unknown): } if (isDialogueMetadata(metadata)) { - return metadata.visibility !== "hidden" && metadata.dialogueType !== "action_result"; + return ( + metadata.visibility !== "hidden" && + metadata.dialogueType !== "action_result" + ); } if (content && typeof content === "object") { diff --git a/packages/lib/types/referral-me.ts b/packages/lib/types/referral-me.ts index cea6d659c..b183863f8 100644 --- a/packages/lib/types/referral-me.ts +++ b/packages/lib/types/referral-me.ts @@ -36,7 +36,9 @@ export function coerceNonNegativeIntegerCount(val: unknown): number | null { return null; } -export function parseReferralMeResponse(data: unknown): ReferralMeResponse | null { +export function parseReferralMeResponse( + data: unknown, +): ReferralMeResponse | null { if (typeof data !== "object" || data === null) return null; const o = data as Record; if (typeof o.code !== "string" || o.code.length === 0) return null; diff --git a/packages/lib/types/social-media.ts b/packages/lib/types/social-media.ts index 6fc2cf9db..bdbd50524 100644 --- a/packages/lib/types/social-media.ts +++ b/packages/lib/types/social-media.ts @@ -289,8 +289,13 @@ export interface SocialMediaProvider { credentials: SocialCredentials, postId: string, ): Promise<{ success: boolean; error?: string }>; - getPostAnalytics?(credentials: SocialCredentials, postId: string): Promise; - getAccountAnalytics?(credentials: SocialCredentials): Promise; + getPostAnalytics?( + credentials: SocialCredentials, + postId: string, + ): Promise; + getAccountAnalytics?( + credentials: SocialCredentials, + ): Promise; uploadMedia?( credentials: SocialCredentials, media: MediaAttachment, @@ -502,7 +507,9 @@ export function createErrorResult( }; } -export function aggregateResults(results: PostResult[]): MultiPlatformPostResult { +export function aggregateResults( + results: PostResult[], +): MultiPlatformPostResult { const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); return { @@ -551,7 +558,11 @@ export const SocialPlatformSchema = z.enum([ "mastodon", ]); -export const NotificationPlatformSchema = z.enum(["discord", "telegram", "slack"]); +export const NotificationPlatformSchema = z.enum([ + "discord", + "telegram", + "slack", +]); export const MediaAttachmentSchema = z.object({ type: z.enum(["image", "video", "gif"]), diff --git a/packages/lib/utils.ts b/packages/lib/utils.ts index 99c59f715..daab9ff4f 100644 --- a/packages/lib/utils.ts +++ b/packages/lib/utils.ts @@ -22,6 +22,7 @@ export function cn(...inputs: ClassValue[]) { * @returns True if the string is a valid UUID, false otherwise. */ export function isValidUUID(id: string): boolean { - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(id); } diff --git a/packages/lib/utils/agent-billing.ts b/packages/lib/utils/agent-billing.ts index 4d74cb789..d29e9bccb 100644 --- a/packages/lib/utils/agent-billing.ts +++ b/packages/lib/utils/agent-billing.ts @@ -73,12 +73,17 @@ export const ESTIMATED_COSTS = { * Check if agent/org can afford an operation BEFORE executing it. * This is a non-locking check - actual deduction happens post-execution. */ -export async function preCheckBilling(context: BillingContext): Promise { +export async function preCheckBilling( + context: BillingContext, +): Promise { const { agentId, organizationId, estimatedCost } = context; // If agent has a budget, check that first if (agentId) { - const budgetCheck = await agentBudgetService.checkBudget(agentId, estimatedCost); + const budgetCheck = await agentBudgetService.checkBudget( + agentId, + estimatedCost, + ); if (budgetCheck.canProceed) { return { @@ -102,10 +107,13 @@ export async function preCheckBilling(context: BillingContext): Promise(text: string, schema: z.ZodType, context?: string): T { +export function parseAiJson( + text: string, + schema: z.ZodType, + context?: string, +): T { const extracted = extractJsonFromAiResponse(text); let parsed: unknown; @@ -34,8 +38,12 @@ export function parseAiJson(text: string, schema: z.ZodType, context?: str const result = schema.safeParse(parsed); if (!result.success) { - const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", "); - throw new Error(`AI response validation failed${context ? ` (${context})` : ""}: ${issues}`); + const issues = result.error.issues + .map((i) => `${i.path.join(".")}: ${i.message}`) + .join(", "); + throw new Error( + `AI response validation failed${context ? ` (${context})` : ""}: ${issues}`, + ); } return result.data; diff --git a/packages/lib/utils/app-url.ts b/packages/lib/utils/app-url.ts index f39be8ab3..3ce699ebb 100644 --- a/packages/lib/utils/app-url.ts +++ b/packages/lib/utils/app-url.ts @@ -4,7 +4,8 @@ * we use this as the canonical app origin (no trailing slash). */ export function getAppUrl(env: NodeJS.ProcessEnv = process.env): string { - const url = env.NEXT_PUBLIC_APP_URL || env.VERCEL_URL || "http://localhost:3000"; + const url = + env.NEXT_PUBLIC_APP_URL || env.VERCEL_URL || "http://localhost:3000"; const base = url.startsWith("http") ? url : `https://${url}`; return base.replace(/\/$/, ""); } diff --git a/packages/lib/utils/audio.ts b/packages/lib/utils/audio.ts index da3064c93..e552b9616 100644 --- a/packages/lib/utils/audio.ts +++ b/packages/lib/utils/audio.ts @@ -84,7 +84,8 @@ export function createAudioContext(sampleRate?: number): AudioContext { const contextOptions = sampleRate ? { sampleRate } : undefined; const AudioContextClass = window.AudioContext || - (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; + (window as unknown as { webkitAudioContext: typeof AudioContext }) + .webkitAudioContext; return new AudioContextClass(contextOptions); } diff --git a/packages/lib/utils/blooio-api.ts b/packages/lib/utils/blooio-api.ts index 4c718de03..6bc2f5b11 100644 --- a/packages/lib/utils/blooio-api.ts +++ b/packages/lib/utils/blooio-api.ts @@ -177,7 +177,11 @@ export async function verifyBlooioSignature( false, ["sign"], ); - const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(signedPayload)); + const signature = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(signedPayload), + ); const computedSignature = Array.from(new Uint8Array(signature)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); @@ -192,7 +196,10 @@ export async function verifyBlooioSignature( // timingSafeEqual requires same length buffers - we've ensured this above // Also verify actual lengths match (after constant-time comparison) - const signaturesMatch = crypto.timingSafeEqual(computedBuffer, expectedBuffer); + const signaturesMatch = crypto.timingSafeEqual( + computedBuffer, + expectedBuffer, + ); const lengthsMatch = computedSignature.length === expectedSignature.length; return signaturesMatch && lengthsMatch; } catch { @@ -232,7 +239,8 @@ export function isValidBlooioMediaUrl(url: string): boolean { } // Must be from allowed domain return ALLOWED_BLOOIO_MEDIA_DOMAINS.some( - (domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`), + (domain) => + parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`), ); } catch { return false; @@ -252,7 +260,10 @@ export function extractBlooioMediaUrls( return attachments .map((a) => (typeof a === "string" ? a : a.url)) - .filter((url): url is string => typeof url === "string" && isValidBlooioMediaUrl(url)); + .filter( + (url): url is string => + typeof url === "string" && isValidBlooioMediaUrl(url), + ); } /** diff --git a/packages/lib/utils/cors.ts b/packages/lib/utils/cors.ts index 5a0a2d639..e2d21df05 100644 --- a/packages/lib/utils/cors.ts +++ b/packages/lib/utils/cors.ts @@ -12,7 +12,8 @@ const ALLOWED_ORIGINS = [ export function getCorsHeaders(origin: string | null): Record { // Only reflect origin if it's in the allowlist; otherwise use first allowed origin or reject // Only set origin header for allowed origins, otherwise omit it entirely - const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin) ? origin : undefined; // Omit header for non-allowed origins + const allowedOrigin = + origin && ALLOWED_ORIGINS.includes(origin) ? origin : undefined; // Omit header for non-allowed origins const headers: Record = { "Access-Control-Allow-Methods": "GET, POST, OPTIONS", diff --git a/packages/lib/utils/date.ts b/packages/lib/utils/date.ts index caf45b874..c7768357a 100644 --- a/packages/lib/utils/date.ts +++ b/packages/lib/utils/date.ts @@ -38,7 +38,9 @@ export function safeToISOString(value: unknown): string { } const date = new Date(timestamp); - return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString(); + return Number.isNaN(date.getTime()) + ? new Date().toISOString() + : date.toISOString(); } catch { return new Date().toISOString(); } diff --git a/packages/lib/utils/default-avatar.ts b/packages/lib/utils/default-avatar.ts index 0e581a5ca..a64f7d438 100644 --- a/packages/lib/utils/default-avatar.ts +++ b/packages/lib/utils/default-avatar.ts @@ -153,7 +153,8 @@ export function getAvailableAvatarStyles(): Array<{ url: string; }> { return CLOUD_AGENT_AVATARS.map((url, index) => { - const filename = url.split("/").pop()?.replace(".webp", "") ?? `agent-${index}`; + const filename = + url.split("/").pop()?.replace(".webp", "") ?? `agent-${index}`; return { id: `cloud-${filename}`, name: `Agent ${index + 1}`, @@ -169,7 +170,10 @@ export function getAvailableAvatarStyles(): Array<{ * @param name - Optional character name for deterministic avatar selection * @returns A valid avatar URL (either the original or a deterministic/fallback avatar) */ -export function ensureAvatarUrl(avatarUrl: string | null | undefined, name?: string): string { +export function ensureAvatarUrl( + avatarUrl: string | null | undefined, + name?: string, +): string { if (avatarUrl && avatarUrl.trim() !== "") { return avatarUrl; } diff --git a/packages/lib/utils/deterministic-uuid.ts b/packages/lib/utils/deterministic-uuid.ts index d3b3df90e..f67a6cfe3 100644 --- a/packages/lib/utils/deterministic-uuid.ts +++ b/packages/lib/utils/deterministic-uuid.ts @@ -24,7 +24,9 @@ export function generateElizaAppRoomId( agentId: string, identifier: string, ): string { - return generateDeterministicUUID(`eliza-app:${channel}:room:${agentId}:${identifier}`); + return generateDeterministicUUID( + `eliza-app:${channel}:room:${agentId}:${identifier}`, + ); } /** diff --git a/packages/lib/utils/discord-api.ts b/packages/lib/utils/discord-api.ts index b3cbad745..d1ec6be3b 100644 --- a/packages/lib/utils/discord-api.ts +++ b/packages/lib/utils/discord-api.ts @@ -93,7 +93,9 @@ export async function discordBearerApiRequest( if (!response.ok) { const error = await response.json().catch(() => ({})); - throw new Error(`Discord API error: ${response.status} - ${error.message || "Unknown error"}`); + throw new Error( + `Discord API error: ${response.status} - ${error.message || "Unknown error"}`, + ); } return response.json(); diff --git a/packages/lib/utils/discord-helpers.ts b/packages/lib/utils/discord-helpers.ts index 38c7ae890..58b3623d4 100644 --- a/packages/lib/utils/discord-helpers.ts +++ b/packages/lib/utils/discord-helpers.ts @@ -74,7 +74,10 @@ export function getChannelTypeName(type: number): string { * Check if channel type is text-based (can send messages) */ export function isTextChannel(type: number): boolean { - return type === DiscordChannelType.GuildText || type === DiscordChannelType.GuildAnnouncement; + return ( + type === DiscordChannelType.GuildText || + type === DiscordChannelType.GuildAnnouncement + ); } /** @@ -212,7 +215,9 @@ export function createLinkButton(label: string, url: string) { /** * Create an action row with buttons */ -export function createActionRow(buttons: Array<{ label: string; url: string }>) { +export function createActionRow( + buttons: Array<{ label: string; url: string }>, +) { return { type: 1 as const, // Action Row components: buttons.map((b) => createLinkButton(b.label, b.url)), @@ -234,7 +239,8 @@ export function createEmbed(options: { const embed: Record = {}; if (options.title) embed.title = options.title; - if (options.description) embed.description = truncate(options.description, 4096); + if (options.description) + embed.description = truncate(options.description, 4096); if (options.url) embed.url = options.url; if (options.color !== undefined) embed.color = options.color; if (options.thumbnailUrl) embed.thumbnail = { url: options.thumbnailUrl }; diff --git a/packages/lib/utils/error-handling.ts b/packages/lib/utils/error-handling.ts index b38297a39..4d131d496 100644 --- a/packages/lib/utils/error-handling.ts +++ b/packages/lib/utils/error-handling.ts @@ -4,6 +4,7 @@ export function extractErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === "string") return error; - if (error && typeof error === "object" && "message" in error) return String(error.message); + if (error && typeof error === "object" && "message" in error) + return String(error.message); return "Unknown error"; } diff --git a/packages/lib/utils/google-api.ts b/packages/lib/utils/google-api.ts index 38f638cc1..95f1a1a5d 100644 --- a/packages/lib/utils/google-api.ts +++ b/packages/lib/utils/google-api.ts @@ -13,14 +13,17 @@ const DEFAULT_TIMEOUT_MS = 30000; * @param timeoutMs - Timeout in milliseconds * @returns AbortSignal that will abort after the specified timeout */ -function createTimeoutSignal(timeoutMs: number = DEFAULT_TIMEOUT_MS): AbortSignal { +function createTimeoutSignal( + timeoutMs: number = DEFAULT_TIMEOUT_MS, +): AbortSignal { return AbortSignal.timeout(timeoutMs); } export const GOOGLE_AUTH_BASE = "https://accounts.google.com/o/oauth2/v2/auth"; export const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"; export const GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke"; -export const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"; +export const GOOGLE_USERINFO_URL = + "https://www.googleapis.com/oauth2/v2/userinfo"; /** * Available Google OAuth scopes for the workflow builder @@ -36,10 +39,14 @@ export const GOOGLE_SCOPES = { CALENDAR: "https://www.googleapis.com/auth/calendar", CALENDAR_READONLY: "https://www.googleapis.com/auth/calendar.readonly", CALENDAR_EVENTS: "https://www.googleapis.com/auth/calendar.events", - CALENDAR_EVENTS_READONLY: "https://www.googleapis.com/auth/calendar.events.readonly", - CALENDAR_EVENTS_OWNED: "https://www.googleapis.com/auth/calendar.events.owned", - CALENDAR_EVENTS_OWNED_READONLY: "https://www.googleapis.com/auth/calendar.events.owned.readonly", - CALENDAR_CALENDARS_READONLY: "https://www.googleapis.com/auth/calendar.calendars.readonly", + CALENDAR_EVENTS_READONLY: + "https://www.googleapis.com/auth/calendar.events.readonly", + CALENDAR_EVENTS_OWNED: + "https://www.googleapis.com/auth/calendar.events.owned", + CALENDAR_EVENTS_OWNED_READONLY: + "https://www.googleapis.com/auth/calendar.events.owned.readonly", + CALENDAR_CALENDARS_READONLY: + "https://www.googleapis.com/auth/calendar.calendars.readonly", // Contacts scopes CONTACTS_READONLY: "https://www.googleapis.com/auth/contacts.readonly", @@ -76,7 +83,9 @@ export const DEFAULT_GOOGLE_SCOPES = [ * Set of all allowed Google OAuth scopes * Used to validate user-requested scopes */ -export const ALLOWED_GOOGLE_SCOPES = new Set(Object.values(GOOGLE_SCOPES)); +export const ALLOWED_GOOGLE_SCOPES = new Set( + Object.values(GOOGLE_SCOPES), +); /** * Validate and filter requested scopes to only include allowed ones @@ -205,7 +214,9 @@ export async function refreshGoogleToken(params: { /** * Get user info from Google */ -export async function getGoogleUserInfo(accessToken: string): Promise { +export async function getGoogleUserInfo( + accessToken: string, +): Promise { const response = await fetch(GOOGLE_USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/packages/lib/utils/google-mcp-shared.ts b/packages/lib/utils/google-mcp-shared.ts index 6feb7b308..4eb90a275 100644 --- a/packages/lib/utils/google-mcp-shared.ts +++ b/packages/lib/utils/google-mcp-shared.ts @@ -33,7 +33,9 @@ export async function googleFetchWithToken( } catch (err) { clearTimeout(timeoutId); if (err instanceof DOMException && err.name === "AbortError") { - throw new Error(`Google API request timed out after ${GOOGLE_API_TIMEOUT_MS / 1000}s`); + throw new Error( + `Google API request timed out after ${GOOGLE_API_TIMEOUT_MS / 1000}s`, + ); } throw err; } @@ -47,13 +49,17 @@ export async function googleFetchWithToken( const apiCode = errorBody.error?.code || errorBody.error?.status; const parts: string[] = []; if (apiMsg) parts.push(apiMsg); - if (apiCode && apiCode !== response.status) parts.push(`code: ${apiCode}`); + if (apiCode && apiCode !== response.status) + parts.push(`code: ${apiCode}`); if (response.status === 429) { const retryAfter = response.headers.get("Retry-After"); logger.warn("[GoogleMCP] Rate limit hit", { url, retryAfter }); if (retryAfter) parts.push(`retry after ${retryAfter}s`); } - errorDetail = parts.length > 0 ? parts.join(" — ") : `Google API error: ${response.status}`; + errorDetail = + parts.length > 0 + ? parts.join(" — ") + : `Google API error: ${response.status}`; } catch { errorDetail = `Google API error: ${response.status} ${response.statusText}`; } @@ -109,9 +115,12 @@ export function extractBody(payload: Record): string { // ── Mappers ────────────────────────────────────────────────────────────────── -export function mapGmailMessage(d: Record): Record { +export function mapGmailMessage( + d: Record, +): Record { const payload = d.payload as Record | undefined; - const headers = (payload?.headers as Array<{ name: string; value: string }>) || []; + const headers = + (payload?.headers as Array<{ name: string; value: string }>) || []; return { id: d.id, threadId: d.threadId, @@ -124,7 +133,9 @@ export function mapGmailMessage(d: Record): Record): Record { +export function mapCalendarEvent( + e: Record, +): Record { const start = e.start as Record | undefined; const end = e.end as Record | undefined; const attendees = e.attendees as Array> | undefined; @@ -146,7 +157,9 @@ export function mapCalendarEvent(e: Record): Record): Record { +export function mapContact( + person: Record, +): Record { const p = (person.person || person) as Record; const names = p.names as Array> | undefined; const emails = p.emailAddresses as Array> | undefined; @@ -221,7 +234,8 @@ function getTimeZoneOffsetMinutes(date: Date, timeZone: string): number { second: "2-digit", hour12: false, }).formatToParts(date); - const token = parts.find((part) => part.type === "timeZoneName")?.value?.trim() ?? "GMT"; + const token = + parts.find((part) => part.type === "timeZoneName")?.value?.trim() ?? "GMT"; if (token === "GMT" || token === "UTC") return 0; const match = token.match(/^GMT([+-])(\d{1,2})(?::?(\d{2}))?$/i); if (!match) { @@ -243,7 +257,10 @@ function formatOffsetToken(offsetMinutes: number): string { return `${sign}${hours}:${minutes}`; } -function formatInstantAsRfc3339InTimeZone(dateTime: string, timeZone: string): string { +function formatInstantAsRfc3339InTimeZone( + dateTime: string, + timeZone: string, +): string { const date = new Date(dateTime); if (!Number.isFinite(date.getTime())) { throw new Error(`Invalid datetime: ${dateTime}`); @@ -277,7 +294,10 @@ export function applyTimeZone( }; } -const calendarTzCache = new Map(); +const calendarTzCache = new Map< + string, + { value: string | null; expiresAt: number } +>(); const CALENDAR_TZ_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes /** @@ -297,10 +317,15 @@ export async function getCalendarTimeZone( } try { - const res = await fetchFn("https://www.googleapis.com/calendar/v3/calendars/primary"); + const res = await fetchFn( + "https://www.googleapis.com/calendar/v3/calendars/primary", + ); const data = await res.json(); const tz = (data.timeZone as string) || null; - calendarTzCache.set(cacheKey, { value: tz, expiresAt: now + CALENDAR_TZ_CACHE_TTL_MS }); + calendarTzCache.set(cacheKey, { + value: tz, + expiresAt: now + CALENDAR_TZ_CACHE_TTL_MS, + }); return tz; } catch { return null; diff --git a/packages/lib/utils/hubspot-mcp-shared.ts b/packages/lib/utils/hubspot-mcp-shared.ts index 510c7be5f..1277cbdfe 100644 --- a/packages/lib/utils/hubspot-mcp-shared.ts +++ b/packages/lib/utils/hubspot-mcp-shared.ts @@ -56,7 +56,9 @@ type HubSpotOwner = { teams?: unknown; }; -export async function getHubSpotTokenForOrg(organizationId: string): Promise { +export async function getHubSpotTokenForOrg( + organizationId: string, +): Promise { try { const result = await oauthService.getValidTokenByPlatform({ organizationId, @@ -68,7 +70,9 @@ export async function getHubSpotTokenForOrg(organizationId: string): Promise Connections."); + throw new Error( + "HubSpot account not connected. Connect in Settings > Connections.", + ); } } @@ -78,7 +82,9 @@ export async function hubspotFetchForOrg( options: RequestInit = {}, ): Promise { const token = await getHubSpotTokenForOrg(organizationId); - const url = endpoint.startsWith("http") ? endpoint : `${HUBSPOT_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${HUBSPOT_API_BASE}${endpoint}`; const response = await fetch(url, { ...options, @@ -130,20 +136,27 @@ export function mapHubSpotOwner(owner: HubSpotOwner): Record { }; } -export async function getHubSpotStatus(organizationId: string): Promise> { +export async function getHubSpotStatus( + organizationId: string, +): Promise> { const connections = await oauthService.listConnections({ organizationId, platform: "hubspot", }); - const active = connections.find((connection) => connection.status === "active"); + const active = connections.find( + (connection) => connection.status === "active", + ); if (!active) { - const expired = connections.find((connection) => connection.status === "expired"); + const expired = connections.find( + (connection) => connection.status === "expired", + ); if (expired) { return { connected: false, status: "expired", - message: "HubSpot connection expired. Please reconnect in Settings > Connections.", + message: + "HubSpot connection expired. Please reconnect in Settings > Connections.", }; } return { @@ -182,7 +195,9 @@ export async function listHubSpotObjects( `/crm/v3/objects/${objectType}?${params}`, ); const data = await response.json(); - const results = Array.isArray(data.results) ? (data.results as HubSpotRecord[]) : []; + const results = Array.isArray(data.results) + ? (data.results as HubSpotRecord[]) + : []; return { results, @@ -211,10 +226,14 @@ export async function createHubSpotObject( objectType: HubSpotObjectType, properties: Record, ): Promise { - const response = await hubspotFetchForOrg(organizationId, `/crm/v3/objects/${objectType}`, { - method: "POST", - body: JSON.stringify({ properties }), - }); + const response = await hubspotFetchForOrg( + organizationId, + `/crm/v3/objects/${objectType}`, + { + method: "POST", + body: JSON.stringify({ properties }), + }, + ); return (await response.json()) as HubSpotRecord; } @@ -257,7 +276,9 @@ export async function searchHubSpotObjects( }, ); const data = await response.json(); - const results = Array.isArray(data.results) ? (data.results as HubSpotRecord[]) : []; + const results = Array.isArray(data.results) + ? (data.results as HubSpotRecord[]) + : []; return { results, @@ -278,9 +299,14 @@ export async function listHubSpotOwners( params.set("email", options.email); } - const response = await hubspotFetchForOrg(organizationId, `/crm/v3/owners?${params}`); + const response = await hubspotFetchForOrg( + organizationId, + `/crm/v3/owners?${params}`, + ); const data = await response.json(); - const results = Array.isArray(data.results) ? (data.results as HubSpotOwner[]) : []; + const results = Array.isArray(data.results) + ? (data.results as HubSpotOwner[]) + : []; return { results, @@ -306,7 +332,9 @@ export async function createHubSpotAssociation( const associationType = associationTypeMap[fromObjectType]?.[toObjectType]; if (!associationType) { - throw new Error(`Invalid association: ${fromObjectType} -> ${toObjectType}`); + throw new Error( + `Invalid association: ${fromObjectType} -> ${toObjectType}`, + ); } await hubspotFetchForOrg( diff --git a/packages/lib/utils/idempotency.ts b/packages/lib/utils/idempotency.ts index e7495d0b8..9a3a32bd1 100644 --- a/packages/lib/utils/idempotency.ts +++ b/packages/lib/utils/idempotency.ts @@ -38,7 +38,10 @@ export async function isAlreadyProcessed(key: string): Promise { return true; } catch (error) { - logger.error("[Idempotency] Error checking key", { key, error: getErrorMessage(error) }); + logger.error("[Idempotency] Error checking key", { + key, + error: getErrorMessage(error), + }); return false; } } @@ -49,7 +52,10 @@ export async function isAlreadyProcessed(key: string): Promise { * Unlike isAlreadyProcessed + markAsProcessed, this is a single atomic operation * with no TOCTOU race window. */ -export async function tryClaimForProcessing(key: string, source = "unknown"): Promise { +export async function tryClaimForProcessing( + key: string, + source = "unknown", +): Promise { try { const expires_at = new Date(Date.now() + IDEMPOTENCY_TTL_MS); const rows = await dbWrite @@ -78,14 +84,20 @@ export async function releaseProcessingClaim(key: string): Promise { try { await dbWrite.delete(idempotencyKeys).where(eq(idempotencyKeys.key, key)); } catch (error) { - logger.error("[Idempotency] Error releasing claim", { key, error: getErrorMessage(error) }); + logger.error("[Idempotency] Error releasing claim", { + key, + error: getErrorMessage(error), + }); } } /** * Mark a message as processed. Uses upsert to handle race conditions. */ -export async function markAsProcessed(key: string, source = "unknown"): Promise { +export async function markAsProcessed( + key: string, + source = "unknown", +): Promise { try { const expires_at = new Date(Date.now() + IDEMPOTENCY_TTL_MS); await dbWrite @@ -93,7 +105,11 @@ export async function markAsProcessed(key: string, source = "unknown"): Promise< .values({ key, source, expires_at }) .onConflictDoUpdate({ target: idempotencyKeys.key, set: { expires_at } }); } catch (error) { - logger.error("[Idempotency] Error marking key", { key, source, error: getErrorMessage(error) }); + logger.error("[Idempotency] Error marking key", { + key, + source, + error: getErrorMessage(error), + }); } } @@ -108,7 +124,9 @@ export async function getProcessedMessagesCount(): Promise { .where(gt(idempotencyKeys.expires_at, new Date())); return result?.count ?? 0; } catch (error) { - logger.error("[Idempotency] Error getting count", { error: getErrorMessage(error) }); + logger.error("[Idempotency] Error getting count", { + error: getErrorMessage(error), + }); return 0; } } @@ -124,11 +142,15 @@ export async function cleanupExpiredKeys(): Promise { .returning({ id: idempotencyKeys.id }); if (deleted.length > 0) { - logger.info("[Idempotency] Cleaned up expired keys", { count: deleted.length }); + logger.info("[Idempotency] Cleaned up expired keys", { + count: deleted.length, + }); } return deleted.length; } catch (error) { - logger.error("[Idempotency] Error cleaning up", { error: getErrorMessage(error) }); + logger.error("[Idempotency] Error cleaning up", { + error: getErrorMessage(error), + }); return 0; } } @@ -138,6 +160,8 @@ export async function clearProcessedMessages(): Promise { try { await dbWrite.delete(idempotencyKeys); } catch (error) { - logger.error("[Idempotency] Error clearing keys", { error: getErrorMessage(error) }); + logger.error("[Idempotency] Error clearing keys", { + error: getErrorMessage(error), + }); } } diff --git a/packages/lib/utils/json-parsing.ts b/packages/lib/utils/json-parsing.ts index f19a96393..eb380af8c 100644 --- a/packages/lib/utils/json-parsing.ts +++ b/packages/lib/utils/json-parsing.ts @@ -11,7 +11,9 @@ import { extractErrorMessage } from "./error-handling"; * Safely parse JSON response, returning empty object on failure * Use when parsing error responses where JSON might be malformed */ -export async function safeJsonParse>(response: Response): Promise { +export async function safeJsonParse>( + response: Response, +): Promise { try { const text = await response.text(); if (!text.trim()) { @@ -32,6 +34,8 @@ export function parseJson(text: string, context?: string): T { return JSON.parse(text) as T; } catch (error) { const contextMsg = context ? ` (${context})` : ""; - throw new Error(`Failed to parse JSON${contextMsg}: ${extractErrorMessage(error)}`); + throw new Error( + `Failed to parse JSON${contextMsg}: ${extractErrorMessage(error)}`, + ); } } diff --git a/packages/lib/utils/knowledge.ts b/packages/lib/utils/knowledge.ts index 0490d55cf..9f6ff6ba1 100644 --- a/packages/lib/utils/knowledge.ts +++ b/packages/lib/utils/knowledge.ts @@ -18,7 +18,11 @@ const ORPHANED_BLOB_LOG_MARKER = "[ORPHANED_BLOB]"; export interface OrphanedBlobInfo { blobUrl: string; userId?: string; - reason: "cleanup_failed" | "partial_upload_failure" | "expired_pending" | "unknown"; + reason: + | "cleanup_failed" + | "partial_upload_failure" + | "expired_pending" + | "unknown"; originalError?: string; timestamp: number; } diff --git a/packages/lib/utils/ledger-source-id.ts b/packages/lib/utils/ledger-source-id.ts index d2be83e03..5b158af4c 100644 --- a/packages/lib/utils/ledger-source-id.ts +++ b/packages/lib/utils/ledger-source-id.ts @@ -1,6 +1,7 @@ import crypto from "crypto"; -const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function formatHashAsUuid(hash: string): string { const normalized = hash.slice(0, 32).split(""); diff --git a/packages/lib/utils/logger.ts b/packages/lib/utils/logger.ts index 9926e7855..d55062074 100644 --- a/packages/lib/utils/logger.ts +++ b/packages/lib/utils/logger.ts @@ -124,10 +124,15 @@ export const redact = { lowerKey === "api_key" || (lowerKey.includes("token") && !lowerKey.includes("tokenid")) || (lowerKey.includes("key") && - (lowerKey.includes("ssh") || lowerKey.includes("api") || lowerKey.includes("signing"))) + (lowerKey.includes("ssh") || + lowerKey.includes("api") || + lowerKey.includes("signing"))) ) { redacted[key] = "[REDACTED]"; - } else if (lowerKey.includes("txhash") || lowerKey.includes("transaction_hash")) { + } else if ( + lowerKey.includes("txhash") || + lowerKey.includes("transaction_hash") + ) { redacted[key] = redact.txHash(value); } else if ( lowerKey === "ip" || @@ -135,7 +140,10 @@ export const redact = { lowerKey.includes("source_ip") ) { redacted[key] = redact.ip(value); - } else if (lowerKey.includes("paymentid") || lowerKey.includes("payment_id")) { + } else if ( + lowerKey.includes("paymentid") || + lowerKey.includes("payment_id") + ) { redacted[key] = redact.paymentId(value); } else if ( lowerKey.includes("organizationid") || @@ -145,9 +153,15 @@ export const redact = { redacted[key] = redact.orgId(value); } else if (lowerKey.includes("userid") || lowerKey.includes("user_id")) { redacted[key] = redact.userId(value); - } else if (lowerKey.includes("trackid") || lowerKey.includes("track_id")) { + } else if ( + lowerKey.includes("trackid") || + lowerKey.includes("track_id") + ) { redacted[key] = redact.trackId(value); - } else if (lowerKey.includes("address") && (value.startsWith("0x") || value.length > 30)) { + } else if ( + lowerKey.includes("address") && + (value.startsWith("0x") || value.length > 30) + ) { redacted[key] = redact.address(value); } else { redacted[key] = value; diff --git a/packages/lib/utils/message-text.ts b/packages/lib/utils/message-text.ts index 7fae3d5de..1ccafa74a 100644 --- a/packages/lib/utils/message-text.ts +++ b/packages/lib/utils/message-text.ts @@ -9,13 +9,26 @@ * Priority-ordered list of field names to check for text content. * More common/reliable fields are checked first. */ -const TEXT_FIELD_PRIORITY = ["text", "thought", "response", "body", "content", "message"] as const; +const TEXT_FIELD_PRIORITY = [ + "text", + "thought", + "response", + "body", + "content", + "message", +] as const; /** * Fields to exclude when searching for any string content. * These fields contain metadata, not user-visible text. */ -const EXCLUDED_FIELDS = new Set(["source", "action", "inReplyTo", "type", "id"]); +const EXCLUDED_FIELDS = new Set([ + "source", + "action", + "inReplyTo", + "type", + "id", +]); /** * Extract text content from a message content object. @@ -52,7 +65,10 @@ export function extractTextFromContent(content: unknown): string { // Last resort: find any non-empty string field (prefer longer strings) const stringFields = Object.entries(c) .filter( - ([key, v]) => typeof v === "string" && (v as string).length > 0 && !EXCLUDED_FIELDS.has(key), + ([key, v]) => + typeof v === "string" && + (v as string).length > 0 && + !EXCLUDED_FIELDS.has(key), ) .sort((a, b) => (b[1] as string).length - (a[1] as string).length); @@ -95,7 +111,10 @@ export function extractTextFromMetadata(metadata: unknown): string { * @param metadata - The message metadata object (optional) * @returns The extracted text content, or empty string if not found */ -export function extractMessageText(content: unknown, metadata?: unknown): string { +export function extractMessageText( + content: unknown, + metadata?: unknown, +): string { // Try content first const textFromContent = extractTextFromContent(content); if (textFromContent) { diff --git a/packages/lib/utils/perf-trace.ts b/packages/lib/utils/perf-trace.ts index 64bab765b..5ed7752b7 100644 --- a/packages/lib/utils/perf-trace.ts +++ b/packages/lib/utils/perf-trace.ts @@ -60,7 +60,11 @@ function isEnabled(): boolean { } /** Singleton no-op trace -- returned when tracing is disabled. */ -const NOOP_RESULT: PerfTraceResult = Object.freeze({ traceId: "", totalMs: 0, phases: [] }); +const NOOP_RESULT: PerfTraceResult = Object.freeze({ + traceId: "", + totalMs: 0, + phases: [], +}); const NOOP_TRACE: PerfTrace = Object.freeze({ mark() {}, end() { @@ -138,7 +142,9 @@ export function createPerfTrace( cachedResult = { traceId, totalMs, phases }; if (totalMs >= minDuration) { - const phasesSummary = phases.map((p) => `${p.name}=${p.durationMs}ms`).join(" "); + const phasesSummary = phases + .map((p) => `${p.name}=${p.durationMs}ms`) + .join(" "); const logMsg = `[PerfTrace:${traceId}] ${phasesSummary} total=${totalMs}ms`; if (logLevel === "debug") { diff --git a/packages/lib/utils/persistence-guard.ts b/packages/lib/utils/persistence-guard.ts index 7237c7729..cb5c0d6b0 100644 --- a/packages/lib/utils/persistence-guard.ts +++ b/packages/lib/utils/persistence-guard.ts @@ -2,7 +2,12 @@ function envFlagEnabled(raw: string | undefined): boolean { const normalized = String(raw ?? "") .trim() .toLowerCase(); - return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; + return ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ); } export function allowEphemeralCloudStateFallback(): boolean { diff --git a/packages/lib/utils/phone-normalization.ts b/packages/lib/utils/phone-normalization.ts index 4b9ab8d77..adce10aa9 100644 --- a/packages/lib/utils/phone-normalization.ts +++ b/packages/lib/utils/phone-normalization.ts @@ -46,18 +46,25 @@ export function normalizeToE164(phoneNumber: string): string | null { * Normalize phone number or email using libphonenumber-js. * Returns E.164 format for phones, lowercase for emails, or cleaned input on failure. */ -export function normalizePhoneNumber(phone: string, defaultCountry?: string): string { +export function normalizePhoneNumber( + phone: string, + defaultCountry?: string, +): string { const input = phone.trim(); // Handle email addresses (for iMessage) if (input.includes("@")) { if (!isValidEmail(input)) { - logger.warn("[PhoneNormalization] Invalid email format", { email: input }); + logger.warn("[PhoneNormalization] Invalid email format", { + email: input, + }); } return input.toLowerCase(); } - const country = (defaultCountry || process.env.DEFAULT_COUNTRY_CODE || "US") as CountryCode; + const country = (defaultCountry || + process.env.DEFAULT_COUNTRY_CODE || + "US") as CountryCode; try { const parsed = input.startsWith("+") @@ -70,11 +77,17 @@ export function normalizePhoneNumber(phone: string, defaultCountry?: string): st // Best-effort format for parseable but invalid numbers if (parsed) { - logger.warn("[PhoneNormalization] Using best-effort format", { phone: input, country }); + logger.warn("[PhoneNormalization] Using best-effort format", { + phone: input, + country, + }); return parsed.format("E.164"); } - logger.warn("[PhoneNormalization] Could not parse", { phone: input, country }); + logger.warn("[PhoneNormalization] Could not parse", { + phone: input, + country, + }); return input.replace(/[^\d+]/g, ""); } catch (error) { logger.warn("[PhoneNormalization] Invalid phone", { @@ -128,7 +141,9 @@ export function parsePhoneNumber( return null; } - const country = (defaultCountry || process.env.DEFAULT_COUNTRY_CODE || "US") as CountryCode; + const country = (defaultCountry || + process.env.DEFAULT_COUNTRY_CODE || + "US") as CountryCode; try { const parsed = trimmedPhone.startsWith("+") diff --git a/packages/lib/utils/platform.ts b/packages/lib/utils/platform.ts index 3f8fa4353..889b101b4 100644 --- a/packages/lib/utils/platform.ts +++ b/packages/lib/utils/platform.ts @@ -23,7 +23,9 @@ export function isBrowser(): boolean { export function isMobileApp(): boolean { if (!isBrowser()) return false; - return process.env.NEXT_PUBLIC_IS_MOBILE_APP === "true" || isIOS() || isAndroid(); + return ( + process.env.NEXT_PUBLIC_IS_MOBILE_APP === "true" || isIOS() || isAndroid() + ); } /** diff --git a/packages/lib/utils/random-names.ts b/packages/lib/utils/random-names.ts index 495835707..7cf1d850b 100644 --- a/packages/lib/utils/random-names.ts +++ b/packages/lib/utils/random-names.ts @@ -145,9 +145,18 @@ const NOUNS = [ "bolt", ] as const; -const SERVICE_SUFFIXES = ["api", "service", "hub", "connect", "sync", "flow", "bridge"] as const; +const SERVICE_SUFFIXES = [ + "api", + "service", + "hub", + "connect", + "sync", + "flow", + "bridge", +] as const; -const pick = (arr: readonly T[]): T => arr[Math.floor(Math.random() * arr.length)]; +const pick = (arr: readonly T[]): T => + arr[Math.floor(Math.random() * arr.length)]; const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); export function generateRandomName(): string { diff --git a/packages/lib/utils/referral-invite-url.ts b/packages/lib/utils/referral-invite-url.ts index d96f03cee..cd9ab745d 100644 --- a/packages/lib/utils/referral-invite-url.ts +++ b/packages/lib/utils/referral-invite-url.ts @@ -5,7 +5,10 @@ * and header Invite button—avoids drift if we add `intent=signup` or rename params later. * Login also honors `referral_code`; we standardize on `ref` for share links (see app/login). */ -export function buildReferralInviteLoginUrl(origin: string, code: string): string { +export function buildReferralInviteLoginUrl( + origin: string, + code: string, +): string { const base = origin.replace(/\/$/, ""); return `${base}/login?ref=${encodeURIComponent(code)}`; } diff --git a/packages/lib/utils/referral-me-fetch.ts b/packages/lib/utils/referral-me-fetch.ts index 546826490..9c750e7bf 100644 --- a/packages/lib/utils/referral-me-fetch.ts +++ b/packages/lib/utils/referral-me-fetch.ts @@ -1,4 +1,7 @@ -import { parseReferralMeResponse, type ReferralMeResponse } from "@/lib/types/referral-me"; +import { + parseReferralMeResponse, + type ReferralMeResponse, +} from "@/lib/types/referral-me"; export const REFERRALS_ME_API_PATH = "/api/v1/referrals"; diff --git a/packages/lib/utils/responses-stream-reconcile.ts b/packages/lib/utils/responses-stream-reconcile.ts index 5432657a9..c67578725 100644 --- a/packages/lib/utils/responses-stream-reconcile.ts +++ b/packages/lib/utils/responses-stream-reconcile.ts @@ -51,7 +51,10 @@ export type StreamTerminationReason = "end" | "cancel" | "error"; */ export function wrapWithUsageExtraction( upstream: ReadableStream, - onComplete: (usage: ResponsesUsage | null, reason: StreamTerminationReason) => void, + onComplete: ( + usage: ResponsesUsage | null, + reason: StreamTerminationReason, + ) => void, ): ReadableStream { const decoder = new TextDecoder("utf-8", { fatal: false }); let buffer = ""; @@ -188,10 +191,16 @@ function parseSseFrame(frame: string): Record | null { function extractUsage(usageObj: Record): ResponsesUsage { const inputTokens = numberOr(usageObj.input_tokens, 0); const outputTokens = numberOr(usageObj.output_tokens, 0); - const inputDetails = usageObj.input_tokens_details as Record | undefined; - const outputDetails = usageObj.output_tokens_details as Record | undefined; + const inputDetails = usageObj.input_tokens_details as + | Record + | undefined; + const outputDetails = usageObj.output_tokens_details as + | Record + | undefined; const cachedInputTokens = - inputDetails && typeof inputDetails === "object" ? numberOr(inputDetails.cached_tokens, 0) : 0; + inputDetails && typeof inputDetails === "object" + ? numberOr(inputDetails.cached_tokens, 0) + : 0; const reasoningTokens = outputDetails && typeof outputDetails === "object" ? numberOr(outputDetails.reasoning_tokens, 0) diff --git a/packages/lib/utils/siwe-helpers.ts b/packages/lib/utils/siwe-helpers.ts index cda79f784..8e3db4ebc 100644 --- a/packages/lib/utils/siwe-helpers.ts +++ b/packages/lib/utils/siwe-helpers.ts @@ -49,7 +49,9 @@ export async function validateSIWEMessage( } const expectedHost = getAppHost(); if (parsed.domain !== expectedHost) { - throw new Error(`${SIWE_DOMAIN_MISMATCH}: got ${parsed.domain}, expected ${expectedHost}`); + throw new Error( + `${SIWE_DOMAIN_MISMATCH}: got ${parsed.domain}, expected ${expectedHost}`, + ); } const address = getAddress(parsed.address); diff --git a/packages/lib/utils/telegram-api.ts b/packages/lib/utils/telegram-api.ts index bb1117eed..b3877d80e 100644 --- a/packages/lib/utils/telegram-api.ts +++ b/packages/lib/utils/telegram-api.ts @@ -26,7 +26,8 @@ export async function telegramBotApiRequest( if (!data.ok) { throw new Error( - data.description ?? `Telegram API error: ${data.error_code ?? response.status}`, + data.description ?? + `Telegram API error: ${data.error_code ?? response.status}`, ); } @@ -54,7 +55,8 @@ export async function telegramBotApiGet( if (!data.ok) { throw new Error( - data.description ?? `Telegram API error: ${data.error_code ?? response.status}`, + data.description ?? + `Telegram API error: ${data.error_code ?? response.status}`, ); } diff --git a/packages/lib/utils/telegram-helpers.ts b/packages/lib/utils/telegram-helpers.ts index 3481a3810..24244d179 100644 --- a/packages/lib/utils/telegram-helpers.ts +++ b/packages/lib/utils/telegram-helpers.ts @@ -31,7 +31,9 @@ export function splitMessage(text: string, maxLength = 4096): string[] { return chunks; } -export function createInlineKeyboard(buttons: Array<{ text: string; url: string }>): { +export function createInlineKeyboard( + buttons: Array<{ text: string; url: string }>, +): { inline_keyboard: Array>; } { return { @@ -42,7 +44,9 @@ export function createInlineKeyboard(buttons: Array<{ text: string; url: string export function createMultiRowKeyboard( rows: Array>, ): { - inline_keyboard: Array>; + inline_keyboard: Array< + Array<{ text: string; url?: string; callback_data?: string }> + >; } { return { inline_keyboard: rows }; } @@ -83,9 +87,16 @@ export const TELEGRAM_RATE_LIMITS = { MAX_CAPTION_LENGTH: 1024, } as const; -export type TelegramUpdateType = "message" | "edited_message" | "channel_post" | "callback_query"; +export type TelegramUpdateType = + | "message" + | "edited_message" + | "channel_post" + | "callback_query"; -export function extractMessageText(message: { text?: string; caption?: string }): string { +export function extractMessageText(message: { + text?: string; + caption?: string; +}): string { return message.text || message.caption || ""; } diff --git a/packages/lib/utils/toast-adapter.ts b/packages/lib/utils/toast-adapter.ts index f7ab8fab9..9c45ecd05 100644 --- a/packages/lib/utils/toast-adapter.ts +++ b/packages/lib/utils/toast-adapter.ts @@ -15,7 +15,10 @@ import { toast as sonnerToast } from "sonner"; * @param options.mode - Toast type (success, error, or info). * @returns Toast ID for programmatic dismissal. */ -export const toast = (options: { message: string; mode: "success" | "error" | "info" }) => { +export const toast = (options: { + message: string; + mode: "success" | "error" | "info"; +}) => { switch (options.mode) { case "success": return sonnerToast.success(options.message); diff --git a/packages/lib/utils/token-address.ts b/packages/lib/utils/token-address.ts index 0c1efba40..63ed5312b 100644 --- a/packages/lib/utils/token-address.ts +++ b/packages/lib/utils/token-address.ts @@ -81,7 +81,10 @@ function shouldLowercase(address: string, chain?: string | null): boolean { * @param chain Optional chain identifier (e.g. "ethereum", "solana"). * @returns Normalised address string, or the original if no normalisation applies. */ -export function normalizeTokenAddress(address: string, chain?: string | null): string { +export function normalizeTokenAddress( + address: string, + chain?: string | null, +): string { if (shouldLowercase(address, chain)) { return address.toLowerCase(); } diff --git a/packages/lib/utils/twilio-api.ts b/packages/lib/utils/twilio-api.ts index 2a205bbfc..c83003b1b 100644 --- a/packages/lib/utils/twilio-api.ts +++ b/packages/lib/utils/twilio-api.ts @@ -152,10 +152,16 @@ export async function verifyTwilioSignature( false, ["sign"], ); - const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); + const signatureBuffer = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(data), + ); // Convert to base64 - const computedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer))); + const computedSignature = btoa( + String.fromCharCode(...new Uint8Array(signatureBuffer)), + ); // Use constant-time comparison to prevent timing attacks // Pad both strings to the same length to avoid timing leaks from length differences @@ -167,7 +173,10 @@ export async function verifyTwilioSignature( // timingSafeEqual requires same length buffers - we've ensured this above // Also verify actual lengths match (after constant-time comparison) - const signaturesMatch = crypto.timingSafeEqual(computedBuffer, expectedBuffer); + const signaturesMatch = crypto.timingSafeEqual( + computedBuffer, + expectedBuffer, + ); const lengthsMatch = computedSignature.length === signature.length; return signaturesMatch && lengthsMatch; } catch { @@ -206,7 +215,8 @@ export function isValidMediaUrl(url: string): boolean { } // Must be from allowed domain return ALLOWED_MEDIA_DOMAINS.some( - (domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`), + (domain) => + parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`), ); } catch { return false; diff --git a/packages/lib/utils/twitter-api.ts b/packages/lib/utils/twitter-api.ts index f1f61729f..cb5febbc0 100644 --- a/packages/lib/utils/twitter-api.ts +++ b/packages/lib/utils/twitter-api.ts @@ -15,7 +15,9 @@ export async function twitterApiRequest( accessToken: string, options: RequestInit = {}, ): Promise { - const url = endpoint.startsWith("http") ? endpoint : `${TWITTER_API_BASE}${endpoint}`; + const url = endpoint.startsWith("http") + ? endpoint + : `${TWITTER_API_BASE}${endpoint}`; const response = await fetch(url, { ...options, diff --git a/packages/lib/utils/validation.ts b/packages/lib/utils/validation.ts index cbe5dcf12..48b3823f8 100644 --- a/packages/lib/utils/validation.ts +++ b/packages/lib/utils/validation.ts @@ -9,7 +9,8 @@ * Matches standard UUID format: xxxxxxxx-xxxx-Vxxx-Nxxx-xxxxxxxxxxxx * where V = version (1-5) and N = variant (8, 9, a, b) */ -const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; /** * Validates that a string is a valid UUID format. @@ -42,7 +43,9 @@ export function isValidUUID(value: string): boolean { * @param value - The potentially malformed UUID string. * @returns The sanitized UUID if valid, undefined otherwise. */ -export function sanitizeUUID(value: string | undefined | null): string | undefined { +export function sanitizeUUID( + value: string | undefined | null, +): string | undefined { if (!value) return undefined; // Remove URL-encoded garbage that commonly appends to UUIDs: diff --git a/packages/lib/utils/vercel-api.ts b/packages/lib/utils/vercel-api.ts index 8f24aa66c..137ba6d59 100644 --- a/packages/lib/utils/vercel-api.ts +++ b/packages/lib/utils/vercel-api.ts @@ -40,7 +40,9 @@ export async function vercelApiRequest( const error = await response.json().catch(() => ({ error: { message: response.statusText }, })); - throw new Error(error.error?.message || `Vercel API error: ${response.status}`); + throw new Error( + error.error?.message || `Vercel API error: ${response.status}`, + ); } return response.json(); diff --git a/packages/lib/utils/whatsapp-api.ts b/packages/lib/utils/whatsapp-api.ts index d1ae986dd..365e23960 100644 --- a/packages/lib/utils/whatsapp-api.ts +++ b/packages/lib/utils/whatsapp-api.ts @@ -32,10 +32,14 @@ export interface WhatsAppSendMessageRequest { const WhatsAppSendMessageResponseSchema = z.object({ messaging_product: z.string(), contacts: z.array(z.object({ input: z.string(), wa_id: z.string() })), - messages: z.array(z.object({ id: z.string(), message_status: z.string().optional() })), + messages: z.array( + z.object({ id: z.string(), message_status: z.string().optional() }), + ), }); -export type WhatsAppSendMessageResponse = z.infer; +export type WhatsAppSendMessageResponse = z.infer< + typeof WhatsAppSendMessageResponseSchema +>; export interface WhatsAppMarkReadRequest { messaging_product: "whatsapp"; @@ -114,7 +118,9 @@ export const WhatsAppWebhookPayloadSchema = z.object({ entry: z.array(WhatsAppWebhookEntrySchema), }); -export type WhatsAppWebhookPayload = z.infer; +export type WhatsAppWebhookPayload = z.infer< + typeof WhatsAppWebhookPayloadSchema +>; // ============================================================================ // Webhook Signature Verification @@ -131,7 +137,11 @@ export function verifyWhatsAppSignature( signatureHeader: string, rawBody: string, ): boolean { - if (!signatureHeader || !appSecret || !signatureHeader.startsWith("sha256=")) { + if ( + !signatureHeader || + !appSecret || + !signatureHeader.startsWith("sha256=") + ) { return false; } @@ -140,7 +150,10 @@ export function verifyWhatsAppSignature( const expectedSignature = signatureHeader.replace("sha256=", ""); // Compute HMAC-SHA256 - const computedSignature = crypto.createHmac("sha256", appSecret).update(rawBody).digest("hex"); + const computedSignature = crypto + .createHmac("sha256", appSecret) + .update(rawBody) + .digest("hex"); // Use constant-time comparison to prevent timing attacks const expectedBuffer = Buffer.from(expectedSignature, "hex"); @@ -164,7 +177,9 @@ export function verifyWhatsAppSignature( * Parse and validate a WhatsApp webhook payload. * Returns the validated payload or throws a ZodError. */ -export function parseWhatsAppWebhookPayload(data: unknown): WhatsAppWebhookPayload { +export function parseWhatsAppWebhookPayload( + data: unknown, +): WhatsAppWebhookPayload { return WhatsAppWebhookPayloadSchema.parse(data); } @@ -288,7 +303,9 @@ export async function markWhatsAppMessageAsRead( if (!response.ok) { const errorText = await response.text(); - throw new Error(`WhatsApp mark-read error (${response.status}): ${errorText}`); + throw new Error( + `WhatsApp mark-read error (${response.status}): ${errorText}`, + ); } } diff --git a/packages/scripts/audit-migrations.ts b/packages/scripts/audit-migrations.ts index a30407b54..972f1fc30 100644 --- a/packages/scripts/audit-migrations.ts +++ b/packages/scripts/audit-migrations.ts @@ -45,7 +45,10 @@ interface DrizzleMigration { } async function getJournalEntries(): Promise { - const journalPath = path.join(process.cwd(), "packages/db/migrations/meta/_journal.json"); + const journalPath = path.join( + process.cwd(), + "packages/db/migrations/meta/_journal.json", + ); const content = await readFile(journalPath, "utf-8"); return JSON.parse(content) as Journal; } @@ -62,7 +65,9 @@ async function getMigrationFiles(): Promise { }); } -async function getAppliedMigrations(client: pg.Client): Promise { +async function getAppliedMigrations( + client: pg.Client, +): Promise { const result = await client.query( 'SELECT * FROM "__drizzle_migrations" ORDER BY id', ); @@ -86,7 +91,9 @@ async function main() { filesByNumber.set(num, existing); } - const duplicates = Array.from(filesByNumber.entries()).filter(([, files]) => files.length > 1); + const duplicates = Array.from(filesByNumber.entries()).filter( + ([, files]) => files.length > 1, + ); if (duplicates.length > 0) { console.log("\n ⚠️ DUPLICATE MIGRATION NUMBERS:"); for (const [num, files] of duplicates) { @@ -153,7 +160,9 @@ async function main() { if (!tableCheck.rows[0]?.exists) { console.log(" ⚠️ __drizzle_migrations table does not exist"); - console.log(" This database has never had migrations applied via Drizzle"); + console.log( + " This database has never had migrations applied via Drizzle", + ); return; } @@ -161,7 +170,9 @@ async function main() { console.log(` Applied migrations: ${appliedMigrations.length}`); console.log(" Entries:"); for (const migration of appliedMigrations) { - console.log(` ${migration.id}: ${migration.hash} (${migration.created_at})`); + console.log( + ` ${migration.id}: ${migration.hash} (${migration.created_at})`, + ); } console.log("\n📈 COMPARISON:"); @@ -170,7 +181,9 @@ async function main() { console.log(` SQL files on disk: ${migrationFiles.length}`); if (journal.entries.length !== appliedMigrations.length) { - console.log("\n ⚠️ MISMATCH between journal entries and applied migrations!"); + console.log( + "\n ⚠️ MISMATCH between journal entries and applied migrations!", + ); } } catch (error) { console.log(` ❌ Error connecting to database: ${error}`); diff --git a/packages/scripts/backfill-steward-users.ts b/packages/scripts/backfill-steward-users.ts index cf9b79447..9fd269292 100644 --- a/packages/scripts/backfill-steward-users.ts +++ b/packages/scripts/backfill-steward-users.ts @@ -75,6 +75,9 @@ Options: } main().catch((error) => { - console.error("Backfill failed:", error instanceof Error ? error.message : String(error)); + console.error( + "Backfill failed:", + error instanceof Error ? error.message : String(error), + ); process.exit(1); }); diff --git a/packages/scripts/check-migration-journal.ts b/packages/scripts/check-migration-journal.ts index 47510a594..18136fa00 100644 --- a/packages/scripts/check-migration-journal.ts +++ b/packages/scripts/check-migration-journal.ts @@ -52,7 +52,9 @@ async function main() { for (const [prefix, files] of trackedFilesByPrefix) { if (files.length > 1) { - errors.push(`Duplicate journal-tracked migration prefix ${prefix}: ${files.join(", ")}`); + errors.push( + `Duplicate journal-tracked migration prefix ${prefix}: ${files.join(", ")}`, + ); } } diff --git a/packages/scripts/check-types-split.ts b/packages/scripts/check-types-split.ts index 95459ab9f..14c553bdf 100644 --- a/packages/scripts/check-types-split.ts +++ b/packages/scripts/check-types-split.ts @@ -51,15 +51,25 @@ async function splitIntoSubdirectories(dir: string): Promise { async function getDirectoriesToCheck(): Promise { const libSubdirs = await splitIntoSubdirectories("packages/lib"); const appSubdirs = await splitIntoSubdirectories("app"); - const componentSubdirs = await splitIntoSubdirectories("packages/ui/src/components"); + const componentSubdirs = await splitIntoSubdirectories( + "packages/ui/src/components", + ); return ["packages/db", ...libSubdirs, ...componentSubdirs, ...appSubdirs]; } -async function createTempTsconfig(directory: string, baseTsconfig: object): Promise { +async function createTempTsconfig( + directory: string, + baseTsconfig: object, +): Promise { const safeDirectoryName = directory.replace(/[\\/]/g, "."); const workspaceRoot = process.cwd(); - const tempDir = join(workspaceRoot, "node_modules", ".cache", "check-types-split"); + const tempDir = join( + workspaceRoot, + "node_modules", + ".cache", + "check-types-split", + ); await mkdir(tempDir, { recursive: true }); const tempPath = join( tempDir, @@ -110,7 +120,10 @@ async function createTempTsconfig(directory: string, baseTsconfig: object): Prom return tempPath; } -async function checkDirectory(directory: string, baseTsconfig: object): Promise { +async function checkDirectory( + directory: string, + baseTsconfig: object, +): Promise { const start = Date.now(); let tempConfigPath: string | null = null; @@ -142,7 +155,9 @@ async function checkDirectory(directory: string, baseTsconfig: object): Promise< error.message : String(error); - console.log(` ✗ ${directory}/ has errors (${(duration / 1000).toFixed(1)}s)`); + console.log( + ` ✗ ${directory}/ has errors (${(duration / 1000).toFixed(1)}s)`, + ); return { directory, success: false, output, duration }; } finally { diff --git a/packages/scripts/cleanup-eliza-app-user.ts b/packages/scripts/cleanup-eliza-app-user.ts index 2ef98ff73..f2d074c7b 100644 --- a/packages/scripts/cleanup-eliza-app-user.ts +++ b/packages/scripts/cleanup-eliza-app-user.ts @@ -47,7 +47,9 @@ async function clearRedisKeys(patterns: string[]) { const restToken = process.env.KV_REST_API_TOKEN; if (!restUrl || !restToken) { - console.log(" ⚠ No Redis configured (KV_REST_API_URL/TOKEN), skipping cache cleanup"); + console.log( + " ⚠ No Redis configured (KV_REST_API_URL/TOKEN), skipping cache cleanup", + ); return; } @@ -75,7 +77,8 @@ async function clearRedisKeys(patterns: string[]) { match: pattern, count: 100, }); - cursor = typeof result[0] === "string" ? parseInt(result[0], 10) : result[0]; + cursor = + typeof result[0] === "string" ? parseInt(result[0], 10) : result[0]; const keys = result[1]; if (keys.length > 0) { await redis.del(...keys); @@ -109,7 +112,9 @@ async function cleanupByPhone(phoneNumber: string) { // Delete organization (cascades to user, api_keys, etc.) if (user.organization_id) { - await db.delete(organizations).where(eq(organizations.id, user.organization_id)); + await db + .delete(organizations) + .where(eq(organizations.id, user.organization_id)); console.log(` ✓ Deleted organization and all related data`); } else { // No org, just delete user @@ -140,10 +145,14 @@ async function cleanupByTelegram(telegramId: string) { } console.log(` Found user: ${user.id} (${user.name || "unnamed"})`); - console.log(` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`); + console.log( + ` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`, + ); if (user.organization_id) { - await db.delete(organizations).where(eq(organizations.id, user.organization_id)); + await db + .delete(organizations) + .where(eq(organizations.id, user.organization_id)); console.log(` ✓ Deleted organization and all related data`); } else { await db.delete(users).where(eq(users.id, user.id)); @@ -169,14 +178,20 @@ async function cleanupByDiscord(discordId: string) { return; } - console.log(` Found user: ${user.id} (${user.name || user.discord_username || "unnamed"})`); + console.log( + ` Found user: ${user.id} (${user.name || user.discord_username || "unnamed"})`, + ); console.log( ` Discord: ${user.discord_username || "N/A"} (${user.discord_global_name || "N/A"})`, ); - console.log(` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`); + console.log( + ` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`, + ); if (user.organization_id) { - await db.delete(organizations).where(eq(organizations.id, user.organization_id)); + await db + .delete(organizations) + .where(eq(organizations.id, user.organization_id)); console.log(` ✓ Deleted organization and all related data`); } else { await db.delete(users).where(eq(users.id, user.id)); @@ -201,11 +216,17 @@ async function cleanupByWhatsApp(whatsappId: string) { return; } - console.log(` Found user: ${user.id} (${user.name || user.whatsapp_name || "unnamed"})`); - console.log(` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`); + console.log( + ` Found user: ${user.id} (${user.name || user.whatsapp_name || "unnamed"})`, + ); + console.log( + ` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`, + ); if (user.organization_id) { - await db.delete(organizations).where(eq(organizations.id, user.organization_id)); + await db + .delete(organizations) + .where(eq(organizations.id, user.organization_id)); console.log(` ✓ Deleted organization and all related data`); } else { await db.delete(users).where(eq(users.id, user.id)); @@ -242,10 +263,14 @@ async function cleanupById(userId: string) { console.log(` Found user: ${user.id} (${user.name || "unnamed"})`); if (identifiers) console.log(` Identifiers: ${identifiers}`); - console.log(` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`); + console.log( + ` Organization: ${user.organization?.id} (${user.organization?.name || "unnamed"})`, + ); if (user.organization_id) { - await db.delete(organizations).where(eq(organizations.id, user.organization_id)); + await db + .delete(organizations) + .where(eq(organizations.id, user.organization_id)); console.log(` ✓ Deleted organization and all related data`); } else { await db.delete(users).where(eq(users.id, user.id)); @@ -293,7 +318,9 @@ async function cleanupAllTestUsers() { console.log(` - ${user.id} (${user.name || "unnamed"}) [${identifiers}]`); if (user.organization_id) { - await db.delete(organizations).where(eq(organizations.id, user.organization_id)); + await db + .delete(organizations) + .where(eq(organizations.id, user.organization_id)); } else { await db.delete(users).where(eq(users.id, user.id)); } @@ -302,7 +329,9 @@ async function cleanupAllTestUsers() { // Clear all eliza-app sessions await clearRedisKeys([`${SESSION_KEY_PREFIX}*`]); - console.log(`\n✅ Deleted ${testUsers.length} user(s) and their organizations\n`); + console.log( + `\n✅ Deleted ${testUsers.length} user(s) and their organizations\n`, + ); } async function main() { diff --git a/packages/scripts/clear-cache.ts b/packages/scripts/clear-cache.ts index c8cddc645..a1b5b8aa3 100644 --- a/packages/scripts/clear-cache.ts +++ b/packages/scripts/clear-cache.ts @@ -72,7 +72,9 @@ async function clearWithUpstash(url: string, token: string) { const redis = new UpstashRedis({ url, token }); if (pattern) { - console.log("Pattern-based clearing not fully supported with Upstash REST API."); + console.log( + "Pattern-based clearing not fully supported with Upstash REST API.", + ); console.log("Use FLUSHALL for full clear or switch to local Redis."); process.exit(1); } @@ -97,10 +99,15 @@ async function main() { await clearWithUpstash(restUrl, restToken); } else if (redisUrl) { console.log("Using Upstash via REDIS_URL\n"); - await clearWithUpstash(process.env.KV_REST_API_URL || "", process.env.KV_REST_API_TOKEN || ""); + await clearWithUpstash( + process.env.KV_REST_API_URL || "", + process.env.KV_REST_API_TOKEN || "", + ); } else { console.error("No Redis configuration found!"); - console.error("Set REDIS_URL=redis://localhost:6379 for local or configure Upstash."); + console.error( + "Set REDIS_URL=redis://localhost:6379 for local or configure Upstash.", + ); process.exit(1); } } diff --git a/packages/scripts/dead-code-reachability.ts b/packages/scripts/dead-code-reachability.ts index ef9d7394c..4c4d674db 100644 --- a/packages/scripts/dead-code-reachability.ts +++ b/packages/scripts/dead-code-reachability.ts @@ -6,7 +6,14 @@ */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { basename, dirname, join, normalize, relative, resolve } from "node:path"; +import { + basename, + dirname, + join, + normalize, + relative, + resolve, +} from "node:path"; const ROOT = resolve(import.meta.dir, "../.."); @@ -88,7 +95,11 @@ function collectCandidates(): string[] { join(ROOT, "packages/services/gateway-webhook"), ]; for (const r of roots) walkDirs(r, isCandidateSource, acc); - for (const f of ["next.config.ts", "drizzle.config.ts", "mdx-components.tsx"]) { + for (const f of [ + "next.config.ts", + "drizzle.config.ts", + "mdx-components.tsx", + ]) { const p = join(ROOT, f); if (existsSync(p)) acc.push(p); } @@ -117,14 +128,21 @@ function walkAppRouteFiles(dir: string, out: string[]): void { function collectRootEntries(): string[] { const entries: string[] = []; walkAppRouteFiles(join(ROOT, "app"), entries); - for (const f of ["next.config.ts", "drizzle.config.ts", "mdx-components.tsx"]) { + for (const f of [ + "next.config.ts", + "drizzle.config.ts", + "mdx-components.tsx", + ]) { const p = join(ROOT, f); if (existsSync(p)) entries.push(p); } const pkgPath = join(ROOT, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { scripts?: Record }; + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { + scripts?: Record; + }; const scriptStr = JSON.stringify(pkg.scripts ?? {}); - const re = /(?:^|[\s'"`])((?:packages\/scripts\/|scripts\/)[^\s'"`]+\.(?:ts|tsx))(?:\s|$|['"`])/g; + const re = + /(?:^|[\s'"`])((?:packages\/scripts\/|scripts\/)[^\s'"`]+\.(?:ts|tsx))(?:\s|$|['"`])/g; let m: RegExpExecArray | null; while ((m = re.exec(scriptStr)) !== null) { const p = join(ROOT, m[1]); @@ -153,14 +171,22 @@ function mapSpecifierToPath(fromFile: string, spec: string): string | null { ) { return null; } - if (!trimmed.startsWith(".") && !trimmed.startsWith("@/") && !isSupportedWorkspaceAlias) { + if ( + !trimmed.startsWith(".") && + !trimmed.startsWith("@/") && + !isSupportedWorkspaceAlias + ) { return null; } if (trimmed === "@elizaos/cloud-ui") { return join(ROOT, "packages/ui/src/index.ts"); } if (trimmed.startsWith("@elizaos/cloud-ui/")) { - return join(ROOT, "packages/ui/src", trimmed.slice("@elizaos/cloud-ui/".length)); + return join( + ROOT, + "packages/ui/src", + trimmed.slice("@elizaos/cloud-ui/".length), + ); } if (trimmed.startsWith("@/lib/")) { return join(ROOT, "packages/lib", trimmed.slice("@/lib/".length)); @@ -175,7 +201,11 @@ function mapSpecifierToPath(fromFile: string, spec: string): string | null { return join(ROOT, "packages/types", trimmed.slice("@/types/".length)); } if (trimmed.startsWith("@/components/")) { - return join(ROOT, "packages/ui/src/components", trimmed.slice("@/components/".length)); + return join( + ROOT, + "packages/ui/src/components", + trimmed.slice("@/components/".length), + ); } if (trimmed.startsWith("@/")) { return join(ROOT, trimmed.slice(2)); @@ -207,10 +237,14 @@ function tryResolveFile(basePath: string): string | null { /** Path-like specifiers only (relative, @/, @elizaos/). Skips bare npm packages. */ const RE_FROM = /\bfrom\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]/g; /** Side-effect: import "@/foo"; (no named binding / no "from") */ -const RE_IMPORT_SIDE = /(?:^|[;\n])\s*import\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]\s*;?/gm; -const RE_IMPORT_CALL = /import\s*\(\s*['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]\s*\)/g; -const RE_EXPORT_STAR = /export\s+\*\s+from\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]/g; -const RE_EXPORT_NAMED = /export\s*\{[^}]+\}\s+from\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]/g; +const RE_IMPORT_SIDE = + /(?:^|[;\n])\s*import\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]\s*;?/gm; +const RE_IMPORT_CALL = + /import\s*\(\s*['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]\s*\)/g; +const RE_EXPORT_STAR = + /export\s+\*\s+from\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]/g; +const RE_EXPORT_NAMED = + /export\s*\{[^}]+\}\s+from\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]/g; const RE_EXPORT_NAMED_TYPE = /export\s+type\s+\{[^}]+\}\s+from\s+['"]((?:\.{1,2}\/|@\/|@elizaos\/)[^'"]+)['"]/g; @@ -263,7 +297,11 @@ function bfs(entries: string[]): Set { return visited; } -function walkMatch(dir: string, match: (abs: string, rel: string) => boolean, out: string[]): void { +function walkMatch( + dir: string, + match: (abs: string, rel: string) => boolean, + out: string[], +): void { if (!existsSync(dir)) return; for (const name of readdirSync(dir)) { if (SKIP_DIRS.has(name)) continue; @@ -284,7 +322,9 @@ function collectTestEntries(): string[] { ); walkMatch( join(ROOT, "packages/lib"), - (abs, rel) => rel.endsWith(".test.ts") || (rel.includes("__tests__/") && abs.endsWith(".ts")), + (abs, rel) => + rel.endsWith(".test.ts") || + (rel.includes("__tests__/") && abs.endsWith(".ts")), entries, ); walkMatch( @@ -330,9 +370,13 @@ function main(): void { console.log( "Dynamic import(string), require(variable), next/dynamic with variables, and runtime-only loads are not traced.", ); - console.log("Barrels: export * / export {} from in visited files ARE followed.\n"); + console.log( + "Barrels: export * / export {} from in visited files ARE followed.\n", + ); - console.log(`--- A) Unreachable from production graph (${prodDead.length} files) ---`); + console.log( + `--- A) Unreachable from production graph (${prodDead.length} files) ---`, + ); for (const p of prodDead) console.log(relative(ROOT, p)); console.log( diff --git a/packages/scripts/generate-jwt-keys.ts b/packages/scripts/generate-jwt-keys.ts index 09aa8ec28..dfd7b4507 100644 --- a/packages/scripts/generate-jwt-keys.ts +++ b/packages/scripts/generate-jwt-keys.ts @@ -10,12 +10,17 @@ const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256", }); -const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }) as string; +const privatePem = privateKey.export({ + type: "pkcs8", + format: "pem", +}) as string; const publicPem = publicKey.export({ type: "spki", format: "pem" }) as string; const privateB64 = Buffer.from(privatePem, "utf8").toString("base64"); const publicB64 = Buffer.from(publicPem, "utf8").toString("base64"); -console.log("# Add these to your .env (ES256 PKCS#8 / SPKI, base64-encoded PEM)\n"); +console.log( + "# Add these to your .env (ES256 PKCS#8 / SPKI, base64-encoded PEM)\n", +); console.log(`JWT_SIGNING_PRIVATE_KEY=${privateB64}`); console.log(`JWT_SIGNING_PUBLIC_KEY=${publicB64}`); diff --git a/packages/scripts/generate-llms-txt.ts b/packages/scripts/generate-llms-txt.ts index 873009bcc..0d189d2e8 100644 --- a/packages/scripts/generate-llms-txt.ts +++ b/packages/scripts/generate-llms-txt.ts @@ -64,7 +64,10 @@ function mdxToMarkdown(source: string): string { s = s.replace(/^\s*export\s+.*$/gm, ""); // Convert simple inline HTML code tags to markdown backticks - s = s.replace(/([\s\S]*?)<\/code>/g, (_m, inner) => `\`${String(inner).trim()}\``); + s = s.replace( + /([\s\S]*?)<\/code>/g, + (_m, inner) => `\`${String(inner).trim()}\``, + ); // Drop JSX component tags (Callout, Tabs, Steps, Cards, etc.) // We remove the tags but keep the inner markdown. @@ -115,7 +118,8 @@ function toDocsUrlPath(contentDir: string, filePath: string): string { function normalizeBaseUrl(raw?: string): string { const base = (raw || "").trim(); if (!base) return "https://cloud.milady.ai"; - if (base.startsWith("http://") || base.startsWith("https://")) return base.replace(/\/+$/, ""); + if (base.startsWith("http://") || base.startsWith("https://")) + return base.replace(/\/+$/, ""); // If someone passes a bare host (e.g. VERCEL_URL), treat as https return `https://${base}`.replace(/\/+$/, ""); } @@ -187,7 +191,9 @@ async function main() { const wellKnownDir = path.join(publicDir, ".well-known"); const baseUrl = normalizeBaseUrl( - process.env.NEXT_PUBLIC_APP_URL || process.env.NEXT_PUBLIC_SITE_URL || process.env.VERCEL_URL, + process.env.NEXT_PUBLIC_APP_URL || + process.env.NEXT_PUBLIC_SITE_URL || + process.env.VERCEL_URL, ); const files = await walk(contentDir); diff --git a/packages/scripts/postinstall.mjs b/packages/scripts/postinstall.mjs index 7bb68e626..a49e7afca 100644 --- a/packages/scripts/postinstall.mjs +++ b/packages/scripts/postinstall.mjs @@ -5,9 +5,33 @@ import path from "node:path"; const repoRoot = process.cwd(); const coreDistFiles = [ - path.join(repoRoot, "node_modules", "@elizaos", "core", "dist", "browser", "index.browser.js"), - path.join(repoRoot, "node_modules", "@elizaos", "core", "dist", "edge", "index.edge.js"), - path.join(repoRoot, "node_modules", "@elizaos", "core", "dist", "node", "index.node.js"), + path.join( + repoRoot, + "node_modules", + "@elizaos", + "core", + "dist", + "browser", + "index.browser.js", + ), + path.join( + repoRoot, + "node_modules", + "@elizaos", + "core", + "dist", + "edge", + "index.edge.js", + ), + path.join( + repoRoot, + "node_modules", + "@elizaos", + "core", + "dist", + "node", + "index.node.js", + ), ]; let patchedFiles = 0; diff --git a/packages/scripts/promote-admin.ts b/packages/scripts/promote-admin.ts index 7514f9185..fb2a6d4e0 100644 --- a/packages/scripts/promote-admin.ts +++ b/packages/scripts/promote-admin.ts @@ -83,7 +83,8 @@ Special Commands: // Promote admin const walletAddress = args[0]; - const role = (args[1] as "super_admin" | "moderator" | "viewer") || "moderator"; + const role = + (args[1] as "super_admin" | "moderator" | "viewer") || "moderator"; const notes = args.slice(2).join(" ") || undefined; // Validate wallet address format @@ -97,7 +98,9 @@ Special Commands: // Validate role const validRoles = ["super_admin", "moderator", "viewer"]; if (!validRoles.includes(role)) { - console.error(`❌ Error: Invalid role. Must be one of: ${validRoles.join(", ")}`); + console.error( + `❌ Error: Invalid role. Must be one of: ${validRoles.join(", ")}`, + ); process.exit(1); } diff --git a/packages/scripts/provision-waifu-tenant.ts b/packages/scripts/provision-waifu-tenant.ts index 77a060d46..da3994662 100644 --- a/packages/scripts/provision-waifu-tenant.ts +++ b/packages/scripts/provision-waifu-tenant.ts @@ -8,7 +8,9 @@ */ const STEWARD_URL = process.env.STEWARD_API_URL || "http://localhost:3200"; -const PLATFORM_KEY = (process.env.STEWARD_PLATFORM_KEYS ?? "").split(",")[0].trim(); +const PLATFORM_KEY = (process.env.STEWARD_PLATFORM_KEYS ?? "") + .split(",")[0] + .trim(); if (!PLATFORM_KEY) { console.error("STEWARD_PLATFORM_KEYS is required"); diff --git a/packages/scripts/repair-drizzle-journal.ts b/packages/scripts/repair-drizzle-journal.ts index 17b919e80..156934932 100644 --- a/packages/scripts/repair-drizzle-journal.ts +++ b/packages/scripts/repair-drizzle-journal.ts @@ -31,7 +31,10 @@ interface Journal { entries: JournalEntry[]; } -const JOURNAL_PATH = path.join(process.cwd(), "packages/db/migrations/meta/_journal.json"); +const JOURNAL_PATH = path.join( + process.cwd(), + "packages/db/migrations/meta/_journal.json", +); // idx 43 intentionally maps back to 0043. The duplicate 0043 was introduced // later in the journal history, so repairing a stale journal can look like a @@ -64,7 +67,9 @@ async function main() { } if (changed === 0) { - console.log("Drizzle journal already matches the current migration filenames."); + console.log( + "Drizzle journal already matches the current migration filenames.", + ); return; } diff --git a/packages/scripts/seed-credit-packs.ts b/packages/scripts/seed-credit-packs.ts index 2e015d792..70dd81f8f 100644 --- a/packages/scripts/seed-credit-packs.ts +++ b/packages/scripts/seed-credit-packs.ts @@ -39,7 +39,10 @@ async function seedCreditPacks() { for (const pack of creditPacks) { try { - const [result] = await db.insert(creditPacksTable).values(pack).returning(); + const [result] = await db + .insert(creditPacksTable) + .values(pack) + .returning(); console.log(`✓ Created: ${pack.name} (${result.id})`); } catch (error) { console.error(`✗ Failed to create ${pack.name}:`, error); diff --git a/packages/scripts/seed-local-dev.ts b/packages/scripts/seed-local-dev.ts index 3f97181cc..7660ca0a6 100644 --- a/packages/scripts/seed-local-dev.ts +++ b/packages/scripts/seed-local-dev.ts @@ -77,8 +77,10 @@ async function seedLocalDev() { description: "50,000 credits for AI generations", credits: 50000, price_cents: 4999, - stripe_price_id: process.env.STRIPE_SMALL_PACK_PRICE_ID || "price_test_small", - stripe_product_id: process.env.STRIPE_SMALL_PACK_PRODUCT_ID || "prod_test_small", + stripe_price_id: + process.env.STRIPE_SMALL_PACK_PRICE_ID || "price_test_small", + stripe_product_id: + process.env.STRIPE_SMALL_PACK_PRODUCT_ID || "prod_test_small", sort_order: 1, }, { @@ -86,8 +88,10 @@ async function seedLocalDev() { description: "150,000 credits for AI generations", credits: 150000, price_cents: 12999, - stripe_price_id: process.env.STRIPE_MEDIUM_PACK_PRICE_ID || "price_test_medium", - stripe_product_id: process.env.STRIPE_MEDIUM_PACK_PRODUCT_ID || "prod_test_medium", + stripe_price_id: + process.env.STRIPE_MEDIUM_PACK_PRICE_ID || "price_test_medium", + stripe_product_id: + process.env.STRIPE_MEDIUM_PACK_PRODUCT_ID || "prod_test_medium", sort_order: 2, }, { @@ -95,8 +99,10 @@ async function seedLocalDev() { description: "500,000 credits for AI generations", credits: 500000, price_cents: 39999, - stripe_price_id: process.env.STRIPE_LARGE_PACK_PRICE_ID || "price_test_large", - stripe_product_id: process.env.STRIPE_LARGE_PACK_PRODUCT_ID || "prod_test_large", + stripe_price_id: + process.env.STRIPE_LARGE_PACK_PRICE_ID || "price_test_large", + stripe_product_id: + process.env.STRIPE_LARGE_PACK_PRODUCT_ID || "prod_test_large", sort_order: 3, }, ]; @@ -160,7 +166,10 @@ async function seedLocalDev() { console.log(" 5. Run: bun run dev"); console.log(" 6. Open fresh tab: http://localhost:3000"); } catch (error) { - console.error("\n❌ Seeding failed:", error instanceof Error ? error.message : String(error)); + console.error( + "\n❌ Seeding failed:", + error instanceof Error ? error.message : String(error), + ); process.exit(1); } } diff --git a/packages/scripts/setup-local-db-comprehensive.ts b/packages/scripts/setup-local-db-comprehensive.ts index 20bf927dc..fcf2d3d97 100644 --- a/packages/scripts/setup-local-db-comprehensive.ts +++ b/packages/scripts/setup-local-db-comprehensive.ts @@ -94,7 +94,9 @@ async function checkPgVector() { await client.connect(); // Check if extension exists - const result = await client.query("SELECT * FROM pg_extension WHERE extname = 'vector'"); + const result = await client.query( + "SELECT * FROM pg_extension WHERE extname = 'vector'", + ); if (result.rows.length > 0) { log("✓ pgvector extension is installed"); @@ -175,7 +177,9 @@ async function checkTables() { await client.end(); const existingTables = result.rows.map((row) => row.table_name); - const missingTables = requiredTables.filter((table) => !existingTables.includes(table)); + const missingTables = requiredTables.filter( + (table) => !existingTables.includes(table), + ); if (missingTables.length === 0) { log(`✓ All required tables exist (${requiredTables.length} tables)`); @@ -276,7 +280,9 @@ async function main() { console.log("Your local database is ready!"); console.log(`Connection string: ${LOCAL_DATABASE_URL}`); console.log("\nNext steps:"); - console.log("1. Make sure DATABASE_URL in .env.local points to the local database"); + console.log( + "1. Make sure DATABASE_URL in .env.local points to the local database", + ); console.log("2. Run 'bun run dev' to start the development server"); console.log("3. Visit http://localhost:3000/dashboard"); console.log("\nUseful commands:"); diff --git a/packages/scripts/setup.ts b/packages/scripts/setup.ts index 21467100b..c6952874b 100644 --- a/packages/scripts/setup.ts +++ b/packages/scripts/setup.ts @@ -93,7 +93,9 @@ interface ConfigStatus { wallet: { configured: boolean; address?: string; balance?: string }; } -async function checkConfiguration(env: Record): Promise { +async function checkConfiguration( + env: Record, +): Promise { const status: ConfigStatus = { database: { configured: false }, auth: { configured: false, provider: "privy" }, @@ -136,7 +138,10 @@ async function checkConfiguration(env: Record): Promise): Promise { { key: "PRIVY_APP_SECRET", hint: "From Privy dashboard" }, ]; - const missing = required.filter((r) => !env[r.key] || isPlaceholderValue(env[r.key])); + const missing = required.filter( + (r) => !env[r.key] || isPlaceholderValue(env[r.key]), + ); if (missing.length > 0) { console.log("\n⚠️ Required configuration missing:"); for (const m of missing) { console.log(` ${m.key} - ${m.hint}`); } - console.log("\n Edit .env.local to add these values, then run setup again."); + console.log( + "\n Edit .env.local to add these values, then run setup again.", + ); } } @@ -269,7 +282,8 @@ async function main() { console.log("\n🚀 Next Steps"); console.log("=============="); - const missingRequired = !status.database.configured || !status.auth.configured; + const missingRequired = + !status.database.configured || !status.auth.configured; if (missingRequired) { console.log(" 1. Add required configuration to .env.local"); diff --git a/packages/services/billing/src/markup.ts b/packages/services/billing/src/markup.ts index 647185e95..c210aa278 100644 --- a/packages/services/billing/src/markup.ts +++ b/packages/services/billing/src/markup.ts @@ -36,16 +36,22 @@ export interface MarkupBreakdown { function assertValidRate(markupRate: number): void { if (!Number.isFinite(markupRate)) { - throw new RangeError(`markupRate must be a finite number, received ${markupRate}`); + throw new RangeError( + `markupRate must be a finite number, received ${markupRate}`, + ); } if (markupRate < 0) { - throw new RangeError(`markupRate must be non-negative, received ${markupRate}`); + throw new RangeError( + `markupRate must be non-negative, received ${markupRate}`, + ); } } function assertValidCost(cost: number, fieldName: string): void { if (!Number.isFinite(cost)) { - throw new RangeError(`${fieldName} must be a finite number, received ${cost}`); + throw new RangeError( + `${fieldName} must be a finite number, received ${cost}`, + ); } if (cost < 0) { throw new RangeError(`${fieldName} must be non-negative, received ${cost}`); diff --git a/packages/services/gateway-discord/src/gateway-manager.ts b/packages/services/gateway-discord/src/gateway-manager.ts index 0d5d0806d..8e6a111b8 100644 --- a/packages/services/gateway-discord/src/gateway-manager.ts +++ b/packages/services/gateway-discord/src/gateway-manager.ts @@ -19,8 +19,15 @@ import { type User, } from "discord.js"; import { logger } from "./logger"; -import { forwardToServer, refreshKedaActivity, resolveAgentServer } from "./server-router"; -import { hasVoiceAttachments, VoiceMessageHandler } from "./voice-message-handler"; +import { + forwardToServer, + refreshKedaActivity, + resolveAgentServer, +} from "./server-router"; +import { + hasVoiceAttachments, + VoiceMessageHandler, +} from "./voice-message-handler"; // ============================================ // Helpers @@ -34,7 +41,8 @@ import { hasVoiceAttachments, VoiceMessageHandler } from "./voice-message-handle * - Part 2 (timestamp): 5+ characters * - Part 3 (HMAC): 20+ characters */ -const DISCORD_TOKEN_PATTERN = /[A-Za-z0-9_-]{15,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{20,}/g; +const DISCORD_TOKEN_PATTERN = + /[A-Za-z0-9_-]{15,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{20,}/g; /** * Sanitize error messages to prevent accidental token exposure in logs. @@ -49,12 +57,18 @@ function sanitizeError(error: unknown): string { * Parse an integer from environment variable with validation. * Throws if the value is not a valid integer or below minimum to fail fast on misconfiguration. */ -function parseIntEnv(name: string, defaultValue: number, minValue: number = 1): number { +function parseIntEnv( + name: string, + defaultValue: number, + minValue: number = 1, +): number { const value = process.env[name]; if (value === undefined) return defaultValue; const parsed = parseInt(value, 10); if (Number.isNaN(parsed)) { - throw new Error(`Invalid ${name} environment variable: "${value}" is not a valid integer`); + throw new Error( + `Invalid ${name} environment variable: "${value}" is not a valid integer`, + ); } if (parsed < minValue) { throw new Error( @@ -127,7 +141,10 @@ const FAILOVER_LOCK_TTL_SECONDS = 30; * * The threshold should be at least 2x heartbeat interval to avoid false positives. */ -const FAILOVER_CHECK_INTERVAL_MS = parseIntEnv("FAILOVER_CHECK_INTERVAL_MS", 30_000); +const FAILOVER_CHECK_INTERVAL_MS = parseIntEnv( + "FAILOVER_CHECK_INTERVAL_MS", + 30_000, +); const DEAD_POD_THRESHOLD_MS = parseIntEnv("DEAD_POD_THRESHOLD_MS", 45_000); /** Maximum bots per pod - prevents resource exhaustion */ @@ -142,7 +159,8 @@ const MAX_BOTS_PER_POD = parseIntEnv("MAX_BOTS_PER_POD", 100); * Configurable via ELIZA_APP_LEADER_KEY env var to prevent collisions * when sharing Redis across multiple environments (prod/staging/local). */ -const ELIZA_APP_LEADER_KEY = process.env.ELIZA_APP_LEADER_KEY || "discord:eliza-app-bot:leader"; +const ELIZA_APP_LEADER_KEY = + process.env.ELIZA_APP_LEADER_KEY || "discord:eliza-app-bot:leader"; /** Leader election lock TTL in seconds (10 seconds) */ const ELIZA_APP_LEADER_TTL_SECONDS = 10; @@ -161,7 +179,13 @@ const ELIZA_APP_LEADER_CHECK_INTERVAL_MS = 3000; const escapePrometheusLabel = (value: string): string => value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"'); -const metric = (name: string, type: string, help: string, pod: string, value: number): string => { +const metric = ( + name: string, + type: string, + help: string, + pod: string, + value: number, +): string => { const escapedPod = escapePrometheusLabel(pod); return `# HELP ${name} ${help}\n# TYPE ${name} ${type}\n${name}{pod="${escapedPod}"} ${value}`; }; @@ -522,14 +546,17 @@ export class GatewayManager { // Release all connections in database so other pods can pick them up immediately // This is critical for graceful shutdowns (deployments, scaling) to avoid message loss try { - await fetchWithTimeout(`${this.config.elizaCloudUrl}/api/internal/discord/gateway/shutdown`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...this.getAuthHeader(), + await fetchWithTimeout( + `${this.config.elizaCloudUrl}/api/internal/discord/gateway/shutdown`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...this.getAuthHeader(), + }, + body: JSON.stringify({ pod_name: this.config.podName }), }, - body: JSON.stringify({ pod_name: this.config.podName }), - }); + ); logger.info("Released connections in database", { podName: this.config.podName, }); @@ -584,17 +611,20 @@ export class GatewayManager { // Notify backend that this pod is draining so it can reassign bots // This is proactive - doesn't wait for heartbeat timeout try { - await fetchWithTimeout(`${this.config.elizaCloudUrl}/api/internal/discord/gateway/drain`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...this.getAuthHeader(), + await fetchWithTimeout( + `${this.config.elizaCloudUrl}/api/internal/discord/gateway/drain`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...this.getAuthHeader(), + }, + body: JSON.stringify({ + pod_name: this.config.podName, + connection_ids: Array.from(this.connections.keys()), + }), }, - body: JSON.stringify({ - pod_name: this.config.podName, - connection_ids: Array.from(this.connections.keys()), - }), - }); + ); logger.info("Notified backend of draining state", { podName: this.config.podName, }); @@ -635,7 +665,9 @@ export class GatewayManager { try { // Include current/max counts so backend only claims new connections if we have capacity const currentCount = this.connections.size; - const url = new URL(`${this.config.elizaCloudUrl}/api/internal/discord/gateway/assignments`); + const url = new URL( + `${this.config.elizaCloudUrl}/api/internal/discord/gateway/assignments`, + ); url.searchParams.set("pod", this.config.podName); url.searchParams.set("current", currentCount.toString()); url.searchParams.set("max", MAX_BOTS_PER_POD.toString()); @@ -678,11 +710,14 @@ export class GatewayManager { // Check capacity and reserve slot atomically to prevent TOCTOU race // Without this, concurrent operations could both pass the check before either adds to the map if (this.connections.size >= MAX_BOTS_PER_POD) { - logger.warn("MAX_BOTS_PER_POD limit reached, skipping remaining assignments", { - currentBots: this.connections.size, - maxBots: MAX_BOTS_PER_POD, - skippedConnectionId: assignment.connectionId, - }); + logger.warn( + "MAX_BOTS_PER_POD limit reached, skipping remaining assignments", + { + currentBots: this.connections.size, + maxBots: MAX_BOTS_PER_POD, + skippedConnectionId: assignment.connectionId, + }, + ); break; } @@ -799,7 +834,10 @@ export class GatewayManager { }); } }; - conn.listeners.set(eventName, wrappedHandler as unknown as (...args: unknown[]) => void); + conn.listeners.set( + eventName, + wrappedHandler as unknown as (...args: unknown[]) => void, + ); return wrappedHandler; }; @@ -837,52 +875,76 @@ export class GatewayManager { Events.MessageUpdate, createHandler( Events.MessageUpdate, - async (_oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => { + async ( + _oldMessage: Message | PartialMessage, + newMessage: Message | PartialMessage, + ) => { conn.eventsReceived++; if (newMessage.partial) return; - await this.forwardEvent(assignment.connectionId, conn, "MESSAGE_UPDATE", { - id: newMessage.id, - channel_id: newMessage.channelId, - guild_id: newMessage.guildId, - content: newMessage.content, - edited_timestamp: newMessage.editedAt?.toISOString(), - author: newMessage.author - ? { - id: newMessage.author.id, - username: newMessage.author.username, - bot: newMessage.author.bot, - } - : undefined, - }); + await this.forwardEvent( + assignment.connectionId, + conn, + "MESSAGE_UPDATE", + { + id: newMessage.id, + channel_id: newMessage.channelId, + guild_id: newMessage.guildId, + content: newMessage.content, + edited_timestamp: newMessage.editedAt?.toISOString(), + author: newMessage.author + ? { + id: newMessage.author.id, + username: newMessage.author.username, + bot: newMessage.author.bot, + } + : undefined, + }, + ); }, ), ); client.on( Events.MessageDelete, - createHandler(Events.MessageDelete, async (message: Message | PartialMessage) => { - conn.eventsReceived++; - await this.forwardEvent(assignment.connectionId, conn, "MESSAGE_DELETE", { - id: message.id, - channel_id: message.channelId, - guild_id: message.guildId, - }); - }), + createHandler( + Events.MessageDelete, + async (message: Message | PartialMessage) => { + conn.eventsReceived++; + await this.forwardEvent( + assignment.connectionId, + conn, + "MESSAGE_DELETE", + { + id: message.id, + channel_id: message.channelId, + guild_id: message.guildId, + }, + ); + }, + ), ); client.on( Events.MessageReactionAdd, createHandler( Events.MessageReactionAdd, - async (reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser) => { + async ( + reaction: MessageReaction | PartialMessageReaction, + user: User | PartialUser, + ) => { conn.eventsReceived++; - await this.forwardEvent(assignment.connectionId, conn, "MESSAGE_REACTION_ADD", { - message_id: reaction.message.id, - channel_id: reaction.message.channelId, - guild_id: reaction.message.guildId, - emoji: { name: reaction.emoji.name, id: reaction.emoji.id }, - user_id: user.id, - }); + await this.forwardEvent( + assignment.connectionId, + conn, + "MESSAGE_REACTION_ADD", + { + message_id: reaction.message.id, + channel_id: reaction.message.channelId, + guild_id: reaction.message.guildId, + emoji: { name: reaction.emoji.name, id: reaction.emoji.id }, + user_id: user.id, + }, + ); }, ), ); @@ -891,59 +953,80 @@ export class GatewayManager { Events.GuildMemberAdd, createHandler(Events.GuildMemberAdd, async (member: GuildMember) => { conn.eventsReceived++; - await this.forwardEvent(assignment.connectionId, conn, "GUILD_MEMBER_ADD", { - guild_id: member.guild.id, - user: { - id: member.user.id, - username: member.user.username, - discriminator: member.user.discriminator, - avatar: member.user.avatar, - bot: member.user.bot, + await this.forwardEvent( + assignment.connectionId, + conn, + "GUILD_MEMBER_ADD", + { + guild_id: member.guild.id, + user: { + id: member.user.id, + username: member.user.username, + discriminator: member.user.discriminator, + avatar: member.user.avatar, + bot: member.user.bot, + }, + nick: member.nickname, + roles: member.roles.cache.map((r: Role) => r.id), + joined_at: member.joinedAt?.toISOString(), }, - nick: member.nickname, - roles: member.roles.cache.map((r: Role) => r.id), - joined_at: member.joinedAt?.toISOString(), - }); + ); }), ); client.on( Events.GuildMemberRemove, - createHandler(Events.GuildMemberRemove, async (member: GuildMember | PartialGuildMember) => { - conn.eventsReceived++; - await this.forwardEvent(assignment.connectionId, conn, "GUILD_MEMBER_REMOVE", { - guild_id: member.guild.id, - user: { - id: member.user.id, - username: member.user.username, - bot: member.user.bot, - }, - }); - }), + createHandler( + Events.GuildMemberRemove, + async (member: GuildMember | PartialGuildMember) => { + conn.eventsReceived++; + await this.forwardEvent( + assignment.connectionId, + conn, + "GUILD_MEMBER_REMOVE", + { + guild_id: member.guild.id, + user: { + id: member.user.id, + username: member.user.username, + bot: member.user.bot, + }, + }, + ); + }, + ), ); client.on( Events.InteractionCreate, - createHandler(Events.InteractionCreate, async (interaction: Interaction) => { - conn.eventsReceived++; - await this.forwardEvent(assignment.connectionId, conn, "INTERACTION_CREATE", { - id: interaction.id, - type: interaction.type, - channel_id: interaction.channelId, - guild_id: interaction.guildId, - user: { - id: interaction.user.id, - username: interaction.user.username, - bot: interaction.user.bot, - }, - data: interaction.isChatInputCommand() - ? { - name: interaction.commandName, - options: interaction.options.data, - } - : undefined, - }); - }), + createHandler( + Events.InteractionCreate, + async (interaction: Interaction) => { + conn.eventsReceived++; + await this.forwardEvent( + assignment.connectionId, + conn, + "INTERACTION_CREATE", + { + id: interaction.id, + type: interaction.type, + channel_id: interaction.channelId, + guild_id: interaction.guildId, + user: { + id: interaction.user.id, + username: interaction.user.username, + bot: interaction.user.bot, + }, + data: interaction.isChatInputCommand() + ? { + name: interaction.commandName, + options: interaction.options.data, + } + : undefined, + }, + ); + }, + ), ); client.on( @@ -956,7 +1039,11 @@ export class GatewayManager { connectionId: assignment.connectionId, error: error.message, }); - await this.updateConnectionStatus(assignment.connectionId, "error", error.message); + await this.updateConnectionStatus( + assignment.connectionId, + "error", + error.message, + ); }), ); @@ -968,7 +1055,10 @@ export class GatewayManager { logger.warn("Bot disconnected", { connectionId: assignment.connectionId, }); - await this.updateConnectionStatus(assignment.connectionId, "disconnected"); + await this.updateConnectionStatus( + assignment.connectionId, + "disconnected", + ); }), ); @@ -997,7 +1087,11 @@ export class GatewayManager { this.connections.delete(assignment.connectionId); // Update status in database - will allow reassignment on next poll - await this.updateConnectionStatus(assignment.connectionId, "error", errorMessage); + await this.updateConnectionStatus( + assignment.connectionId, + "error", + errorMessage, + ); } } @@ -1032,7 +1126,10 @@ export class GatewayManager { for (const [connectionId, conn] of this.connections) { // Only cleanup disconnected/error connections with a timestamp - if ((conn.status === "disconnected" || conn.status === "error") && conn.statusChangedAt) { + if ( + (conn.status === "disconnected" || conn.status === "error") && + conn.statusChangedAt + ) { const staleDuration = now - conn.statusChangedAt.getTime(); if (staleDuration > STALE_CONNECTION_THRESHOLD_MS) { staleConnections.push(connectionId); @@ -1060,7 +1157,10 @@ export class GatewayManager { } } - private async handleMessage(connectionId: string, message: Message): Promise { + private async handleMessage( + connectionId: string, + message: Message, + ): Promise { if (message.author.bot) return; const conn = this.connections.get(connectionId); @@ -1104,7 +1204,9 @@ export class GatewayManager { username: u.username, bot: u.bot, })), - referenced_message: message.reference ? { id: message.reference.messageId } : undefined, + referenced_message: message.reference + ? { id: message.reference.messageId } + : undefined, }; if ( @@ -1112,12 +1214,13 @@ export class GatewayManager { hasVoiceAttachments(message.attachments, message.flags) ) { try { - const voiceAttachments = await this.voiceHandler.processVoiceAttachments( - message.attachments, - connectionId, - message.id, - message.flags, - ); + const voiceAttachments = + await this.voiceHandler.processVoiceAttachments( + message.attachments, + connectionId, + message.id, + message.flags, + ); if (voiceAttachments.length > 0) { eventData.voice_attachments = voiceAttachments; @@ -1127,11 +1230,14 @@ export class GatewayManager { count: voiceAttachments.length, }); } else { - logger.warn("Voice attachments detected but none processed successfully", { - connectionId, - messageId: message.id, - attachmentCount: message.attachments.size, - }); + logger.warn( + "Voice attachments detected but none processed successfully", + { + connectionId, + messageId: message.id, + attachmentCount: message.attachments.size, + }, + ); } } catch (error) { logger.error("Failed to process voice attachments", { @@ -1183,7 +1289,8 @@ export class GatewayManager { ); if (response) { - const truncated = response.length > 2000 ? response.slice(0, 2000) : response; + const truncated = + response.length > 2000 ? response.slice(0, 2000) : response; await message.reply(truncated); } @@ -1280,21 +1387,24 @@ export class GatewayManager { botUserId?: string, ): Promise { try { - await fetchWithTimeout(`${this.config.elizaCloudUrl}/api/internal/discord/gateway/status`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...this.getAuthHeader(), + await fetchWithTimeout( + `${this.config.elizaCloudUrl}/api/internal/discord/gateway/status`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...this.getAuthHeader(), + }, + body: JSON.stringify({ + connection_id: connectionId, + pod_name: this.config.podName, + status, + error_message: errorMessage, + // Bot user ID for mention detection (different from application_id) + bot_user_id: botUserId, + }), }, - body: JSON.stringify({ - connection_id: connectionId, - pod_name: this.config.podName, - status, - error_message: errorMessage, - // Bot user ID for mention detection (different from application_id) - bot_user_id: botUserId, - }), - }); + ); } catch (error) { logger.error("Failed to update connection status", { connectionId, @@ -1304,7 +1414,10 @@ export class GatewayManager { } } - private async saveSessionState(connectionId: string, conn: BotConnection): Promise { + private async saveSessionState( + connectionId: string, + conn: BotConnection, + ): Promise { if (!this.redis) return; const state = { @@ -1342,12 +1455,14 @@ export class GatewayManager { if (this.connections.size > 0) { try { // Collect stats for each connection - const connectionStats = Array.from(this.connections.entries()).map(([id, conn]) => ({ - id, - guildCount: conn.guildCount, - eventsReceived: conn.eventsReceived, - eventsRouted: conn.eventsRouted, - })); + const connectionStats = Array.from(this.connections.entries()).map( + ([id, conn]) => ({ + id, + guildCount: conn.guildCount, + eventsReceived: conn.eventsReceived, + eventsRouted: conn.eventsRouted, + }), + ); await fetchWithTimeout( `${this.config.elizaCloudUrl}/api/internal/discord/gateway/heartbeat`, @@ -1417,7 +1532,8 @@ export class GatewayManager { continue; } - const state = typeof podState === "string" ? JSON.parse(podState) : podState; + const state = + typeof podState === "string" ? JSON.parse(podState) : podState; const timeSinceHeartbeat = Date.now() - state.lastHeartbeat; if (timeSinceHeartbeat > DEAD_POD_THRESHOLD_MS) { @@ -1519,18 +1635,24 @@ export class GatewayManager { // Require explicit opt-in via ELIZA_APP_DISCORD_BOT_ENABLED const enabled = process.env.ELIZA_APP_DISCORD_BOT_ENABLED === "true"; if (!enabled) { - logger.debug("Eliza App Discord bot disabled (ELIZA_APP_DISCORD_BOT_ENABLED not set)"); + logger.debug( + "Eliza App Discord bot disabled (ELIZA_APP_DISCORD_BOT_ENABLED not set)", + ); return; } const botToken = process.env.ELIZA_APP_DISCORD_BOT_TOKEN; if (!botToken) { - logger.error("Eliza App Discord bot enabled but ELIZA_APP_DISCORD_BOT_TOKEN not set"); + logger.error( + "Eliza App Discord bot enabled but ELIZA_APP_DISCORD_BOT_TOKEN not set", + ); return; } if (!this.redis) { - logger.warn("Eliza App bot leader election requires Redis - bot disabled"); + logger.warn( + "Eliza App bot leader election requires Redis - bot disabled", + ); return; } @@ -1562,12 +1684,18 @@ export class GatewayManager { try { if (this.isElizaAppLeader) { // Already leader - renew the lock - const renewed = await this.redis.expire(ELIZA_APP_LEADER_KEY, ELIZA_APP_LEADER_TTL_SECONDS); + const renewed = await this.redis.expire( + ELIZA_APP_LEADER_KEY, + ELIZA_APP_LEADER_TTL_SECONDS, + ); if (!renewed) { // Lock was lost (expired or deleted) - logger.warn("Lost Eliza App bot leadership, attempting to reacquire", { - podName: this.config.podName, - }); + logger.warn( + "Lost Eliza App bot leadership, attempting to reacquire", + { + podName: this.config.podName, + }, + ); this.isElizaAppLeader = false; await this.disconnectElizaAppBot(); } else { @@ -1579,10 +1707,14 @@ export class GatewayManager { } // Try to acquire leadership - const acquired = await this.redis.set(ELIZA_APP_LEADER_KEY, this.config.podName, { - ex: ELIZA_APP_LEADER_TTL_SECONDS, - nx: true, - }); + const acquired = await this.redis.set( + ELIZA_APP_LEADER_KEY, + this.config.podName, + { + ex: ELIZA_APP_LEADER_TTL_SECONDS, + nx: true, + }, + ); if (acquired === "OK") { logger.info("Acquired Eliza App bot leadership", { @@ -1695,7 +1827,9 @@ export class GatewayManager { await this.routeManagedMiladyMessage(message, trimmedContent); } - private async handleManagedMiladyGuildMessage(message: Message): Promise { + private async handleManagedMiladyGuildMessage( + message: Message, + ): Promise { const botUserId = this.elizaAppClient?.user?.id; if (!botUserId || !message.guildId) { return; @@ -1708,7 +1842,8 @@ export class GatewayManager { const botMentionRegex = new RegExp(`<@!?${botUserId}>`, "g"); const botMentioned = - message.mentions.users.has(botUserId) || botMentionRegex.test(trimmedContent); + message.mentions.users.has(botUserId) || + botMentionRegex.test(trimmedContent); if (!botMentioned) { return; } @@ -1717,8 +1852,14 @@ export class GatewayManager { (user: { id: string }) => user.id !== botUserId, ); const repliedUserId = message.mentions.repliedUser?.id; - const repliedToAnotherUser = Boolean(repliedUserId && repliedUserId !== botUserId); - if (mentionedOtherUser || message.mentions.everyone || repliedToAnotherUser) { + const repliedToAnotherUser = Boolean( + repliedUserId && repliedUserId !== botUserId, + ); + if ( + mentionedOtherUser || + message.mentions.everyone || + repliedToAnotherUser + ) { logger.debug("Ignoring managed guild message that targets someone else", { guildId: message.guildId, channelId: message.channelId, @@ -1735,7 +1876,10 @@ export class GatewayManager { await this.routeManagedMiladyMessage(message, sanitizedContent); } - private async routeManagedMiladyMessage(message: Message, content: string): Promise { + private async routeManagedMiladyMessage( + message: Message, + content: string, + ): Promise { try { if ("sendTyping" in message.channel) { await message.channel.sendTyping(); @@ -1757,7 +1901,10 @@ export class GatewayManager { sender: { id: message.author.id, username: message.author.username, - displayName: message.member?.displayName ?? message.author.globalName ?? undefined, + displayName: + message.member?.displayName ?? + message.author.globalName ?? + undefined, avatar: message.author.displayAvatarURL() || null, }, }), @@ -1798,7 +1945,8 @@ export class GatewayManager { } const replyText = routed.replyText.trim(); - const truncated = replyText.length > 2000 ? replyText.slice(0, 2000) : replyText; + const truncated = + replyText.length > 2000 ? replyText.slice(0, 2000) : replyText; await message.reply({ content: truncated, allowedMentions: { repliedUser: false }, @@ -1822,7 +1970,8 @@ export class GatewayManager { // Control plane connectivity affects health const CRITICAL_FAILURE_THRESHOLD = 5; - const controlPlaneLost = this.consecutivePollFailures >= CRITICAL_FAILURE_THRESHOLD; + const controlPlaneLost = + this.consecutivePollFailures >= CRITICAL_FAILURE_THRESHOLD; // Note: draining pods are still "healthy" for liveness (don't restart) // but will fail readiness (don't accept new work) @@ -1856,9 +2005,27 @@ export class GatewayManager { const pod = this.config.podName; const metrics = [ - metric("discord_gateway_bots_total", "gauge", "Total bots managed", pod, h.totalBots), - metric("discord_gateway_bots_connected", "gauge", "Connected bots", pod, h.connectedBots), - metric("discord_gateway_guilds_total", "gauge", "Total guilds", pod, h.totalGuilds), + metric( + "discord_gateway_bots_total", + "gauge", + "Total bots managed", + pod, + h.totalBots, + ), + metric( + "discord_gateway_bots_connected", + "gauge", + "Connected bots", + pod, + h.connectedBots, + ), + metric( + "discord_gateway_guilds_total", + "gauge", + "Total guilds", + pod, + h.totalGuilds, + ), metric( "discord_gateway_uptime_seconds", "gauge", @@ -1894,8 +2061,12 @@ export class GatewayManager { metrics.push( `discord_gateway_events_received{connection="${escapedId}"} ${conn.eventsReceived}`, ); - metrics.push(`discord_gateway_events_routed{connection="${escapedId}"} ${conn.eventsRouted}`); - metrics.push(`discord_gateway_events_failed{connection="${escapedId}"} ${conn.eventsFailed}`); + metrics.push( + `discord_gateway_events_routed{connection="${escapedId}"} ${conn.eventsRouted}`, + ); + metrics.push( + `discord_gateway_events_failed{connection="${escapedId}"} ${conn.eventsFailed}`, + ); } return metrics.join("\n"); diff --git a/packages/services/gateway-discord/src/hash-router.ts b/packages/services/gateway-discord/src/hash-router.ts index aa8b7899d..158432af1 100644 --- a/packages/services/gateway-discord/src/hash-router.ts +++ b/packages/services/gateway-discord/src/hash-router.ts @@ -18,7 +18,10 @@ let k8sCaCert: string | null = null; function getK8sToken(): string | null { if (k8sToken !== null) return k8sToken || null; try { - k8sToken = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf-8").trim(); + k8sToken = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/token", + "utf-8", + ).trim(); } catch { k8sToken = ""; } @@ -28,7 +31,10 @@ function getK8sToken(): string | null { function getK8sCaCert(): string | null { if (k8sCaCert !== null) return k8sCaCert || null; try { - k8sCaCert = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "utf-8"); + k8sCaCert = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + "utf-8", + ); } catch { k8sCaCert = ""; } @@ -61,7 +67,10 @@ interface EndpointSliceList { }>; } -async function resolvePodIPs(serviceName: string, namespace: string): Promise { +async function resolvePodIPs( + serviceName: string, + namespace: string, +): Promise { const apiUrl = `https://kubernetes.default.svc/apis/discovery.k8s.io/v1/namespaces/${namespace}/endpointslices?labelSelector=kubernetes.io/service-name=${serviceName}`; const token = getK8sToken(); diff --git a/packages/services/gateway-discord/src/index.ts b/packages/services/gateway-discord/src/index.ts index 99333c90e..f15046e58 100644 --- a/packages/services/gateway-discord/src/index.ts +++ b/packages/services/gateway-discord/src/index.ts @@ -43,7 +43,9 @@ const gatewayBootstrapSecret = process.env.GATEWAY_BOOTSTRAP_SECRET; // ELIZA_CLOUD_URL takes precedence, then falls back to NEXT_PUBLIC_APP_URL // This allows reusing the same env var as the main app for local development const elizaCloudUrl = - process.env.ELIZA_CLOUD_URL || process.env.NEXT_PUBLIC_APP_URL || "https://cloud.milady.ai"; + process.env.ELIZA_CLOUD_URL || + process.env.NEXT_PUBLIC_APP_URL || + "https://cloud.milady.ai"; const project = process.env.PROJECT ?? "cloud"; diff --git a/packages/services/gateway-discord/src/logger.ts b/packages/services/gateway-discord/src/logger.ts index 9336d3abf..5d7391d95 100644 --- a/packages/services/gateway-discord/src/logger.ts +++ b/packages/services/gateway-discord/src/logger.ts @@ -16,7 +16,11 @@ function shouldLog(level: LogLevel): boolean { return LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL]; } -function formatMessage(level: LogLevel, message: string, meta?: Record): string { +function formatMessage( + level: LogLevel, + message: string, + meta?: Record, +): string { const timestamp = new Date().toISOString(); const base = { timestamp, level, message, ...meta }; return JSON.stringify(base); @@ -33,6 +37,7 @@ export const logger = { if (shouldLog("warn")) console.warn(formatMessage("warn", message, meta)); }, error(message: string, meta?: Record) { - if (shouldLog("error")) console.error(formatMessage("error", message, meta)); + if (shouldLog("error")) + console.error(formatMessage("error", message, meta)); }, }; diff --git a/packages/services/gateway-discord/src/server-router.ts b/packages/services/gateway-discord/src/server-router.ts index 7275bde78..e3eaab46d 100644 --- a/packages/services/gateway-discord/src/server-router.ts +++ b/packages/services/gateway-discord/src/server-router.ts @@ -77,7 +77,10 @@ export async function resolveAgentServer( return { serverName, serverUrl }; } -export async function refreshKedaActivity(redis: Redis, serverName: string): Promise { +export async function refreshKedaActivity( + redis: Redis, + serverName: string, +): Promise { const key = `keda:${serverName}:activity`; await redis.lpush(key, Date.now().toString()); await redis.ltrim(key, 0, 0); @@ -90,7 +93,10 @@ let k8sCaCert: string | null = null; function getK8sToken(): string | null { if (k8sToken !== null) return k8sToken; try { - k8sToken = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf-8").trim(); + k8sToken = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/token", + "utf-8", + ).trim(); } catch { k8sToken = ""; } @@ -100,7 +106,10 @@ function getK8sToken(): string | null { function getK8sCaCert(): string | null { if (k8sCaCert !== null) return k8sCaCert; try { - k8sCaCert = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "utf-8"); + k8sCaCert = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + "utf-8", + ); } catch { k8sCaCert = ""; } @@ -113,7 +122,10 @@ function parseNamespaceFromUrl(serverUrl: string): string | null { return match?.[1] ?? null; } -export async function wakeServer(serverName: string, serverUrl: string): Promise { +export async function wakeServer( + serverName: string, + serverUrl: string, +): Promise { const token = getK8sToken(); if (!token) return; @@ -201,7 +213,11 @@ type TargetResult = | { ok: true; response: string } | { ok: false; error: Error; isConnectionError: boolean }; -async function tryTarget(target: string, agentId: string, body: string): Promise { +async function tryTarget( + target: string, + agentId: string, + body: string, +): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), FORWARD_TIMEOUT_MS); diff --git a/packages/services/gateway-discord/src/voice-message-handler.ts b/packages/services/gateway-discord/src/voice-message-handler.ts index 028902e41..ed8e5ce74 100644 --- a/packages/services/gateway-discord/src/voice-message-handler.ts +++ b/packages/services/gateway-discord/src/voice-message-handler.ts @@ -21,14 +21,17 @@ function parseIntEnv(name: string, defaultValue: number): number { if (value === undefined) return defaultValue; const parsed = parseInt(value, 10); if (Number.isNaN(parsed)) { - throw new Error(`Invalid ${name} environment variable: "${value}" is not a valid integer`); + throw new Error( + `Invalid ${name} environment variable: "${value}" is not a valid integer`, + ); } return parsed; } const VOICE_AUDIO_TTL_SECONDS = parseIntEnv("VOICE_AUDIO_TTL_SECONDS", 3600); -const VOICE_STORAGE_PATH_PREFIX = process.env.VOICE_STORAGE_PATH_PREFIX ?? "discord-voice"; +const VOICE_STORAGE_PATH_PREFIX = + process.env.VOICE_STORAGE_PATH_PREFIX ?? "discord-voice"; const CLEANUP_INTERVAL_MS = parseIntEnv("VOICE_CLEANUP_INTERVAL_MS", 900_000); // 15 minutes @@ -67,7 +70,10 @@ export interface VoiceAttachmentMetadata { * Checks if an attachment is a voice message. */ function isVoiceAttachment(attachment: Attachment): boolean { - return attachment.contentType?.startsWith("audio/") || attachment.name?.endsWith(".ogg"); + return ( + attachment.contentType?.startsWith("audio/") || + attachment.name?.endsWith(".ogg") + ); } /** @@ -244,12 +250,16 @@ export class VoiceMessageHandler { expires_at: result.value.expiresAt.toISOString(), size: result.value.size, content_type: result.value.contentType, - filename: voiceAttachments[index].name ?? `voice-${voiceAttachments[index].id}.ogg`, + filename: + voiceAttachments[index].name ?? + `voice-${voiceAttachments[index].id}.ogg`, }); } else { const attachmentId = voiceAttachments[index].id; const errorMessage = - result.reason instanceof Error ? result.reason.message : String(result.reason); + result.reason instanceof Error + ? result.reason.message + : String(result.reason); failed.push({ attachmentId, error: errorMessage }); logger.error("Failed to process voice attachment", { connectionId, @@ -316,7 +326,9 @@ export class VoiceMessageHandler { return 0; } - const deleteResults = await Promise.allSettled(expiredBlobs.map((blob) => del(blob.url))); + const deleteResults = await Promise.allSettled( + expiredBlobs.map((blob) => del(blob.url)), + ); let deletedCount = 0; const failed: Array<{ url: string; error: string }> = []; @@ -332,7 +344,9 @@ export class VoiceMessageHandler { } else { const blob = expiredBlobs[index]; const errorMessage = - result.reason instanceof Error ? result.reason.message : String(result.reason); + result.reason instanceof Error + ? result.reason.message + : String(result.reason); failed.push({ url: blob.url, error: errorMessage }); logger.error("Failed to delete expired voice audio file", { url: blob.url, diff --git a/packages/services/gateway-discord/tests/leader-election.test.ts b/packages/services/gateway-discord/tests/leader-election.test.ts index 75ed57f1e..c785432cd 100644 --- a/packages/services/gateway-discord/tests/leader-election.test.ts +++ b/packages/services/gateway-discord/tests/leader-election.test.ts @@ -48,7 +48,9 @@ describe("Eliza App Bot Leader Election Constants", () => { test("check interval allows multiple renewals before TTL expires", () => { // Should be able to renew at least 2-3 times before TTL expires const ttlMs = ELIZA_APP_LEADER_TTL_SECONDS * 1000; - const renewalsBeforeExpiry = Math.floor(ttlMs / ELIZA_APP_LEADER_CHECK_INTERVAL_MS); + const renewalsBeforeExpiry = Math.floor( + ttlMs / ELIZA_APP_LEADER_CHECK_INTERVAL_MS, + ); expect(renewalsBeforeExpiry).toBeGreaterThanOrEqual(3); }); }); @@ -60,7 +62,11 @@ describe("Leader Election Logic", () => { interface MockRedis { data: Map; - set(key: string, value: string, options: { ex: number; nx: boolean }): RedisSetResult; + set( + key: string, + value: string, + options: { ex: number; nx: boolean }, + ): RedisSetResult; get(key: string): string | null; expire(key: string, seconds: number): boolean; del(key: string): boolean; @@ -71,7 +77,11 @@ describe("Leader Election Logic", () => { return { data, - set(key: string, value: string, options: { ex: number; nx: boolean }): RedisSetResult { + set( + key: string, + value: string, + options: { ex: number; nx: boolean }, + ): RedisSetResult { const now = Date.now(); const existing = data.get(key); @@ -155,7 +165,10 @@ describe("Leader Election Logic", () => { }); // Renew - const renewed = redis.expire(ELIZA_APP_LEADER_KEY, ELIZA_APP_LEADER_TTL_SECONDS); + const renewed = redis.expire( + ELIZA_APP_LEADER_KEY, + ELIZA_APP_LEADER_TTL_SECONDS, + ); expect(renewed).toBe(true); }); @@ -241,7 +254,8 @@ describe("Leader Election Logic", () => { // - Lock just renewed (TTL full) // - Next check by other pod at max interval const maxFailoverMs = - ELIZA_APP_LEADER_TTL_SECONDS * 1000 + ELIZA_APP_LEADER_CHECK_INTERVAL_MS; + ELIZA_APP_LEADER_TTL_SECONDS * 1000 + + ELIZA_APP_LEADER_CHECK_INTERVAL_MS; expect(maxFailoverMs).toBe(13000); // 10s TTL + 3s check = 13s max }); @@ -254,7 +268,8 @@ describe("Leader Election Logic", () => { test("average failover time estimation", () => { // On average: half of TTL remaining + half of check interval const avgFailoverMs = - (ELIZA_APP_LEADER_TTL_SECONDS * 1000) / 2 + ELIZA_APP_LEADER_CHECK_INTERVAL_MS / 2; + (ELIZA_APP_LEADER_TTL_SECONDS * 1000) / 2 + + ELIZA_APP_LEADER_CHECK_INTERVAL_MS / 2; expect(avgFailoverMs).toBe(6500); // 5s + 1.5s = 6.5s average }); }); @@ -276,7 +291,8 @@ describe("Leader Election with Discord Intents", () => { }); test("combined intents for Eliza App bot", () => { - const combinedIntents = REQUIRED_INTENTS.DirectMessages | REQUIRED_INTENTS.MessageContent; + const combinedIntents = + REQUIRED_INTENTS.DirectMessages | REQUIRED_INTENTS.MessageContent; expect(combinedIntents).toBe(36864); // 4096 + 32768 }); }); @@ -301,7 +317,10 @@ describe("Discord Partials for DM Support", () => { }); test("both Channel and Message partials should be configured", () => { - const requiredPartials = [REQUIRED_PARTIALS.Channel, REQUIRED_PARTIALS.Message]; + const requiredPartials = [ + REQUIRED_PARTIALS.Channel, + REQUIRED_PARTIALS.Message, + ]; expect(requiredPartials).toContain(0); // Channel expect(requiredPartials).toContain(2); // Message expect(requiredPartials.length).toBe(2); @@ -309,7 +328,9 @@ describe("Discord Partials for DM Support", () => { }); describe("Environment Variable Configuration", () => { - const hasElizaAppBotConfig = (env: Record): boolean => { + const hasElizaAppBotConfig = ( + env: Record, + ): boolean => { return !!env.ELIZA_APP_DISCORD_BOT_TOKEN; }; @@ -317,14 +338,20 @@ describe("Environment Variable Configuration", () => { return !!(env.KV_REST_API_URL && env.KV_REST_API_TOKEN); }; - const canEnableLeaderElection = (env: Record): boolean => { + const canEnableLeaderElection = ( + env: Record, + ): boolean => { return hasElizaAppBotConfig(env) && hasRedisConfig(env); }; test("detects Eliza App bot configuration", () => { - expect(hasElizaAppBotConfig({ ELIZA_APP_DISCORD_BOT_TOKEN: "token" })).toBe(true); + expect(hasElizaAppBotConfig({ ELIZA_APP_DISCORD_BOT_TOKEN: "token" })).toBe( + true, + ); expect(hasElizaAppBotConfig({})).toBe(false); - expect(hasElizaAppBotConfig({ ELIZA_APP_DISCORD_BOT_TOKEN: "" })).toBe(false); + expect(hasElizaAppBotConfig({ ELIZA_APP_DISCORD_BOT_TOKEN: "" })).toBe( + false, + ); }); test("detects Redis configuration", () => { @@ -497,14 +524,23 @@ describe("Webhook Forwarding", () => { bot: boolean; }; content: string; - attachments: Array<{ url: string; content_type?: string; filename?: string }>; + attachments: Array<{ + url: string; + content_type?: string; + filename?: string; + }>; }; } const createForwardPayload = ( messageId: string, channelId: string, - author: { id: string; username: string; globalName?: string; avatar?: string }, + author: { + id: string; + username: string; + globalName?: string; + avatar?: string; + }, content: string, ): ForwardPayload => { return { @@ -544,12 +580,22 @@ describe("Webhook Forwarding", () => { }); test("guild_id is always null for DM forwarding", () => { - const payload = createForwardPayload("123", "456", { id: "789", username: "user" }, "test"); + const payload = createForwardPayload( + "123", + "456", + { id: "789", username: "user" }, + "test", + ); expect(payload.data.guild_id).toBeNull(); }); test("bot flag is always false for forwarded messages", () => { - const payload = createForwardPayload("123", "456", { id: "789", username: "user" }, "test"); + const payload = createForwardPayload( + "123", + "456", + { id: "789", username: "user" }, + "test", + ); expect(payload.data.author.bot).toBe(false); }); }); diff --git a/packages/services/gateway-discord/tests/voice-message-handler.test.ts b/packages/services/gateway-discord/tests/voice-message-handler.test.ts index b7ffaf0cb..833df7596 100644 --- a/packages/services/gateway-discord/tests/voice-message-handler.test.ts +++ b/packages/services/gateway-discord/tests/voice-message-handler.test.ts @@ -19,15 +19,22 @@ describe("Voice attachment detection logic", () => { test("audio content types are detected as voice", () => { // isVoiceAttachment checks contentType?.startsWith("audio/") || name?.endsWith(".ogg") - const isVoiceAttachment = (att: { contentType?: string | null; name?: string | null }) => { - return att.contentType?.startsWith("audio/") || att.name?.endsWith(".ogg"); + const isVoiceAttachment = (att: { + contentType?: string | null; + name?: string | null; + }) => { + return ( + att.contentType?.startsWith("audio/") || att.name?.endsWith(".ogg") + ); }; expect(isVoiceAttachment({ contentType: "audio/ogg" })).toBe(true); expect(isVoiceAttachment({ contentType: "audio/wav" })).toBe(true); expect(isVoiceAttachment({ contentType: "audio/mpeg" })).toBe(true); expect(isVoiceAttachment({ name: "voice.ogg" })).toBe(true); - expect(isVoiceAttachment({ name: "recording.ogg", contentType: null })).toBe(true); + expect( + isVoiceAttachment({ name: "recording.ogg", contentType: null }), + ).toBe(true); }); test("non-audio content types are not detected as voice", () => { @@ -35,7 +42,9 @@ describe("Voice attachment detection logic", () => { contentType?: string | null; name?: string | null; }): boolean => { - return !!(att.contentType?.startsWith("audio/") || att.name?.endsWith(".ogg")); + return !!( + att.contentType?.startsWith("audio/") || att.name?.endsWith(".ogg") + ); }; expect(isVoiceAttachment({ contentType: "image/png" })).toBe(false); @@ -48,7 +57,8 @@ describe("Voice attachment detection logic", () => { test("IsVoiceMessage flag (8192) can be detected", () => { const IS_VOICE_MESSAGE = 8192; - const hasVoiceFlag = (bitfield: number) => (bitfield & IS_VOICE_MESSAGE) !== 0; + const hasVoiceFlag = (bitfield: number) => + (bitfield & IS_VOICE_MESSAGE) !== 0; expect(hasVoiceFlag(8192)).toBe(true); expect(hasVoiceFlag(8192 | 1 | 2)).toBe(true); // Combined with other flags @@ -62,26 +72,33 @@ describe("Filename sanitization", () => { test("removes path traversal characters", () => { // The actual sanitization regex from voice-message-handler.ts: /[^a-zA-Z0-9._-]/g replaced with "_" // Note: dots are preserved in the regex, so "../" becomes ".._" not "___" - const sanitizeFilename = (name: string) => name.replace(/[^a-zA-Z0-9._-]/g, "_"); + const sanitizeFilename = (name: string) => + name.replace(/[^a-zA-Z0-9._-]/g, "_"); // Path traversal - the dots remain but slashes become underscores expect(sanitizeFilename("../../../etc/passwd")).toBe(".._.._.._etc_passwd"); expect(sanitizeFilename("..\\..\\file.txt")).toBe(".._.._file.txt"); expect(sanitizeFilename("normal-file.ogg")).toBe("normal-file.ogg"); - expect(sanitizeFilename("file with spaces.ogg")).toBe("file_with_spaces.ogg"); + expect(sanitizeFilename("file with spaces.ogg")).toBe( + "file_with_spaces.ogg", + ); expect(sanitizeFilename('file<>:"|?*.ogg')).toBe("file_______.ogg"); }); test("preserves safe characters", () => { - const sanitizeFilename = (name: string) => name.replace(/[^a-zA-Z0-9._-]/g, "_"); + const sanitizeFilename = (name: string) => + name.replace(/[^a-zA-Z0-9._-]/g, "_"); - expect(sanitizeFilename("my-voice_2024.01.15.ogg")).toBe("my-voice_2024.01.15.ogg"); + expect(sanitizeFilename("my-voice_2024.01.15.ogg")).toBe( + "my-voice_2024.01.15.ogg", + ); expect(sanitizeFilename("Voice123.ogg")).toBe("Voice123.ogg"); expect(sanitizeFilename("a.b.c.d.ogg")).toBe("a.b.c.d.ogg"); }); test("path traversal prevention works by sanitizing parent directory references", () => { - const sanitizeFilename = (name: string) => name.replace(/[^a-zA-Z0-9._-]/g, "_"); + const sanitizeFilename = (name: string) => + name.replace(/[^a-zA-Z0-9._-]/g, "_"); // Even though dots remain, the path separator is removed // This means "../" becomes ".._" which won't traverse directories @@ -126,7 +143,10 @@ describe("parseIntEnv behavior", () => { test("parseIntEnv logic validates integers correctly", () => { // Replicate parseIntEnv logic - const parseIntEnv = (value: string | undefined, defaultValue: number): number => { + const parseIntEnv = ( + value: string | undefined, + defaultValue: number, + ): number => { if (value === undefined) return defaultValue; const parsed = parseInt(value, 10); if (Number.isNaN(parsed)) { @@ -139,7 +159,9 @@ describe("parseIntEnv behavior", () => { expect(parseIntEnv("7200", 3600)).toBe(7200); expect(parseIntEnv("0", 3600)).toBe(0); expect(parseIntEnv("-100", 3600)).toBe(-100); - expect(() => parseIntEnv("not-a-number", 3600)).toThrow("not a valid integer"); + expect(() => parseIntEnv("not-a-number", 3600)).toThrow( + "not a valid integer", + ); // parseInt("12.5", 10) returns 12 (truncates at decimal) expect(parseIntEnv("12.5", 3600)).toBe(12); }); diff --git a/packages/services/gateway-webhook/__tests__/build-forward-body.test.ts b/packages/services/gateway-webhook/__tests__/build-forward-body.test.ts index b9a4d535a..1079734d1 100644 --- a/packages/services/gateway-webhook/__tests__/build-forward-body.test.ts +++ b/packages/services/gateway-webhook/__tests__/build-forward-body.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { buildForwardBody, type ForwardMessageOptions } from "../src/server-router"; +import { + buildForwardBody, + type ForwardMessageOptions, +} from "../src/server-router"; describe("buildForwardBody", () => { test("returns only userId and text when options are omitted", () => { diff --git a/packages/services/gateway-webhook/__tests__/internal-auth.test.ts b/packages/services/gateway-webhook/__tests__/internal-auth.test.ts index 8aa0e63f4..3ca3d81fa 100644 --- a/packages/services/gateway-webhook/__tests__/internal-auth.test.ts +++ b/packages/services/gateway-webhook/__tests__/internal-auth.test.ts @@ -17,7 +17,9 @@ describe("validateInternalSecret", () => { }); function makeRequest(secret?: string): Request { - const headers: Record = { "Content-Type": "application/json" }; + const headers: Record = { + "Content-Type": "application/json", + }; if (secret !== undefined) { headers["X-Internal-Secret"] = secret; } @@ -48,7 +50,9 @@ describe("validateInternalSecret", () => { test("returns false when header does not match secret (length and value differ)", () => { process.env.GATEWAY_INTERNAL_SECRET = "short"; - expect(validateInternalSecret(makeRequest("this-is-a-much-longer-secret-value"))).toBe(false); + expect( + validateInternalSecret(makeRequest("this-is-a-much-longer-secret-value")), + ).toBe(false); }); test("returns false for multi-byte UTF-8 secret with different encoding", () => { diff --git a/packages/services/gateway-webhook/src/adapters/blooio.ts b/packages/services/gateway-webhook/src/adapters/blooio.ts index b69fe8d63..df4d64d5e 100644 --- a/packages/services/gateway-webhook/src/adapters/blooio.ts +++ b/packages/services/gateway-webhook/src/adapters/blooio.ts @@ -13,7 +13,12 @@ const BlooioWebhookEventSchema = z.object({ sender: z.string().nullish(), text: z.string().nullish(), attachments: z - .array(z.union([z.string(), z.object({ url: z.string().url(), name: z.string().nullish() })])) + .array( + z.union([ + z.string(), + z.object({ url: z.string().url(), name: z.string().nullish() }), + ]), + ) .nullish(), protocol: z.string().nullish(), is_group: z.boolean().nullish(), @@ -77,7 +82,11 @@ async function verifySignature( false, ["sign"], ); - const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(signedPayload)); + const signature = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(signedPayload), + ); const computedSignature = Array.from(new Uint8Array(signature)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); @@ -103,9 +112,15 @@ async function verifySignature( export const blooioAdapter: PlatformAdapter = { platform: "blooio", - async verifyWebhook(request: Request, rawBody: string, config: WebhookConfig): Promise { + async verifyWebhook( + request: Request, + rawBody: string, + config: WebhookConfig, + ): Promise { if (!config.blooioWebhookSecret) { - logger.warn("Blooio webhook secret not configured — signature verification skipped"); + logger.warn( + "Blooio webhook secret not configured — signature verification skipped", + ); return false; } const sig = request.headers.get("x-blooio-signature") ?? ""; @@ -144,13 +159,20 @@ export const blooioAdapter: PlatformAdapter = { messageId: event.message_id ?? event.internal_id ?? `${Date.now()}`, chatId: event.sender ?? "", senderId: event.sender ?? "", - text: mediaUrls.length > 0 && !text ? `[media: ${mediaUrls.join(", ")}]` : text, + text: + mediaUrls.length > 0 && !text + ? `[media: ${mediaUrls.join(", ")}]` + : text, mediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined, rawPayload: data, }; }, - async sendReply(config: WebhookConfig, event: ChatEvent, text: string): Promise { + async sendReply( + config: WebhookConfig, + event: ChatEvent, + text: string, + ): Promise { if (!config.apiKey) throw new Error("Missing apiKey for Blooio reply"); const url = `${BLOOIO_API_BASE}/chats/${encodeURIComponent(event.senderId)}/messages`; @@ -172,7 +194,10 @@ export const blooioAdapter: PlatformAdapter = { } }, - async sendTypingIndicator(config: WebhookConfig, event: ChatEvent): Promise { + async sendTypingIndicator( + config: WebhookConfig, + event: ChatEvent, + ): Promise { if (!config.apiKey) return; try { const url = `${BLOOIO_API_BASE}/chats/${encodeURIComponent(event.senderId)}/read`; diff --git a/packages/services/gateway-webhook/src/adapters/telegram.ts b/packages/services/gateway-webhook/src/adapters/telegram.ts index c6b7f4939..32ec2a90c 100644 --- a/packages/services/gateway-webhook/src/adapters/telegram.ts +++ b/packages/services/gateway-webhook/src/adapters/telegram.ts @@ -19,7 +19,8 @@ async function telegramApi( const data = await response.json(); if (!data.ok) { throw new Error( - data.description ?? `Telegram API error: ${data.error_code ?? response.status}`, + data.description ?? + `Telegram API error: ${data.error_code ?? response.status}`, ); } return data.result as T; @@ -75,7 +76,11 @@ interface TelegramUpdate { export const telegramAdapter: PlatformAdapter = { platform: "telegram", - async verifyWebhook(request: Request, _rawBody: string, config: WebhookConfig): Promise { + async verifyWebhook( + request: Request, + _rawBody: string, + config: WebhookConfig, + ): Promise { if (!config.webhookSecret) { logger.warn("Telegram webhook secret not configured — rejecting request"); return false; @@ -122,8 +127,13 @@ export const telegramAdapter: PlatformAdapter = { }; }, - async sendReply(config: WebhookConfig, event: ChatEvent, text: string): Promise { - if (!config.botToken) throw new Error("Missing botToken for Telegram reply"); + async sendReply( + config: WebhookConfig, + event: ChatEvent, + text: string, + ): Promise { + if (!config.botToken) + throw new Error("Missing botToken for Telegram reply"); const chunks = splitMessage(text); for (const chunk of chunks) { @@ -145,7 +155,10 @@ export const telegramAdapter: PlatformAdapter = { } }, - async sendTypingIndicator(config: WebhookConfig, event: ChatEvent): Promise { + async sendTypingIndicator( + config: WebhookConfig, + event: ChatEvent, + ): Promise { if (!config.botToken) return; try { await telegramApi(config.botToken, "sendChatAction", { diff --git a/packages/services/gateway-webhook/src/adapters/twilio.ts b/packages/services/gateway-webhook/src/adapters/twilio.ts index b85bac8a0..37ee4b2a2 100644 --- a/packages/services/gateway-webhook/src/adapters/twilio.ts +++ b/packages/services/gateway-webhook/src/adapters/twilio.ts @@ -21,9 +21,12 @@ function resolveSmsCostPerSegment(): number { if (!raw) return DEFAULT_SMS_COST_PER_SEGMENT_USD; const parsed = Number.parseFloat(raw); if (!Number.isFinite(parsed) || parsed < 0) { - logger.warn("Invalid TWILIO_SMS_COST_PER_SEGMENT_USD; falling back to default", { - raw, - }); + logger.warn( + "Invalid TWILIO_SMS_COST_PER_SEGMENT_USD; falling back to default", + { + raw, + }, + ); return DEFAULT_SMS_COST_PER_SEGMENT_USD; } return parsed; @@ -100,8 +103,14 @@ async function verifySignature( false, ["sign"], ); - const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); - const computedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer))); + const signatureBuffer = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(data), + ); + const computedSignature = btoa( + String.fromCharCode(...new Uint8Array(signatureBuffer)), + ); const maxLen = Math.max(computedSignature.length, signature.length); const computedBuf = Buffer.alloc(maxLen); @@ -124,9 +133,15 @@ async function verifySignature( export const twilioAdapter: PlatformAdapter = { platform: "twilio", - async verifyWebhook(request: Request, rawBody: string, config: WebhookConfig): Promise { + async verifyWebhook( + request: Request, + rawBody: string, + config: WebhookConfig, + ): Promise { if (!config.authToken) { - logger.warn("Twilio auth token not configured — signature verification skipped"); + logger.warn( + "Twilio auth token not configured — signature verification skipped", + ); return false; } @@ -183,19 +198,28 @@ export const twilioAdapter: PlatformAdapter = { messageId: event.MessageSid, chatId: event.From, senderId: event.From, - text: mediaUrls.length > 0 && !text ? `[media: ${mediaUrls.join(", ")}]` : text, + text: + mediaUrls.length > 0 && !text + ? `[media: ${mediaUrls.join(", ")}]` + : text, mediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined, rawPayload: params, }; }, - async sendReply(config: WebhookConfig, event: ChatEvent, text: string): Promise { + async sendReply( + config: WebhookConfig, + event: ChatEvent, + text: string, + ): Promise { if (!config.accountSid || !config.authToken || !config.phoneNumber) { throw new Error("Missing Twilio credentials for reply"); } const url = `${TWILIO_API_BASE}/Accounts/${config.accountSid}/Messages.json`; - const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64"); + const auth = Buffer.from( + `${config.accountSid}:${config.authToken}`, + ).toString("base64"); const body = new URLSearchParams({ To: event.senderId, diff --git a/packages/services/gateway-webhook/src/adapters/types.ts b/packages/services/gateway-webhook/src/adapters/types.ts index 41b0a1eda..635f94fe6 100644 --- a/packages/services/gateway-webhook/src/adapters/types.ts +++ b/packages/services/gateway-webhook/src/adapters/types.ts @@ -14,9 +14,17 @@ export interface ChatEvent { export interface PlatformAdapter { platform: Platform; - verifyWebhook(request: Request, rawBody: string, config: WebhookConfig): Promise; + verifyWebhook( + request: Request, + rawBody: string, + config: WebhookConfig, + ): Promise; extractEvent(rawBody: string): Promise; - sendReply(config: WebhookConfig, event: ChatEvent, text: string): Promise; + sendReply( + config: WebhookConfig, + event: ChatEvent, + text: string, + ): Promise; sendTypingIndicator(config: WebhookConfig, event: ChatEvent): Promise; } diff --git a/packages/services/gateway-webhook/src/adapters/whatsapp.ts b/packages/services/gateway-webhook/src/adapters/whatsapp.ts index 7858e02e4..34da40775 100644 --- a/packages/services/gateway-webhook/src/adapters/whatsapp.ts +++ b/packages/services/gateway-webhook/src/adapters/whatsapp.ts @@ -56,9 +56,15 @@ const WhatsAppWebhookPayloadSchema = z.object({ export const whatsappAdapter: PlatformAdapter = { platform: "whatsapp", - async verifyWebhook(request: Request, rawBody: string, config: WebhookConfig): Promise { + async verifyWebhook( + request: Request, + rawBody: string, + config: WebhookConfig, + ): Promise { if (!config.appSecret) { - logger.warn("WhatsApp app secret not configured — signature verification skipped"); + logger.warn( + "WhatsApp app secret not configured — signature verification skipped", + ); return false; } @@ -137,7 +143,11 @@ export const whatsappAdapter: PlatformAdapter = { return null; }, - async sendReply(config: WebhookConfig, event: ChatEvent, text: string): Promise { + async sendReply( + config: WebhookConfig, + event: ChatEvent, + text: string, + ): Promise { if (!config.accessToken || !config.phoneNumberId) { throw new Error("Missing WhatsApp credentials for reply"); } @@ -164,7 +174,10 @@ export const whatsappAdapter: PlatformAdapter = { } }, - async sendTypingIndicator(config: WebhookConfig, event: ChatEvent): Promise { + async sendTypingIndicator( + config: WebhookConfig, + event: ChatEvent, + ): Promise { if (!config.accessToken || !config.phoneNumberId) return; try { const url = `${WHATSAPP_API_BASE}/${config.phoneNumberId}/messages`; diff --git a/packages/services/gateway-webhook/src/auth.ts b/packages/services/gateway-webhook/src/auth.ts index 534ebd5ba..516affcfd 100644 --- a/packages/services/gateway-webhook/src/auth.ts +++ b/packages/services/gateway-webhook/src/auth.ts @@ -42,17 +42,20 @@ async function acquireToken(): Promise { logger.info("Acquiring JWT token", { podName: config.podName }); - const response = await fetchWithTimeout(`${config.cloudUrl}/api/internal/auth/token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Gateway-Secret": config.bootstrapSecret, + const response = await fetchWithTimeout( + `${config.cloudUrl}/api/internal/auth/token`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Gateway-Secret": config.bootstrapSecret, + }, + body: JSON.stringify({ + pod_name: config.podName, + service: "webhook-gateway", + }), }, - body: JSON.stringify({ - pod_name: config.podName, - service: "webhook-gateway", - }), - }); + ); if (!response.ok) { const error = await response.text(); @@ -81,13 +84,16 @@ async function refreshToken(): Promise { logger.info("Refreshing JWT token", { podName: config.podName }); try { - const response = await fetchWithTimeout(`${config.cloudUrl}/api/internal/auth/refresh`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, + const response = await fetchWithTimeout( + `${config.cloudUrl}/api/internal/auth/refresh`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, }, - }); + ); if (!response.ok) { logger.warn("Token refresh failed, acquiring new token", { diff --git a/packages/services/gateway-webhook/src/hash-router.ts b/packages/services/gateway-webhook/src/hash-router.ts index 505a86c62..80915a648 100644 --- a/packages/services/gateway-webhook/src/hash-router.ts +++ b/packages/services/gateway-webhook/src/hash-router.ts @@ -17,13 +17,19 @@ let k8sCaCert: string | null = null; function getK8sToken(): string { if (k8sToken !== null) return k8sToken; - k8sToken = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf-8").trim(); + k8sToken = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/token", + "utf-8", + ).trim(); return k8sToken; } function getK8sCaCert(): string { if (k8sCaCert !== null) return k8sCaCert; - k8sCaCert = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "utf-8"); + k8sCaCert = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + "utf-8", + ); return k8sCaCert; } @@ -53,7 +59,10 @@ interface EndpointSliceList { }>; } -async function resolvePodIPs(serviceName: string, namespace: string): Promise { +async function resolvePodIPs( + serviceName: string, + namespace: string, +): Promise { const apiUrl = `https://kubernetes.default.svc/apis/discovery.k8s.io/v1/namespaces/${namespace}/endpointslices?labelSelector=kubernetes.io/service-name=${serviceName}`; try { diff --git a/packages/services/gateway-webhook/src/index.ts b/packages/services/gateway-webhook/src/index.ts index 521c6cf96..35b4159c9 100644 --- a/packages/services/gateway-webhook/src/index.ts +++ b/packages/services/gateway-webhook/src/index.ts @@ -9,11 +9,15 @@ import { getAuthHeader, initAuth, shutdownAuth } from "./auth"; import { handleInternalEvent } from "./internal-event-handler"; import { logger } from "./logger"; import { initProjectConfig, shutdownProjectConfig } from "./project-config"; -import { getSharedWhatsAppVerifyToken, resolveWebhookConfig } from "./webhook-config"; +import { + getSharedWhatsAppVerifyToken, + resolveWebhookConfig, +} from "./webhook-config"; import { handleWebhook } from "./webhook-handler"; const PORT = Number(process.env.PORT ?? 3000); -const POD_NAME = process.env.POD_NAME ?? process.env.HOSTNAME ?? "webhook-local"; +const POD_NAME = + process.env.POD_NAME ?? process.env.HOSTNAME ?? "webhook-local"; const adapters: Record = { telegram: telegramAdapter, @@ -33,7 +37,9 @@ const redis = new Redis({ const app = new Hono(); -app.get("/health", (c) => c.json({ status: draining ? "draining" : "healthy", pod: POD_NAME })); +app.get("/health", (c) => + c.json({ status: draining ? "draining" : "healthy", pod: POD_NAME }), +); app.get("/ready", (c) => { if (draining) return c.json({ status: "draining" }, 503); return c.json({ status: "ready" }); @@ -80,7 +86,12 @@ app.get("/webhook/:project/whatsapp/:agentId", async (c) => { agentId, ); - if (mode === "subscribe" && config?.verifyToken && token === config.verifyToken && challenge) { + if ( + mode === "subscribe" && + config?.verifyToken && + token === config.verifyToken && + challenge + ) { logger.info("WhatsApp webhook verified", { agentId }); return c.text(challenge, 200); } diff --git a/packages/services/gateway-webhook/src/internal-auth.ts b/packages/services/gateway-webhook/src/internal-auth.ts index 9f2898c2b..356eb0e36 100644 --- a/packages/services/gateway-webhook/src/internal-auth.ts +++ b/packages/services/gateway-webhook/src/internal-auth.ts @@ -33,7 +33,9 @@ export function validateInternalSecret(request: Request): boolean { // to know why requests are being rejected. The timing oracle concern is // mitigated because timingSafeEqual always runs below (no early return). if (secretMissing) { - logger.warn("Internal auth rejected: GATEWAY_INTERNAL_SECRET not configured"); + logger.warn( + "Internal auth rejected: GATEWAY_INTERNAL_SECRET not configured", + ); } else if (headerMissing) { logger.warn("Internal auth rejected: missing X-Internal-Secret header"); } diff --git a/packages/services/gateway-webhook/src/internal-event-handler.ts b/packages/services/gateway-webhook/src/internal-event-handler.ts index 64ac757bb..3c8029d3a 100644 --- a/packages/services/gateway-webhook/src/internal-event-handler.ts +++ b/packages/services/gateway-webhook/src/internal-event-handler.ts @@ -56,7 +56,11 @@ export async function handleInternalEvent( const clHeader = request.headers.get("content-length"); const contentLength = clHeader !== null ? Number(clHeader) : null; - if (contentLength !== null && Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) { + if ( + contentLength !== null && + Number.isFinite(contentLength) && + contentLength > MAX_BODY_BYTES + ) { logger.warn("Internal event rejected: payload too large (content-length)", { contentLength, }); @@ -93,11 +97,17 @@ export async function handleInternalEvent( logger.warn("Internal event rejected: schema validation failed", { issues: parsed.error.issues, }); - return jsonResponse({ error: "invalid request body", details: parsed.error.issues }, 400); + return jsonResponse( + { error: "invalid request body", details: parsed.error.issues }, + 400, + ); } const event = parsed.data; - logger.info("Internal event queued", { agentId: event.agentId, type: event.type }); + logger.info("Internal event queued", { + agentId: event.agentId, + type: event.type, + }); processInternalEvent(event, deps).catch((err) => { logger.error("Background internal event processing failed", { @@ -120,7 +130,10 @@ export async function handleInternalEvent( * to server failed" (error) log lines as the primary signal for * missed deliveries. */ -async function processInternalEvent(event: InternalEvent, deps: InternalEventDeps): Promise { +async function processInternalEvent( + event: InternalEvent, + deps: InternalEventDeps, +): Promise { const { redis } = deps; const server = await resolveAgentServer(redis, event.agentId); diff --git a/packages/services/gateway-webhook/src/logger.ts b/packages/services/gateway-webhook/src/logger.ts index c607a5621..0f00da7aa 100644 --- a/packages/services/gateway-webhook/src/logger.ts +++ b/packages/services/gateway-webhook/src/logger.ts @@ -16,7 +16,11 @@ function shouldLog(level: LogLevel): boolean { return LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL]; } -function formatMessage(level: LogLevel, message: string, meta?: Record): string { +function formatMessage( + level: LogLevel, + message: string, + meta?: Record, +): string { const timestamp = new Date().toISOString(); const base = { timestamp, level, message, ...meta }; return JSON.stringify(base); @@ -33,6 +37,7 @@ export const logger = { if (shouldLog("warn")) console.warn(formatMessage("warn", message, meta)); }, error(message: string, meta?: Record) { - if (shouldLog("error")) console.error(formatMessage("error", message, meta)); + if (shouldLog("error")) + console.error(formatMessage("error", message, meta)); }, }; diff --git a/packages/services/gateway-webhook/src/project-config.ts b/packages/services/gateway-webhook/src/project-config.ts index 7c1379366..78114c186 100644 --- a/packages/services/gateway-webhook/src/project-config.ts +++ b/packages/services/gateway-webhook/src/project-config.ts @@ -14,7 +14,10 @@ let k8sNamespace: string | null = null; function getK8sToken(): string | null { if (k8sToken !== null) return k8sToken; try { - k8sToken = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf-8").trim(); + k8sToken = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/token", + "utf-8", + ).trim(); } catch { k8sToken = ""; } @@ -24,7 +27,10 @@ function getK8sToken(): string | null { function getK8sCaCert(): string | null { if (k8sCaCert !== null) return k8sCaCert; try { - k8sCaCert = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "utf-8"); + k8sCaCert = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + "utf-8", + ); } catch { k8sCaCert = ""; } diff --git a/packages/services/gateway-webhook/src/server-router.ts b/packages/services/gateway-webhook/src/server-router.ts index e06fd73ca..4806d42a1 100644 --- a/packages/services/gateway-webhook/src/server-router.ts +++ b/packages/services/gateway-webhook/src/server-router.ts @@ -16,7 +16,10 @@ interface ServerRoute { serverUrl: string; } -export type RoutingRedis = Pick; +export type RoutingRedis = Pick< + Redis, + "get" | "set" | "lpush" | "ltrim" | "expire" +>; export interface ResolvedIdentity { userId: string; @@ -33,7 +36,9 @@ export async function resolveIdentity( platformName?: string, ): Promise { const cacheKey = `identity:${platform}:${platformId}`; - const cached = await redis.get(cacheKey); + const cached = await redis.get( + cacheKey, + ); if (cached) { if ("notFound" in cached) return null; return cached; @@ -89,7 +94,10 @@ export async function resolveAgentServer( return { serverName, serverUrl }; } -export async function refreshKedaActivity(redis: RoutingRedis, serverName: string): Promise { +export async function refreshKedaActivity( + redis: RoutingRedis, + serverName: string, +): Promise { const key = `keda:${serverName}:activity`; await redis.lpush(key, Date.now().toString()); await redis.ltrim(key, 0, 0); @@ -102,7 +110,10 @@ let k8sCaCert: string | null = null; function getK8sToken(): string | null { if (k8sToken !== null) return k8sToken; try { - k8sToken = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf-8").trim(); + k8sToken = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/token", + "utf-8", + ).trim(); } catch (err) { logger.debug("K8s service account token not available", { error: err instanceof Error ? err.message : String(err), @@ -115,7 +126,10 @@ function getK8sToken(): string | null { function getK8sCaCert(): string | null { if (k8sCaCert !== null) return k8sCaCert; try { - k8sCaCert = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "utf-8"); + k8sCaCert = readFileSync( + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + "utf-8", + ); } catch (err) { logger.debug("K8s CA cert not available", { error: err instanceof Error ? err.message : String(err), @@ -130,7 +144,10 @@ function parseNamespaceFromUrl(serverUrl: string): string | null { return match?.[1] ?? null; } -export async function wakeServer(serverName: string, serverUrl: string): Promise { +export async function wakeServer( + serverName: string, + serverUrl: string, +): Promise { const token = getK8sToken(); if (!token) return; @@ -189,7 +206,10 @@ export function buildForwardBody( text: string, options?: ForwardMessageOptions, ): { userId: string; text: string } & Partial { - const body: { userId: string; text: string } & Partial = { + const body: { + userId: string; + text: string; + } & Partial = { userId, text, }; diff --git a/packages/services/gateway-webhook/src/webhook-config.ts b/packages/services/gateway-webhook/src/webhook-config.ts index f22f9da42..4f3a96a44 100644 --- a/packages/services/gateway-webhook/src/webhook-config.ts +++ b/packages/services/gateway-webhook/src/webhook-config.ts @@ -5,7 +5,10 @@ import { getProjectEnv } from "./project-config"; const CONFIG_CACHE_TTL_SECONDS = 300; -function buildSharedWebhookConfig(platform: Platform, project: string): WebhookConfig { +function buildSharedWebhookConfig( + platform: Platform, + project: string, +): WebhookConfig { const base: WebhookConfig = { agentId: getProjectEnv(project, "DEFAULT_AGENT_ID"), }; @@ -17,7 +20,10 @@ function buildSharedWebhookConfig(platform: Platform, project: string): WebhookC break; case "blooio": base.apiKey = getProjectEnv(project, "BLOOIO_API_KEY"); - base.blooioWebhookSecret = getProjectEnv(project, "BLOOIO_WEBHOOK_SECRET"); + base.blooioWebhookSecret = getProjectEnv( + project, + "BLOOIO_WEBHOOK_SECRET", + ); base.fromNumber = getProjectEnv(project, "BLOOIO_PHONE_NUMBER"); break; case "twilio": diff --git a/packages/services/gateway-webhook/src/webhook-handler.ts b/packages/services/gateway-webhook/src/webhook-handler.ts index 1a2a2ce97..2915389d6 100644 --- a/packages/services/gateway-webhook/src/webhook-handler.ts +++ b/packages/services/gateway-webhook/src/webhook-handler.ts @@ -1,5 +1,10 @@ import type { Redis } from "@upstash/redis"; -import type { ChatEvent, Platform, PlatformAdapter, WebhookConfig } from "./adapters/types"; +import type { + ChatEvent, + Platform, + PlatformAdapter, + WebhookConfig, +} from "./adapters/types"; import { logger } from "./logger"; import { forwardToServer, @@ -180,10 +185,13 @@ async function processMessage( function ackResponse(platform: Platform): Response { // Twilio expects empty TwiML if (platform === "twilio") { - return new Response('', { - status: 200, - headers: { "Content-Type": "text/xml" }, - }); + return new Response( + '', + { + status: 200, + headers: { "Content-Type": "text/xml" }, + }, + ); } return new Response(JSON.stringify({ ok: true }), { status: 200, diff --git a/packages/tests/e2e/auth/anonymous-session.test.ts b/packages/tests/e2e/auth/anonymous-session.test.ts index 23f68e006..94c7ea7a2 100644 --- a/packages/tests/e2e/auth/anonymous-session.test.ts +++ b/packages/tests/e2e/auth/anonymous-session.test.ts @@ -16,7 +16,9 @@ describe("Anonymous Session API", () => { const body = (await response.json()) as any; expect(body).toBeDefined(); // Should return some form of session identifier - expect(body.sessionToken || body.token || body.session || body.userId).toBeTruthy(); + expect( + body.sessionToken || body.token || body.session || body.userId, + ).toBeTruthy(); }); test("GET /api/anonymous-session returns session data when cookie exists", async () => { diff --git a/packages/tests/e2e/auth/cli-auth.test.ts b/packages/tests/e2e/auth/cli-auth.test.ts index 974de0b10..901247419 100644 --- a/packages/tests/e2e/auth/cli-auth.test.ts +++ b/packages/tests/e2e/auth/cli-auth.test.ts @@ -24,12 +24,16 @@ describe("CLI Auth API", () => { }); test("GET /api/auth/cli-session/[id] returns 404 for nonexistent session", async () => { - const response = await api.get("/api/auth/cli-session/nonexistent-session-id"); + const response = await api.get( + "/api/auth/cli-session/nonexistent-session-id", + ); expect([404, 400]).toContain(response.status); }); test("POST /api/auth/cli-session/[id]/complete requires auth", async () => { - const response = await api.post("/api/auth/cli-session/test-session/complete"); + const response = await api.post( + "/api/auth/cli-session/test-session/complete", + ); // Should require authenticated user to complete expect([401, 403, 404]).toContain(response.status); }); diff --git a/packages/tests/e2e/cron/cron-routes.test.ts b/packages/tests/e2e/cron/cron-routes.test.ts index 8e37f0f43..6faeff40a 100644 --- a/packages/tests/e2e/cron/cron-routes.test.ts +++ b/packages/tests/e2e/cron/cron-routes.test.ts @@ -78,20 +78,26 @@ describe("Cron Routes", () => { }); describe("With CRON_SECRET", () => { - test.skipIf(!api.hasCronSecret())("cron routes accept valid CRON_SECRET", async () => { - // Test just one route with CRON_SECRET to verify auth works - const response = await api.get(ALL_CRON_ROUTES[0], { - headers: api.cronHeaders(), - }); - // Should accept the request (200) or fail gracefully (500 from missing deps) - expect([200, 500]).toContain(response.status); - }); + test.skipIf(!api.hasCronSecret())( + "cron routes accept valid CRON_SECRET", + async () => { + // Test just one route with CRON_SECRET to verify auth works + const response = await api.get(ALL_CRON_ROUTES[0], { + headers: api.cronHeaders(), + }); + // Should accept the request (200) or fail gracefully (500 from missing deps) + expect([200, 500]).toContain(response.status); + }, + ); - test.skipIf(!api.hasCronSecret())("v1 cron routes accept valid CRON_SECRET", async () => { - const response = await api.get(V1_CRON_ROUTES[0], { - headers: api.cronHeaders(), - }); - expect([200, 500]).toContain(response.status); - }); + test.skipIf(!api.hasCronSecret())( + "v1 cron routes accept valid CRON_SECRET", + async () => { + const response = await api.get(V1_CRON_ROUTES[0], { + headers: api.cronHeaders(), + }); + expect([200, 500]).toContain(response.status); + }, + ); }); }); diff --git a/packages/tests/e2e/helpers/api-client.ts b/packages/tests/e2e/helpers/api-client.ts index 7cfcfa9fd..2039657cf 100644 --- a/packages/tests/e2e/helpers/api-client.ts +++ b/packages/tests/e2e/helpers/api-client.ts @@ -25,7 +25,8 @@ function getSessionCookie(): string | null { return null; } - const cookieName = process.env.TEST_SESSION_COOKIE_NAME?.trim() || "eliza-test-session"; + const cookieName = + process.env.TEST_SESSION_COOKIE_NAME?.trim() || "eliza-test-session"; return `${cookieName}=${token}`; } @@ -74,7 +75,9 @@ async function getSessionCookieFromServer(): Promise { if (!response.ok) { const body = await response.text(); - throw new Error(`Failed to create live test session: ${response.status} ${body.slice(0, 200)}`); + throw new Error( + `Failed to create live test session: ${response.status} ${body.slice(0, 200)}`, + ); } const body = (await response.json()) as { @@ -93,11 +96,14 @@ async function getSessionCookieFromServer(): Promise { return cookie; } -async function authenticatedRequestHeaders(path: string): Promise> { +async function authenticatedRequestHeaders( + path: string, +): Promise> { const headers = isSessionOnlyPath(path) ? { "Content-Type": "application/json" } : authenticatedHeaders(); - const cookie = await (cachedSessionCookiePromise ??= getSessionCookieFromServer()); + const cookie = await (cachedSessionCookiePromise ??= + getSessionCookieFromServer()); if (cookie) { headers.Cookie = cookie; diff --git a/packages/tests/e2e/helpers/app-lifecycle.ts b/packages/tests/e2e/helpers/app-lifecycle.ts index 417857fab..7b752e10b 100644 --- a/packages/tests/e2e/helpers/app-lifecycle.ts +++ b/packages/tests/e2e/helpers/app-lifecycle.ts @@ -38,9 +38,12 @@ export async function createTestApp( /** Delete an app, swallowing 404s (already deleted) */ export async function deleteTestApp(appId: string): Promise { - await api.del(`/api/v1/apps/${appId}?deleteGitHubRepo=false&deleteVercelProject=false`, { - authenticated: true, - }); + await api.del( + `/api/v1/apps/${appId}?deleteGitHubRepo=false&deleteVercelProject=false`, + { + authenticated: true, + }, + ); } /** Enable monetization on an app */ @@ -65,7 +68,9 @@ export async function enableMonetization( } /** Get monetization settings for an app */ -export async function getMonetization(appId: string): Promise<{ response: Response; body: any }> { +export async function getMonetization( + appId: string, +): Promise<{ response: Response; body: any }> { const response = await api.get(`/api/v1/apps/${appId}/monetization`, { authenticated: true, }); @@ -74,7 +79,9 @@ export async function getMonetization(appId: string): Promise<{ response: Respon } /** Get earnings for an app */ -export async function getEarnings(appId: string): Promise<{ response: Response; body: any }> { +export async function getEarnings( + appId: string, +): Promise<{ response: Response; body: any }> { const response = await api.get(`/api/v1/apps/${appId}/earnings`, { authenticated: true, }); @@ -83,7 +90,9 @@ export async function getEarnings(appId: string): Promise<{ response: Response; } /** Get public info for an app (no auth required) */ -export async function getPublicAppInfo(appId: string): Promise<{ response: Response; body: any }> { +export async function getPublicAppInfo( + appId: string, +): Promise<{ response: Response; body: any }> { const response = await api.get(`/api/v1/apps/${appId}/public`); const body = await response.json(); return { response, body }; @@ -123,7 +132,11 @@ export async function createTestAgent( */ export async function deleteTestAgent(agentId: string): Promise { // Unpublish first (ignore errors) - await api.del(`/api/v1/agents/${agentId}/publish`, { authenticated: true }).catch(() => {}); + await api + .del(`/api/v1/agents/${agentId}/publish`, { authenticated: true }) + .catch(() => {}); // Then delete the character - await api.del(`/api/my-agents/characters/${agentId}`, { authenticated: true }).catch(() => {}); + await api + .del(`/api/my-agents/characters/${agentId}`, { authenticated: true }) + .catch(() => {}); } diff --git a/packages/tests/e2e/setup-server.ts b/packages/tests/e2e/setup-server.ts index 4086e8494..24c972512 100644 --- a/packages/tests/e2e/setup-server.ts +++ b/packages/tests/e2e/setup-server.ts @@ -3,8 +3,10 @@ import { delimiter } from "node:path"; import type { Subprocess } from "bun"; const TEST_SERVER_PORT = process.env.TEST_SERVER_PORT || "3000"; -const SERVER_URL = process.env.TEST_BASE_URL || `http://localhost:${TEST_SERVER_PORT}`; -const TEST_SERVER_DIST_DIR = process.env.TEST_SERVER_DIST_DIR || `.next-test-${TEST_SERVER_PORT}`; +const SERVER_URL = + process.env.TEST_BASE_URL || `http://localhost:${TEST_SERVER_PORT}`; +const TEST_SERVER_DIST_DIR = + process.env.TEST_SERVER_DIST_DIR || `.next-test-${TEST_SERVER_PORT}`; const HEALTH_ENDPOINT = `${SERVER_URL}/api/health`; // Cold Next.js webpack boots can take noticeably longer after large test suites // or when the first request has to compile the health route. @@ -15,7 +17,9 @@ const POLL_INTERVAL_MS = 500; const MANAGED_FETCH_RETRIES = 4; const TEST_SERVER_SCRIPT = process.env.TEST_SERVER_SCRIPT || "dev"; const baseFetch: typeof fetch = globalThis.fetch; -const forwardBasePreconnect: NonNullable = (...args) => { +const forwardBasePreconnect: NonNullable = ( + ...args +) => { if (typeof baseFetch.preconnect === "function") { baseFetch.preconnect(...args); } @@ -32,7 +36,9 @@ function cleanupTestServerDistDir(): void { rmSync(TEST_SERVER_DIST_DIR, { force: true, recursive: true }); } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn(`[E2E Server] Failed to clean ${TEST_SERVER_DIST_DIR}: ${message}`); + console.warn( + `[E2E Server] Failed to clean ${TEST_SERVER_DIST_DIR}: ${message}`, + ); } } @@ -42,14 +48,19 @@ function getBunExecutable(): string { return execPath; } - throw new Error("Bun executable path is unavailable in the current test runtime"); + throw new Error( + "Bun executable path is unavailable in the current test runtime", + ); } function extendPathWithExecutableDirectory( envPath: string | undefined, executablePath: string, ): string { - const executableDir = executablePath.slice(0, Math.max(executablePath.lastIndexOf("/"), 0)); + const executableDir = executablePath.slice( + 0, + Math.max(executablePath.lastIndexOf("/"), 0), + ); if (executableDir.length === 0) { return envPath ?? ""; } @@ -68,7 +79,10 @@ function extendPathWithExecutableDirectory( async function isServerRunning(): Promise { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), HEALTHCHECK_TIMEOUT_MS); + const timeoutId = setTimeout( + () => controller.abort(), + HEALTHCHECK_TIMEOUT_MS, + ); try { const response = await baseFetch(HEALTH_ENDPOINT, { @@ -99,7 +113,9 @@ async function waitForServer(timeoutMs: number): Promise { } async function warmServerRoutes(): Promise { - const warmups: Array<{ path: string; init?: RequestInit }> = [{ path: "/api/health" }]; + const warmups: Array<{ path: string; init?: RequestInit }> = [ + { path: "/api/health" }, + ]; for (const warmup of warmups) { const controller = new AbortController(); @@ -141,7 +157,8 @@ function pipeServerLogs( ) { if ( label === "stderr" && - (text.includes("Unable to acquire lock") || text.includes("EADDRINUSE")) + (text.includes("Unable to acquire lock") || + text.includes("EADDRINUSE")) ) { detectedPeerServerStartup = true; } @@ -151,7 +168,9 @@ function pipeServerLogs( } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!message.toLowerCase().includes("closed")) { - console.warn(`[E2E Server:${label}] log stream ended unexpectedly: ${message}`); + console.warn( + `[E2E Server:${label}] log stream ended unexpectedly: ${message}`, + ); } } })(); @@ -171,7 +190,9 @@ function watchServerExit(process: Subprocess): void { if (code !== 0 && code !== 15) { await Bun.sleep(250); if (detectedPeerServerStartup || (await isServerRunning())) { - console.warn("[E2E Server] Detected another worker-owned dev server; waiting for health"); + console.warn( + "[E2E Server] Detected another worker-owned dev server; waiting for health", + ); return; } serverExitError = new Error(`E2E server exited with code ${code}`); @@ -180,7 +201,10 @@ function watchServerExit(process: Subprocess): void { }); } -async function waitForPortRelease(port: number, timeoutMs = 10_000): Promise { +async function waitForPortRelease( + port: number, + timeoutMs = 10_000, +): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { @@ -268,16 +292,23 @@ export async function ensureServer(): Promise { NEXT_DIST_DIR: TEST_SERVER_DIST_DIR, PORT: TEST_SERVER_PORT, RATE_LIMIT_MULTIPLIER: process.env.RATE_LIMIT_MULTIPLIER || "100", - PATH: extendPathWithExecutableDirectory(process.env.PATH, bunExecutable), + PATH: extendPathWithExecutableDirectory( + process.env.PATH, + bunExecutable, + ), }, }); pipeServerLogs( - serverProcess.stdout instanceof ReadableStream ? serverProcess.stdout : null, + serverProcess.stdout instanceof ReadableStream + ? serverProcess.stdout + : null, "stdout", ); pipeServerLogs( - serverProcess.stderr instanceof ReadableStream ? serverProcess.stderr : null, + serverProcess.stderr instanceof ReadableStream + ? serverProcess.stderr + : null, "stderr", ); watchServerExit(serverProcess); diff --git a/packages/tests/e2e/v1/admin-service-pricing.test.ts b/packages/tests/e2e/v1/admin-service-pricing.test.ts index c52c10971..07afd3dba 100644 --- a/packages/tests/e2e/v1/admin-service-pricing.test.ts +++ b/packages/tests/e2e/v1/admin-service-pricing.test.ts @@ -12,16 +12,21 @@ import * as api from "../helpers/api-client"; describe("Admin Service Pricing API", () => { test("GET /api/v1/admin/service-pricing requires admin auth", async () => { - const response = await api.get("/api/v1/admin/service-pricing?service_id=solana-rpc"); + const response = await api.get( + "/api/v1/admin/service-pricing?service_id=solana-rpc", + ); expect([401, 403]).toContain(response.status); }); test.skipIf(!api.hasApiKey())( "GET /api/v1/admin/service-pricing returns data for an admin key or 403 for a non-admin key", async () => { - const response = await api.get("/api/v1/admin/service-pricing?service_id=solana-rpc", { - authenticated: true, - }); + const response = await api.get( + "/api/v1/admin/service-pricing?service_id=solana-rpc", + { + authenticated: true, + }, + ); expect([200, 403]).toContain(response.status); if (response.status === 200) { diff --git a/packages/tests/e2e/v1/affiliate-miniapp.test.ts b/packages/tests/e2e/v1/affiliate-miniapp.test.ts index 5c95a4423..0e958a96a 100644 --- a/packages/tests/e2e/v1/affiliate-miniapp.test.ts +++ b/packages/tests/e2e/v1/affiliate-miniapp.test.ts @@ -35,7 +35,9 @@ describe.skipIf(!api.hasApiKey())("Affiliate Mini App Flow", () => { afterAll(async () => { if (characterId) { await api - .del(`/api/my-agents/characters/${characterId}`, { authenticated: true }) + .del(`/api/my-agents/characters/${characterId}`, { + authenticated: true, + }) .catch(() => {}); } }); @@ -207,9 +209,12 @@ describe.skipIf(!api.hasApiKey())("Programmatic Mini App Setup", () => { expect(earningsResp.status).toBe(200); // App-level credits endpoint works - const creditsResp = await api.get(`/api/v1/app-credits/balance?app_id=${appId}`, { - authenticated: true, - }); + const creditsResp = await api.get( + `/api/v1/app-credits/balance?app_id=${appId}`, + { + authenticated: true, + }, + ); expect(creditsResp.status).toBe(200); // App users endpoint works (should be empty) diff --git a/packages/tests/e2e/v1/affiliates.test.ts b/packages/tests/e2e/v1/affiliates.test.ts index f08040f88..a676cfa9a 100644 --- a/packages/tests/e2e/v1/affiliates.test.ts +++ b/packages/tests/e2e/v1/affiliates.test.ts @@ -21,10 +21,15 @@ describe("Affiliates API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/affiliates returns data with auth", async () => { - const response = await api.get("/api/v1/affiliates", { authenticated: true }); - expect(response.status).toBe(200); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/affiliates returns data with auth", + async () => { + const response = await api.get("/api/v1/affiliates", { + authenticated: true, + }); + expect(response.status).toBe(200); + }, + ); test.skipIf(!api.hasApiKey())( "Affiliate SKU end-to-end: AI inference with X-Affiliate-Code credits owner", @@ -37,16 +42,22 @@ describe("Affiliates API", () => { ); expect([200, 400]).toContain(createRes.status); - const getRes = await api.get("/api/v1/affiliates", { authenticated: true }); + const getRes = await api.get("/api/v1/affiliates", { + authenticated: true, + }); expect(getRes.status).toBe(200); const getBody = (await getRes.json()) as any; const affiliateCode = getBody.code?.code; expect(affiliateCode).toBeTruthy(); // 2. Initial earnings check - const initialUserRes = await api.get("/api/v1/user", { authenticated: true }); + const initialUserRes = await api.get("/api/v1/user", { + authenticated: true, + }); const initialUserBody = (await initialUserRes.json()) as any; - const initialEarnings = Number(initialUserBody.user?.redeemable_earnings || 0); + const initialEarnings = Number( + initialUserBody.user?.redeemable_earnings || 0, + ); // 3. Perform AI inference with X-Affiliate-Code const chatRes = await api.post( @@ -69,9 +80,13 @@ describe("Affiliates API", () => { expect(chatRes.status).toBe(200); // 4. Verify earnings increased - const finalUserRes = await api.get("/api/v1/user", { authenticated: true }); + const finalUserRes = await api.get("/api/v1/user", { + authenticated: true, + }); const finalUserBody = (await finalUserRes.json()) as any; - const finalEarnings = Number(finalUserBody.user?.redeemable_earnings || 0); + const finalEarnings = Number( + finalUserBody.user?.redeemable_earnings || 0, + ); expect(finalEarnings).toBeGreaterThan(initialEarnings); }, @@ -91,14 +106,17 @@ describe("Referrals API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("POST /api/v1/referrals/apply with invalid code", async () => { - const response = await api.post( - "/api/v1/referrals/apply", - { code: "NONEXISTENT" }, - { authenticated: true }, - ); - expect([200, 400, 404]).toContain(response.status); - }); + test.skipIf(!api.hasApiKey())( + "POST /api/v1/referrals/apply with invalid code", + async () => { + const response = await api.post( + "/api/v1/referrals/apply", + { code: "NONEXISTENT" }, + { authenticated: true }, + ); + expect([200, 400, 404]).toContain(response.status); + }, + ); test.skipIf(!api.hasApiKey())( "GET /api/v1/referrals returns flat code payload with auth", @@ -115,7 +133,9 @@ describe("Referrals API", () => { expect(typeof body.total_referrals).toBe("number"); expect(typeof body.is_active).toBe("boolean"); - const second = await api.get("/api/v1/referrals", { authenticated: true }); + const second = await api.get("/api/v1/referrals", { + authenticated: true, + }); expect(second.status).toBe(200); const body2 = (await second.json()) as { code: string }; expect(body2.code).toBe(body.code); diff --git a/packages/tests/e2e/v1/agent-publishing.test.ts b/packages/tests/e2e/v1/agent-publishing.test.ts index 50192edfa..49be51845 100644 --- a/packages/tests/e2e/v1/agent-publishing.test.ts +++ b/packages/tests/e2e/v1/agent-publishing.test.ts @@ -10,7 +10,14 @@ * Requires: TEST_API_KEY env var pointing at a live Cloud account. */ -import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; +import { + afterAll, + beforeAll, + describe, + expect, + setDefaultTimeout, + test, +} from "bun:test"; import * as api from "../helpers/api-client"; import { createTestAgent, deleteTestAgent } from "../helpers/app-lifecycle"; import { NONEXISTENT_UUID } from "../helpers/test-data"; @@ -177,19 +184,29 @@ describe.skipIf(!api.hasApiKey())("Agent Publishing — Error Cases", () => { }); test("publish requires auth", async () => { - const response = await api.post(`/api/v1/agents/${NONEXISTENT_UUID}/publish`, {}); + const response = await api.post( + `/api/v1/agents/${NONEXISTENT_UUID}/publish`, + {}, + ); expect([401, 403]).toContain(response.status); }); test("monetization on nonexistent agent returns error", async () => { - const response = await api.get(`/api/v1/agents/${NONEXISTENT_UUID}/monetization`, { - authenticated: true, - }); + const response = await api.get( + `/api/v1/agents/${NONEXISTENT_UUID}/monetization`, + { + authenticated: true, + }, + ); expect([403, 404]).toContain(response.status); }); test("POST /api/v1/app/agents validates name", async () => { - const response = await api.post("/api/v1/app/agents", { name: "" }, { authenticated: true }); + const response = await api.post( + "/api/v1/app/agents", + { name: "" }, + { authenticated: true }, + ); expect(response.status).toBe(400); }); diff --git a/packages/tests/e2e/v1/agents.test.ts b/packages/tests/e2e/v1/agents.test.ts index d4d558b83..18a73e2d6 100644 --- a/packages/tests/e2e/v1/agents.test.ts +++ b/packages/tests/e2e/v1/agents.test.ts @@ -33,12 +33,16 @@ describe("Agents API", () => { describe("Agent Actions", () => { test("POST /api/v1/agents/[id]/restart requires auth", async () => { - const response = await api.post(`/api/v1/agents/${NONEXISTENT_UUID}/restart`); + const response = await api.post( + `/api/v1/agents/${NONEXISTENT_UUID}/restart`, + ); expect([401, 403]).toContain(response.status); }); test("GET /api/v1/agents/[id]/status requires auth", async () => { - const response = await api.get(`/api/v1/agents/${NONEXISTENT_UUID}/status`); + const response = await api.get( + `/api/v1/agents/${NONEXISTENT_UUID}/status`, + ); expect([401, 403]).toContain(response.status); }); @@ -50,18 +54,24 @@ describe("Agents API", () => { describe("Agent A2A", () => { test("POST /api/v1/agents/[id]/a2a returns valid response", async () => { - const response = await api.post(`/api/v1/agents/${NONEXISTENT_UUID}/a2a`, { - message: { text: "test" }, - }); + const response = await api.post( + `/api/v1/agents/${NONEXISTENT_UUID}/a2a`, + { + message: { text: "test" }, + }, + ); expect([200, 401, 404]).toContain(response.status); }); }); describe("Agent MCP", () => { test("POST /api/v1/agents/[id]/mcp returns valid response", async () => { - const response = await api.post(`/api/v1/agents/${NONEXISTENT_UUID}/mcp`, { - method: "tools/list", - }); + const response = await api.post( + `/api/v1/agents/${NONEXISTENT_UUID}/mcp`, + { + method: "tools/list", + }, + ); expect([200, 401, 404]).toContain(response.status); }); }); @@ -73,13 +83,18 @@ describe("Containers API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/containers returns container list", async () => { - const response = await api.get("/api/v1/containers", { authenticated: true }); - expect(response.status).toBe(200); - const body = (await response.json()) as any; - const containers = body.data || body.containers || body; - expect(Array.isArray(containers)).toBe(true); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/containers returns container list", + async () => { + const response = await api.get("/api/v1/containers", { + authenticated: true, + }); + expect(response.status).toBe(200); + const body = (await response.json()) as any; + const containers = body.data || body.containers || body; + expect(Array.isArray(containers)).toBe(true); + }, + ); test.skipIf(!api.hasApiKey())( "GET /api/v1/containers/[id] returns 404 for nonexistent", diff --git a/packages/tests/e2e/v1/api-keys.test.ts b/packages/tests/e2e/v1/api-keys.test.ts index 059e4e637..8872c0a0b 100644 --- a/packages/tests/e2e/v1/api-keys.test.ts +++ b/packages/tests/e2e/v1/api-keys.test.ts @@ -14,15 +14,18 @@ describe("API Keys API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/api-keys returns key list with auth", async () => { - const response = await api.get("/api/v1/api-keys", { - authenticated: true, - }); - expect(response.status).toBe(200); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/api-keys returns key list with auth", + async () => { + const response = await api.get("/api/v1/api-keys", { + authenticated: true, + }); + expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(Array.isArray(body.apiKeys || body.keys || body)).toBe(true); - }); + const body = (await response.json()) as any; + expect(Array.isArray(body.apiKeys || body.keys || body)).toBe(true); + }, + ); test("POST /api/v1/api-keys requires authentication", async () => { const response = await api.post("/api/v1/api-keys", { @@ -50,7 +53,9 @@ describe("API Keys API", () => { ); test("DELETE /api/v1/api-keys/[id] requires authentication", async () => { - const response = await api.del("/api/v1/api-keys/00000000-0000-4000-8000-000000000000"); + const response = await api.del( + "/api/v1/api-keys/00000000-0000-4000-8000-000000000000", + ); expect([401, 403]).toContain(response.status); }); }); diff --git a/packages/tests/e2e/v1/app-monetization.test.ts b/packages/tests/e2e/v1/app-monetization.test.ts index 9081c2f2c..37e6058ee 100644 --- a/packages/tests/e2e/v1/app-monetization.test.ts +++ b/packages/tests/e2e/v1/app-monetization.test.ts @@ -7,7 +7,14 @@ * Requires: TEST_API_KEY env var pointing at a live Cloud account. */ -import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; +import { + afterAll, + beforeAll, + describe, + expect, + setDefaultTimeout, + test, +} from "bun:test"; import * as api from "../helpers/api-client"; import { createTestApp, diff --git a/packages/tests/e2e/v1/billing.test.ts b/packages/tests/e2e/v1/billing.test.ts index 3e9eb0ee9..5aaf05d69 100644 --- a/packages/tests/e2e/v1/billing.test.ts +++ b/packages/tests/e2e/v1/billing.test.ts @@ -19,12 +19,17 @@ describe("Billing API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/credits/summary returns billing info", async () => { - const response = await api.get("/api/v1/credits/summary", { authenticated: true }); - expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(body).toBeTruthy(); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/credits/summary returns billing info", + async () => { + const response = await api.get("/api/v1/credits/summary", { + authenticated: true, + }); + expect(response.status).toBe(200); + const body = (await response.json()) as any; + expect(body).toBeTruthy(); + }, + ); }); describe("Topup Routes", () => { @@ -74,10 +79,15 @@ describe("Redemptions API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/redemptions returns list", async () => { - const response = await api.get("/api/v1/redemptions", { authenticated: true }); - expect(response.status).toBe(200); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/redemptions returns list", + async () => { + const response = await api.get("/api/v1/redemptions", { + authenticated: true, + }); + expect(response.status).toBe(200); + }, + ); }); describe("x402 API", () => { diff --git a/packages/tests/e2e/v1/characters.test.ts b/packages/tests/e2e/v1/characters.test.ts index c297332a7..fa1470e15 100644 --- a/packages/tests/e2e/v1/characters.test.ts +++ b/packages/tests/e2e/v1/characters.test.ts @@ -11,14 +11,19 @@ setDefaultTimeout(30_000); describe("Characters API", () => { test("PUT /api/v1/characters/[id]/public requires auth", async () => { - const response = await api.put(`/api/v1/characters/${NONEXISTENT_UUID}/public`, { - isPublic: true, - }); + const response = await api.put( + `/api/v1/characters/${NONEXISTENT_UUID}/public`, + { + isPublic: true, + }, + ); expect([401, 403]).toContain(response.status); }); test("GET /api/v1/characters/[id]/mcps requires auth", async () => { - const response = await api.get(`/api/v1/characters/${NONEXISTENT_UUID}/mcps`); + const response = await api.get( + `/api/v1/characters/${NONEXISTENT_UUID}/mcps`, + ); expect([401, 403, 404]).toContain(response.status); }); }); @@ -29,12 +34,15 @@ describe("Apps API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/apps returns app list", async () => { - const response = await api.get("/api/v1/apps", { authenticated: true }); - expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(Array.isArray(body.apps || body)).toBe(true); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/apps returns app list", + async () => { + const response = await api.get("/api/v1/apps", { authenticated: true }); + expect(response.status).toBe(200); + const body = (await response.json()) as any; + expect(Array.isArray(body.apps || body)).toBe(true); + }, + ); test("POST /api/v1/apps requires auth", async () => { const response = await api.post("/api/v1/apps", { name: "test-app" }); diff --git a/packages/tests/e2e/v1/compat.test.ts b/packages/tests/e2e/v1/compat.test.ts index 20d1c583b..8b8f39108 100644 --- a/packages/tests/e2e/v1/compat.test.ts +++ b/packages/tests/e2e/v1/compat.test.ts @@ -14,12 +14,15 @@ describe("Compat Agents API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/compat/agents returns agents list", async () => { - const response = await api.get("/api/compat/agents", { - authenticated: true, - }); - expect(response.status).toBe(200); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/compat/agents returns agents list", + async () => { + const response = await api.get("/api/compat/agents", { + authenticated: true, + }); + expect(response.status).toBe(200); + }, + ); test("GET /api/compat/agents/[id] handles nonexistent", async () => { const response = await api.get(`/api/compat/agents/${NONEXISTENT_UUID}`); @@ -27,27 +30,37 @@ describe("Compat Agents API", () => { }); test("GET /api/compat/agents/[id]/status handles nonexistent", async () => { - const response = await api.get(`/api/compat/agents/${NONEXISTENT_UUID}/status`); + const response = await api.get( + `/api/compat/agents/${NONEXISTENT_UUID}/status`, + ); expect([404, 401, 403]).toContain(response.status); }); test("GET /api/compat/agents/[id]/logs handles nonexistent", async () => { - const response = await api.get(`/api/compat/agents/${NONEXISTENT_UUID}/logs`); + const response = await api.get( + `/api/compat/agents/${NONEXISTENT_UUID}/logs`, + ); expect([404, 401, 403]).toContain(response.status); }); test("POST /api/compat/agents/[id]/restart requires auth", async () => { - const response = await api.post(`/api/compat/agents/${NONEXISTENT_UUID}/restart`); + const response = await api.post( + `/api/compat/agents/${NONEXISTENT_UUID}/restart`, + ); expect([401, 403]).toContain(response.status); }); test("POST /api/compat/agents/[id]/resume requires auth", async () => { - const response = await api.post(`/api/compat/agents/${NONEXISTENT_UUID}/resume`); + const response = await api.post( + `/api/compat/agents/${NONEXISTENT_UUID}/resume`, + ); expect([401, 403]).toContain(response.status); }); test("POST /api/compat/agents/[id]/suspend requires auth", async () => { - const response = await api.post(`/api/compat/agents/${NONEXISTENT_UUID}/suspend`); + const response = await api.post( + `/api/compat/agents/${NONEXISTENT_UUID}/suspend`, + ); expect([401, 403]).toContain(response.status); }); }); diff --git a/packages/tests/e2e/v1/connections.test.ts b/packages/tests/e2e/v1/connections.test.ts index 9ec12423c..2815e58ea 100644 --- a/packages/tests/e2e/v1/connections.test.ts +++ b/packages/tests/e2e/v1/connections.test.ts @@ -32,7 +32,11 @@ describe("Platform Connections API", () => { test.skipIf(!api.hasApiKey())( "POST /api/v1/connections/github returns unsupported platform", async () => { - const response = await api.post("/api/v1/connections/github", {}, { authenticated: true }); + const response = await api.post( + "/api/v1/connections/github", + {}, + { authenticated: true }, + ); expect(response.status).toBe(404); }, ); diff --git a/packages/tests/e2e/v1/credits-billing.test.ts b/packages/tests/e2e/v1/credits-billing.test.ts index 8cb7ba107..7d9750531 100644 --- a/packages/tests/e2e/v1/credits-billing.test.ts +++ b/packages/tests/e2e/v1/credits-billing.test.ts @@ -10,9 +10,20 @@ * Requires: TEST_API_KEY env var pointing at a live Cloud account. */ -import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; +import { + afterAll, + beforeAll, + describe, + expect, + setDefaultTimeout, + test, +} from "bun:test"; import * as api from "../helpers/api-client"; -import { createTestApp, deleteTestApp, enableMonetization } from "../helpers/app-lifecycle"; +import { + createTestApp, + deleteTestApp, + enableMonetization, +} from "../helpers/app-lifecycle"; setDefaultTimeout(30_000); @@ -105,9 +116,12 @@ describe.skipIf(!api.hasApiKey())("App Credits", () => { }); test("GET /api/v1/app-credits/balance returns app credit balance", async () => { - const response = await api.get(`/api/v1/app-credits/balance?app_id=${appId}`, { - authenticated: true, - }); + const response = await api.get( + `/api/v1/app-credits/balance?app_id=${appId}`, + { + authenticated: true, + }, + ); expect(response.status).toBe(200); const body = (await response.json()) as any; diff --git a/packages/tests/e2e/v1/credits.test.ts b/packages/tests/e2e/v1/credits.test.ts index 9fbe5bb53..a8563d660 100644 --- a/packages/tests/e2e/v1/credits.test.ts +++ b/packages/tests/e2e/v1/credits.test.ts @@ -20,7 +20,9 @@ describe("Credits API", () => { expect(response.status).toBe(200); const body = (await response.json()) as any; - expect(body.balance !== undefined || body.creditBalance !== undefined).toBe(true); + expect( + body.balance !== undefined || body.creditBalance !== undefined, + ).toBe(true); }, ); diff --git a/packages/tests/e2e/v1/eliza-rooms.test.ts b/packages/tests/e2e/v1/eliza-rooms.test.ts index f99eaef3c..4fe673520 100644 --- a/packages/tests/e2e/v1/eliza-rooms.test.ts +++ b/packages/tests/e2e/v1/eliza-rooms.test.ts @@ -25,16 +25,22 @@ describe("Eliza Rooms API", () => { }); test("POST /api/eliza/rooms/[id]/messages requires valid room", async () => { - const response = await api.post(`/api/eliza/rooms/${NONEXISTENT_UUID}/messages`, { - text: "Hello", - }); + const response = await api.post( + `/api/eliza/rooms/${NONEXISTENT_UUID}/messages`, + { + text: "Hello", + }, + ); expect([400, 401, 403, 404]).toContain(response.status); }); test("POST /api/eliza/rooms/[id]/welcome handles nonexistent room", async () => { - const response = await api.post(`/api/eliza/rooms/${NONEXISTENT_UUID}/welcome`, { - text: "Welcome", - }); + const response = await api.post( + `/api/eliza/rooms/${NONEXISTENT_UUID}/welcome`, + { + text: "Welcome", + }, + ); expect([401, 403, 404]).toContain(response.status); }); }); diff --git a/packages/tests/e2e/v1/media.test.ts b/packages/tests/e2e/v1/media.test.ts index c77e1a034..6b3f2b338 100644 --- a/packages/tests/e2e/v1/media.test.ts +++ b/packages/tests/e2e/v1/media.test.ts @@ -13,10 +13,17 @@ describe("Video Generation API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("POST /api/v1/generate-video validates input", async () => { - const response = await api.post("/api/v1/generate-video", {}, { authenticated: true }); - expect([400, 402, 503]).toContain(response.status); - }); + test.skipIf(!api.hasApiKey())( + "POST /api/v1/generate-video validates input", + async () => { + const response = await api.post( + "/api/v1/generate-video", + {}, + { authenticated: true }, + ); + expect([400, 402, 503]).toContain(response.status); + }, + ); }); describe("Voice API", () => { @@ -64,20 +71,23 @@ describe("Embeddings API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("POST /api/v1/embeddings returns embedding vector", async () => { - const response = await api.post( - "/api/v1/embeddings", - { - input: "Test embedding text", - model: "text-embedding-3-small", - }, - { authenticated: true }, - ); - expect([200, 402, 503]).toContain(response.status); + test.skipIf(!api.hasApiKey())( + "POST /api/v1/embeddings returns embedding vector", + async () => { + const response = await api.post( + "/api/v1/embeddings", + { + input: "Test embedding text", + model: "text-embedding-3-small", + }, + { authenticated: true }, + ); + expect([200, 402, 503]).toContain(response.status); - if (response.status === 200) { - const body = (await response.json()) as any; - expect(body.data || body.embeddings).toBeTruthy(); - } - }); + if (response.status === 200) { + const body = (await response.json()) as any; + expect(body.data || body.embeddings).toBeTruthy(); + } + }, + ); }); diff --git a/packages/tests/e2e/v1/miniapp-lifecycle.test.ts b/packages/tests/e2e/v1/miniapp-lifecycle.test.ts index a46fbfb2b..50069ca8f 100644 --- a/packages/tests/e2e/v1/miniapp-lifecycle.test.ts +++ b/packages/tests/e2e/v1/miniapp-lifecycle.test.ts @@ -259,7 +259,11 @@ describe.skipIf(!api.hasApiKey())("Mini App Lifecycle", () => { // ── Error cases ───────────────────────────────────────────────────── describe.skipIf(!api.hasApiKey())("Mini App Lifecycle — Error Cases", () => { test("POST /api/v1/apps rejects invalid payload", async () => { - const response = await api.post("/api/v1/apps", { name: "" }, { authenticated: true }); + const response = await api.post( + "/api/v1/apps", + { name: "" }, + { authenticated: true }, + ); expect(response.status).toBe(400); }); diff --git a/packages/tests/e2e/v1/misc.test.ts b/packages/tests/e2e/v1/misc.test.ts index 310878546..9051b2f74 100644 --- a/packages/tests/e2e/v1/misc.test.ts +++ b/packages/tests/e2e/v1/misc.test.ts @@ -26,10 +26,15 @@ describe("Dashboard API", () => { expect([401, 403, 404]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/dashboard returns dashboard data", async () => { - const response = await api.get("/api/v1/dashboard", { authenticated: true }); - expect([200, 404]).toContain(response.status); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/dashboard returns dashboard data", + async () => { + const response = await api.get("/api/v1/dashboard", { + authenticated: true, + }); + expect([200, 404]).toContain(response.status); + }, + ); }); describe("Admin API", () => { @@ -137,14 +142,17 @@ describe("Credits Summary API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/credits/summary returns credit info", async () => { - const response = await api.get("/api/v1/credits/summary", { - authenticated: true, - }); - expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(body).toBeTruthy(); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/credits/summary returns credit info", + async () => { + const response = await api.get("/api/v1/credits/summary", { + authenticated: true, + }); + expect(response.status).toBe(200); + const body = (await response.json()) as any; + expect(body).toBeTruthy(); + }, + ); }); describe("App Auth API", () => { diff --git a/packages/tests/e2e/v1/models.test.ts b/packages/tests/e2e/v1/models.test.ts index 88a2031c2..8da74412f 100644 --- a/packages/tests/e2e/v1/models.test.ts +++ b/packages/tests/e2e/v1/models.test.ts @@ -17,21 +17,24 @@ describe("Models API", () => { } }); - test.skipIf(!api.hasApiKey())("GET /api/v1/models returns models with auth", async () => { - const response = await api.get("/api/v1/models", { - authenticated: true, - }); - expect([200, 503]).toContain(response.status); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/models returns models with auth", + async () => { + const response = await api.get("/api/v1/models", { + authenticated: true, + }); + expect([200, 503]).toContain(response.status); - if (response.status !== 200) { - return; - } + if (response.status !== 200) { + return; + } - const body = (await response.json()) as any; - const models = body.data || body.models || body; - expect(Array.isArray(models)).toBe(true); - expect(models.length).toBeGreaterThan(0); - }); + const body = (await response.json()) as any; + const models = body.data || body.models || body; + expect(Array.isArray(models)).toBe(true); + expect(models.length).toBeGreaterThan(0); + }, + ); test("POST /api/v1/models/status returns status", async () => { const response = await api.post("/api/v1/models/status", { @@ -65,15 +68,18 @@ describe("Responses API", () => { } }); - test.skipIf(!api.hasApiKey())("POST /api/v1/responses accepts valid input", async () => { - const response = await api.post( - "/api/v1/responses", - { - model: "google/gemini-2.5-flash", - input: [{ role: "user", content: "Say hello" }], - }, - { authenticated: true }, - ); - expect([200, 401, 402, 503]).toContain(response.status); - }); + test.skipIf(!api.hasApiKey())( + "POST /api/v1/responses accepts valid input", + async () => { + const response = await api.post( + "/api/v1/responses", + { + model: "google/gemini-2.5-flash", + input: [{ role: "user", content: "Say hello" }], + }, + { authenticated: true }, + ); + expect([200, 401, 402, 503]).toContain(response.status); + }, + ); }); diff --git a/packages/tests/e2e/v1/redemptions.test.ts b/packages/tests/e2e/v1/redemptions.test.ts index 2f958d23d..d0084f7a3 100644 --- a/packages/tests/e2e/v1/redemptions.test.ts +++ b/packages/tests/e2e/v1/redemptions.test.ts @@ -8,16 +8,19 @@ setDefaultTimeout(15_000); describe("Redemptions API", () => { describe("GET /api/v1/redemptions/status", () => { - test.skipIf(!api.hasApiKey())("returns system status with auth", async () => { - const response = await api.get("/api/v1/redemptions/status", { - authenticated: true, - }); - expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(body.success).toBe(true); - expect(typeof body.canRedeem).toBe("boolean"); - expect(Array.isArray(body.availableNetworks)).toBe(true); - }); + test.skipIf(!api.hasApiKey())( + "returns system status with auth", + async () => { + const response = await api.get("/api/v1/redemptions/status", { + authenticated: true, + }); + expect(response.status).toBe(200); + const body = (await response.json()) as any; + expect(body.success).toBe(true); + expect(typeof body.canRedeem).toBe("boolean"); + expect(Array.isArray(body.availableNetworks)).toBe(true); + }, + ); }); describe("GET /api/v1/redemptions/balance", () => { @@ -26,22 +29,27 @@ describe("Redemptions API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("returns balance data when authenticated", async () => { - const response = await api.get("/api/v1/redemptions/balance", { - authenticated: true, - }); - expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(body.success).toBe(true); - expect(body.balance).toBeDefined(); - expect(body.eligibility).toBeDefined(); - expect(Array.isArray(body.recentEarnings)).toBe(true); - }); + test.skipIf(!api.hasApiKey())( + "returns balance data when authenticated", + async () => { + const response = await api.get("/api/v1/redemptions/balance", { + authenticated: true, + }); + expect(response.status).toBe(200); + const body = (await response.json()) as any; + expect(body.success).toBe(true); + expect(body.balance).toBeDefined(); + expect(body.eligibility).toBeDefined(); + expect(Array.isArray(body.recentEarnings)).toBe(true); + }, + ); }); describe("GET /api/v1/redemptions/quote", () => { test("requires auth", async () => { - const response = await api.get("/api/v1/redemptions/quote?network=base&pointsAmount=100"); + const response = await api.get( + "/api/v1/redemptions/quote?network=base&pointsAmount=100", + ); expect([401, 403]).toContain(response.status); }); @@ -56,9 +64,12 @@ describe("Redemptions API", () => { }); test.skipIf(!api.hasApiKey())("gets quote for valid network", async () => { - const response = await api.get("/api/v1/redemptions/quote?network=base&pointsAmount=100", { - authenticated: true, - }); + const response = await api.get( + "/api/v1/redemptions/quote?network=base&pointsAmount=100", + { + authenticated: true, + }, + ); // Might be 503 if system is down/no balance, which is an expected handled state expect([200, 503, 400]).toContain(response.status); }); diff --git a/packages/tests/e2e/v1/security.test.ts b/packages/tests/e2e/v1/security.test.ts index d3a71d266..cbbf2cff2 100644 --- a/packages/tests/e2e/v1/security.test.ts +++ b/packages/tests/e2e/v1/security.test.ts @@ -39,7 +39,8 @@ describe("Discovery API", () => { const response = await api.get("/api/v1/discovery"); // withRateLimit adds these headers const rateLimitHeader = - response.headers.get("X-RateLimit-Limit") || response.headers.get("x-ratelimit-limit"); + response.headers.get("X-RateLimit-Limit") || + response.headers.get("x-ratelimit-limit"); // Rate limit headers should be present (case-insensitive check) expect(rateLimitHeader || response.status).toBeTruthy(); }); @@ -65,10 +66,13 @@ describe("Apps API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/apps returns data with auth", async () => { - const response = await api.get("/api/v1/apps", { authenticated: true }); - expect([200, 404]).toContain(response.status); - }); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/apps returns data with auth", + async () => { + const response = await api.get("/api/v1/apps", { authenticated: true }); + expect([200, 404]).toContain(response.status); + }, + ); }); // ============================================================================= @@ -263,7 +267,9 @@ describe("Affiliate Create Character — Security", () => { }); test("OPTIONS /api/affiliate/create-character returns CORS headers", async () => { - const response = await fetch(api.url("/api/affiliate/create-character"), { method: "OPTIONS" }); + const response = await fetch(api.url("/api/affiliate/create-character"), { + method: "OPTIONS", + }); expect(response.status).toBe(204); // Should include at least Access-Control-Allow-Methods const methods = response.headers.get("Access-Control-Allow-Methods"); @@ -295,7 +301,9 @@ describe("Internal Routes — Security", () => { }); test("GET /api/internal/webhook/config rejects without auth", async () => { - const response = await api.get("/api/internal/webhook/config?agentId=test&platform=telegram"); + const response = await api.get( + "/api/internal/webhook/config?agentId=test&platform=telegram", + ); expect([401, 403]).toContain(response.status); }); }); diff --git a/packages/tests/e2e/v1/user.test.ts b/packages/tests/e2e/v1/user.test.ts index ccd0f0325..ebcea7ebb 100644 --- a/packages/tests/e2e/v1/user.test.ts +++ b/packages/tests/e2e/v1/user.test.ts @@ -11,14 +11,17 @@ describe("User API", () => { expect([401, 403]).toContain(response.status); }); - test.skipIf(!api.hasApiKey())("GET /api/v1/user returns user data with API key", async () => { - const response = await api.get("/api/v1/user", { authenticated: true }); - expect(response.status).toBe(200); + test.skipIf(!api.hasApiKey())( + "GET /api/v1/user returns user data with API key", + async () => { + const response = await api.get("/api/v1/user", { authenticated: true }); + expect(response.status).toBe(200); - const body = (await response.json()) as any; - expect(body.success).toBe(true); - expect(body.data?.id).toBeTruthy(); - }); + const body = (await response.json()) as any; + expect(body.success).toBe(true); + expect(body.data?.id).toBeTruthy(); + }, + ); test("PATCH /api/v1/user requires authentication", async () => { const response = await api.patch("/api/v1/user", { name: "test" }); diff --git a/packages/tests/infrastructure/http-client.ts b/packages/tests/infrastructure/http-client.ts index 0d4dc9894..9239f6682 100644 --- a/packages/tests/infrastructure/http-client.ts +++ b/packages/tests/infrastructure/http-client.ts @@ -157,7 +157,9 @@ export async function* parseSSEStream( /** * Collect all SSE events from a response into an array */ -export async function collectSSEEvents(response: Response): Promise { +export async function collectSSEEvents( + response: Response, +): Promise { const events: SSEEvent[] = []; for await (const event of parseSSEStream(response)) { events.push(event); @@ -168,7 +170,9 @@ export async function collectSSEEvents(response: Response): Promise /** * Parse SSE events into structured streaming message events */ -export async function parseStreamingResponse(response: Response): Promise { +export async function parseStreamingResponse( + response: Response, +): Promise { const result: StreamingMessageEvents = { chunks: [], reasoningChunks: [], @@ -199,7 +203,9 @@ export async function parseStreamingResponse(response: Response): Promise): Record { + private getHeaders( + customHeaders?: Record, + ): Record { const headers: Record = { "Content-Type": "application/json", ...customHeaders, @@ -273,7 +281,10 @@ export class TestApiClient { async get(path: string, options?: RequestOptions): Promise { const url = `${this.baseUrl}${path}`; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), options?.timeout || this.defaultTimeout); + const timeout = setTimeout( + () => controller.abort(), + options?.timeout || this.defaultTimeout, + ); try { const response = await fetch(url, { @@ -290,10 +301,17 @@ export class TestApiClient { /** * Make a POST request */ - async post(path: string, body: unknown, options?: RequestOptions): Promise { + async post( + path: string, + body: unknown, + options?: RequestOptions, + ): Promise { const url = `${this.baseUrl}${path}`; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), options?.timeout || this.defaultTimeout); + const timeout = setTimeout( + () => controller.abort(), + options?.timeout || this.defaultTimeout, + ); try { const response = await fetch(url, { @@ -314,7 +332,10 @@ export class TestApiClient { async delete(path: string, options?: RequestOptions): Promise { const url = `${this.baseUrl}${path}`; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), options?.timeout || this.defaultTimeout); + const timeout = setTimeout( + () => controller.abort(), + options?.timeout || this.defaultTimeout, + ); try { const response = await fetch(url, { @@ -385,7 +406,9 @@ export class TestApiClient { if (!response.ok) { const errorBody = await response.json().catch(() => ({})); - throw new Error(`Failed to create room: ${response.status} - ${JSON.stringify(errorBody)}`); + throw new Error( + `Failed to create room: ${response.status} - ${JSON.stringify(errorBody)}`, + ); } const data = await response.json(); @@ -443,7 +466,9 @@ export class StreamingError extends Error { /** * Create a test API client */ -export function createTestApiClient(options: TestApiClientOptions): TestApiClient { +export function createTestApiClient( + options: TestApiClientOptions, +): TestApiClient { return new TestApiClient(options); } diff --git a/packages/tests/infrastructure/local-database.ts b/packages/tests/infrastructure/local-database.ts index adce43a91..8c96ce0a6 100644 --- a/packages/tests/infrastructure/local-database.ts +++ b/packages/tests/infrastructure/local-database.ts @@ -9,7 +9,8 @@ import { Client } from "pg"; import { resolveDatabaseUrl } from "@/db/database-url"; /** True when DATABASE_URL is set (use with describe.skipIf(!hasDatabaseUrl) for DB-dependent suites). */ -export const hasDatabaseUrl = typeof resolveDatabaseUrl(process.env) === "string"; +export const hasDatabaseUrl = + typeof resolveDatabaseUrl(process.env) === "string"; /** * Get the connection string from environment diff --git a/packages/tests/infrastructure/local-test-auth.ts b/packages/tests/infrastructure/local-test-auth.ts index 4573cf49c..988a97a21 100644 --- a/packages/tests/infrastructure/local-test-auth.ts +++ b/packages/tests/infrastructure/local-test-auth.ts @@ -33,9 +33,12 @@ export type LocalTestAuthContext = { }; function getDatabaseUrl(): string { - const connectionString = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL; + const connectionString = + process.env.TEST_DATABASE_URL || process.env.DATABASE_URL; if (!connectionString) { - throw new Error("TEST_DATABASE_URL or DATABASE_URL is required for live auth bootstrap"); + throw new Error( + "TEST_DATABASE_URL or DATABASE_URL is required for live auth bootstrap", + ); } return connectionString; } @@ -87,7 +90,10 @@ async function upsertOrganization(client: Client): Promise { return result.rows[0]!.id; } -async function upsertUser(client: Client, organizationId: string): Promise { +async function upsertUser( + client: Client, + organizationId: string, +): Promise { const existingUsers = await client.query<{ id: string }>( `SELECT id FROM users @@ -147,7 +153,13 @@ async function upsertUser(client: Client, organizationId: string): Promise { await client.query("COMMIT"); - const sessionToken = createPlaywrightTestSessionToken(userId, organizationId); + const sessionToken = createPlaywrightTestSessionToken( + userId, + organizationId, + ); process.env.TEST_API_KEY = apiKey; process.env.TEST_USER_ID = userId; diff --git a/packages/tests/infrastructure/test-data-factory.ts b/packages/tests/infrastructure/test-data-factory.ts index f917cdfc5..f49f00b81 100644 --- a/packages/tests/infrastructure/test-data-factory.ts +++ b/packages/tests/infrastructure/test-data-factory.ts @@ -161,7 +161,9 @@ export async function createTestDataSet( userId, orgId, characterName, - JSON.stringify((characterData as { bio?: string })?.bio || "Test character bio"), + JSON.stringify( + (characterData as { bio?: string })?.bio || "Test character bio", + ), true, JSON.stringify(characterData), JSON.stringify(characterSettings), @@ -257,7 +259,9 @@ export async function createAnonymousSession( [sessionId, sessionToken, userId, messageLimit, expiresAt], ); - console.log(`[TestDataFactory] Created anonymous session: ${sessionToken.slice(0, 12)}...`); + console.log( + `[TestDataFactory] Created anonymous session: ${sessionToken.slice(0, 12)}...`, + ); return { sessionToken, userId, sessionId }; } finally { await client.end(); @@ -276,16 +280,27 @@ export async function cleanupTestData( try { // Delete in order respecting foreign keys - await client.query(`DELETE FROM api_keys WHERE organization_id = $1`, [organizationId]); - await client.query(`DELETE FROM user_characters WHERE organization_id = $1`, [organizationId]); + await client.query(`DELETE FROM api_keys WHERE organization_id = $1`, [ + organizationId, + ]); + await client.query( + `DELETE FROM user_characters WHERE organization_id = $1`, + [organizationId], + ); await client.query( `DELETE FROM anonymous_sessions WHERE user_id IN (SELECT id FROM users WHERE organization_id = $1)`, [organizationId], ); - await client.query(`DELETE FROM users WHERE organization_id = $1`, [organizationId]); - await client.query(`DELETE FROM organizations WHERE id = $1`, [organizationId]); - - console.log(`[TestDataFactory] Cleaned up test data for org: ${organizationId}`); + await client.query(`DELETE FROM users WHERE organization_id = $1`, [ + organizationId, + ]); + await client.query(`DELETE FROM organizations WHERE id = $1`, [ + organizationId, + ]); + + console.log( + `[TestDataFactory] Cleaned up test data for org: ${organizationId}`, + ); } finally { await client.end(); } diff --git a/packages/tests/infrastructure/test-runtime.ts b/packages/tests/infrastructure/test-runtime.ts index 94b610f3d..47d5cf9ca 100644 --- a/packages/tests/infrastructure/test-runtime.ts +++ b/packages/tests/infrastructure/test-runtime.ts @@ -52,7 +52,9 @@ import type { UserContext } from "../../lib/eliza/user-context"; import type { TestDataSet } from "./test-data-factory"; // Type for the runtime returned by RuntimeFactory -export type TestRuntime = Awaited>; +export type TestRuntime = Awaited< + ReturnType +>; /** * Result from createTestRuntime including cleanup function @@ -111,7 +113,9 @@ function validateTestData(testData: TestDataSet): void { } if (!testData.user) { - throw new Error("[TestRuntime] Test user is required. Test data must include a valid user."); + throw new Error( + "[TestRuntime] Test user is required. Test data must include a valid user.", + ); } if (!testData.apiKey?.key) { @@ -127,7 +131,10 @@ function validateTestData(testData: TestDataSet): void { } // Block anonymous users in tests (unless explicitly testing anonymous flow) - if (testData.user.isAnonymous && process.env.TEST_BLOCK_ANONYMOUS !== "false") { + if ( + testData.user.isAnonymous && + process.env.TEST_BLOCK_ANONYMOUS !== "false" + ) { throw new Error( "[TestRuntime] Anonymous users are BLOCKED in tests.\n" + "Tests must use properly created test users with valid API keys.\n" + @@ -188,7 +195,9 @@ export async function createTestRuntime( }, cleanup: async () => { try { - const { invalidateRuntime } = await import("../../lib/eliza/runtime-factory"); + const { invalidateRuntime } = await import( + "../../lib/eliza/runtime-factory" + ); await invalidateRuntime(runtime.agentId); } catch (e) { console.warn(`[TestRuntime] Cleanup warning: ${e}`); @@ -228,7 +237,10 @@ export function buildUserContext( } = {}, ): UserContext { // Validate we're not creating anonymous context - if (testData.user.isAnonymous && process.env.TEST_BLOCK_ANONYMOUS !== "false") { + if ( + testData.user.isAnonymous && + process.env.TEST_BLOCK_ANONYMOUS !== "false" + ) { throw new Error( "[TestRuntime] Cannot build UserContext for anonymous user.\n" + "Tests must use authenticated users. Set TEST_BLOCK_ANONYMOUS=false to override.", @@ -288,7 +300,8 @@ export async function createTestUser( }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); - if (!msg.includes("duplicate") && !msg.includes("unique constraint")) throw error; + if (!msg.includes("duplicate") && !msg.includes("unique constraint")) + throw error; } // Ensure room exists @@ -305,7 +318,8 @@ export async function createTestUser( }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); - if (!msg.includes("duplicate") && !msg.includes("unique constraint")) throw error; + if (!msg.includes("duplicate") && !msg.includes("unique constraint")) + throw error; } // Ensure agent entity exists @@ -321,7 +335,8 @@ export async function createTestUser( } } catch (error) { const msg = error instanceof Error ? error.message : String(error); - if (!msg.includes("duplicate") && !msg.includes("unique constraint")) throw error; + if (!msg.includes("duplicate") && !msg.includes("unique constraint")) + throw error; } // Ensure user entity exists @@ -334,7 +349,8 @@ export async function createTestUser( }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); - if (!msg.includes("duplicate") && !msg.includes("unique constraint")) throw error; + if (!msg.includes("duplicate") && !msg.includes("unique constraint")) + throw error; } // Ensure participants @@ -406,11 +422,18 @@ export async function sendTestMessage( options: SendTestMessageOptions = {}, ): Promise { const startTime = Date.now(); - const { timeoutMs = 120000, debug, onStreamChunk, onReasoningChunk } = options; + const { + timeoutMs = 120000, + debug, + onStreamChunk, + onReasoningChunk, + } = options; // Import debug utilities if debug is enabled let getLatestDebugTrace: (() => DebugTrace | undefined) | undefined; - let renderDebugTrace: ((trace: DebugTrace, view?: DebugRenderView) => string) | undefined; + let renderDebugTrace: + | ((trace: DebugTrace, view?: DebugRenderView) => string) + | undefined; let clearDebugTraces: (() => void) | undefined; if (debug?.enabled) { @@ -424,7 +447,9 @@ export async function sendTestMessage( } // Import the production message handler - const { createMessageHandler } = await import("../../lib/eliza/message-handler"); + const { createMessageHandler } = await import( + "../../lib/eliza/message-handler" + ); const { AgentMode } = await import("../../lib/eliza/agent-mode-types"); // Build proper UserContext like production does @@ -446,7 +471,9 @@ export async function sendTestMessage( let error: string | undefined; try { - const { AgentMode: AgentModeEnum } = await import("../../lib/eliza/agent-mode-types"); + const { AgentMode: AgentModeEnum } = await import( + "../../lib/eliza/agent-mode-types" + ); // Process message through handler - this emits MESSAGE_RECEIVED event // which triggers plugin-assistant's handleMessage() @@ -467,7 +494,10 @@ export async function sendTestMessage( : undefined, }), new Promise((_, reject) => - setTimeout(() => reject(new Error("Message processing timeout")), timeoutMs), + setTimeout( + () => reject(new Error("Message processing timeout")), + timeoutMs, + ), ), ]); @@ -491,7 +521,10 @@ export async function sendTestMessage( if (debug?.enabled && getLatestDebugTrace && renderDebugTrace) { debugTrace = getLatestDebugTrace(); if (debugTrace) { - debugMarkdown = renderDebugTrace(debugTrace, debug.renderView ?? "summary"); + debugMarkdown = renderDebugTrace( + debugTrace, + debug.renderView ?? "summary", + ); } } @@ -523,7 +556,10 @@ export function getMcpService(runtime: TestRuntime): { /** * Wait for MCP service to be fully initialized */ -export async function waitForMcpReady(runtime: TestRuntime, timeoutMs = 10000): Promise { +export async function waitForMcpReady( + runtime: TestRuntime, + timeoutMs = 10000, +): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const mcpService = getMcpService(runtime); diff --git a/packages/tests/infrastructure/timing.ts b/packages/tests/infrastructure/timing.ts index d72d7e516..2f5b6ad30 100644 --- a/packages/tests/infrastructure/timing.ts +++ b/packages/tests/infrastructure/timing.ts @@ -29,7 +29,10 @@ export function endTimer(name: string): number { /** * Log a set of timings with a label */ -export function logTimings(label: string, timings: Record): void { +export function logTimings( + label: string, + timings: Record, +): void { console.log(`\n[Timings] ${label}:`); const entries = Object.entries(timings).sort((a, b) => b[1] - a[1]); @@ -137,7 +140,9 @@ export class TimingCollector { return durationMs; } - getResults(name: string): Array<{ durationMs: number; metadata?: Record }> { + getResults( + name: string, + ): Array<{ durationMs: number; metadata?: Record }> { return this.timings.get(name)?.results || []; } @@ -150,7 +155,9 @@ export class TimingCollector { printSummary(): void { console.log("\n[TimingCollector] Summary:"); for (const [name, timing] of this.timings) { - const avg = timing.results.reduce((sum, r) => sum + r.durationMs, 0) / timing.results.length; + const avg = + timing.results.reduce((sum, r) => sum + r.durationMs, 0) / + timing.results.length; const min = Math.min(...timing.results.map((r) => r.durationMs)); const max = Math.max(...timing.results.map((r) => r.durationMs)); console.log( diff --git a/packages/tests/integration/affiliates-service.test.ts b/packages/tests/integration/affiliates-service.test.ts index 252291cac..a05022149 100644 --- a/packages/tests/integration/affiliates-service.test.ts +++ b/packages/tests/integration/affiliates-service.test.ts @@ -107,27 +107,40 @@ describe.skipIf(!process.env.DATABASE_URL)("Affiliate System Services", () => { }); test("can update markup percentage within limits (0 - 1000%)", async () => { - const defaultCode = await affiliatesService.getOrCreateAffiliateCode(userA.id); + const defaultCode = await affiliatesService.getOrCreateAffiliateCode( + userA.id, + ); expect(Number(defaultCode.markup_percent)).toBe(20.0); const updated = await affiliatesService.updateMarkup(userA.id, 150.5); expect(Number(updated.markup_percent)).toBe(150.5); - await expect(affiliatesService.updateMarkup(userA.id, 1001)).rejects.toThrow(); - await expect(affiliatesService.updateMarkup(userA.id, -1)).rejects.toThrow(); + await expect( + affiliatesService.updateMarkup(userA.id, 1001), + ).rejects.toThrow(); + await expect( + affiliatesService.updateMarkup(userA.id, -1), + ).rejects.toThrow(); }); test("can link User B to User A's affiliate code", async () => { - const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode(userA.id); - - const link = await affiliatesService.linkUserToAffiliateCode(userB.id, affiliateOfA.code); + const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode( + userA.id, + ); + + const link = await affiliatesService.linkUserToAffiliateCode( + userB.id, + affiliateOfA.code, + ); expect(link).toBeDefined(); expect(link.user_id).toBe(userB.id); expect(link.affiliate_code_id).toBe(affiliateOfA.id); }); test("cannot link user to their own code", async () => { - const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode(userA.id); + const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode( + userA.id, + ); await expect( affiliatesService.linkUserToAffiliateCode(userA.id, affiliateOfA.code), @@ -135,10 +148,18 @@ describe.skipIf(!process.env.DATABASE_URL)("Affiliate System Services", () => { }); test("reuses an existing link for the same affiliate code", async () => { - const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode(userA.id); - - const firstLink = await affiliatesService.linkUserToAffiliateCode(userB.id, affiliateOfA.code); - const secondLink = await affiliatesService.linkUserToAffiliateCode(userB.id, affiliateOfA.code); + const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode( + userA.id, + ); + + const firstLink = await affiliatesService.linkUserToAffiliateCode( + userB.id, + affiliateOfA.code, + ); + const secondLink = await affiliatesService.linkUserToAffiliateCode( + userB.id, + affiliateOfA.code, + ); expect(secondLink.id).toBe(firstLink.id); expect(secondLink.user_id).toBe(userB.id); @@ -146,11 +167,16 @@ describe.skipIf(!process.env.DATABASE_URL)("Affiliate System Services", () => { }); test("can retrieve the referrer for a user correctly", async () => { - const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode(userA.id); + const affiliateOfA = await affiliatesService.getOrCreateAffiliateCode( + userA.id, + ); await affiliatesService.updateMarkup(userA.id, 150.5); try { - await affiliatesService.linkUserToAffiliateCode(userB.id, affiliateOfA.code); + await affiliatesService.linkUserToAffiliateCode( + userB.id, + affiliateOfA.code, + ); } catch { // Ignore if the link already exists. } diff --git a/packages/tests/integration/app-promote-assets.test.ts b/packages/tests/integration/app-promote-assets.test.ts index aa4dd7c0e..de5f81f96 100644 --- a/packages/tests/integration/app-promote-assets.test.ts +++ b/packages/tests/integration/app-promote-assets.test.ts @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test"; const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const API_KEY = process.env.TEST_API_KEY; const TEST_APP_ID = process.env.TEST_APP_ID; @@ -139,7 +141,9 @@ describe("App Promote Assets API", () => { } if (!process.env.TEST_FULL_GENERATION) { - console.log("Skipping: Set TEST_FULL_GENERATION=true to run full generation test"); + console.log( + "Skipping: Set TEST_FULL_GENERATION=true to run full generation test", + ); return; } diff --git a/packages/tests/integration/connected-services.integration.test.ts b/packages/tests/integration/connected-services.integration.test.ts index 95cf57585..4d50735cc 100644 --- a/packages/tests/integration/connected-services.integration.test.ts +++ b/packages/tests/integration/connected-services.integration.test.ts @@ -19,7 +19,9 @@ import { } from "../infrastructure/test-data-factory"; const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const DATABASE_URL = process.env.DATABASE_URL || ""; const TIMEOUT = 60000; @@ -107,7 +109,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { expect(res.status).toBe(200); const data = await res.json(); - const google = data.services?.find((s: { id: string }) => s.id === "google"); + const google = data.services?.find( + (s: { id: string }) => s.id === "google", + ); if (google) { expect(google).toHaveProperty("connected"); @@ -125,7 +129,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { expect(res.status).toBe(200); const data = await res.json(); - const twilio = data.services?.find((s: { id: string }) => s.id === "twilio"); + const twilio = data.services?.find( + (s: { id: string }) => s.id === "twilio", + ); if (twilio) { expect(twilio).toHaveProperty("connected"); @@ -143,7 +149,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { expect(res.status).toBe(200); const data = await res.json(); - const blooio = data.services?.find((s: { id: string }) => s.id === "blooio"); + const blooio = data.services?.find( + (s: { id: string }) => s.id === "blooio", + ); if (blooio) { expect(blooio).toHaveProperty("connected"); @@ -314,18 +322,23 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { const statusData1 = await statusRes1.json(); // Get initial Blooio state - const initialBlooio = statusData1.services?.find((s: { id: string }) => s.id === "blooio"); + const initialBlooio = statusData1.services?.find( + (s: { id: string }) => s.id === "blooio", + ); const _wasConnected = initialBlooio?.connected; // Connect Blooio - const connectRes = await fetch(`${SERVER_URL}/api/v1/connections/blooio`, { - method: "POST", - headers: getTestAuthHeaders(testData.apiKey.key), - body: JSON.stringify({ - apiKey: "test_key_" + Date.now(), - }), - signal: AbortSignal.timeout(TIMEOUT), - }); + const connectRes = await fetch( + `${SERVER_URL}/api/v1/connections/blooio`, + { + method: "POST", + headers: getTestAuthHeaders(testData.apiKey.key), + body: JSON.stringify({ + apiKey: "test_key_" + Date.now(), + }), + signal: AbortSignal.timeout(TIMEOUT), + }, + ); if (connectRes.status === 200) { // Check status again @@ -338,7 +351,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { expect(statusRes2.status).toBe(200); const statusData2 = await statusRes2.json(); - const newBlooio = statusData2.services?.find((s: { id: string }) => s.id === "blooio"); + const newBlooio = statusData2.services?.find( + (s: { id: string }) => s.id === "blooio", + ); // Should now be connected if (newBlooio) { @@ -356,22 +371,28 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { test("disconnecting service updates status", async () => { // First connect - const connectRes = await fetch(`${SERVER_URL}/api/v1/connections/blooio`, { - method: "POST", - headers: getTestAuthHeaders(testData.apiKey.key), - body: JSON.stringify({ - apiKey: "test_key_disconnect_" + Date.now(), - }), - signal: AbortSignal.timeout(TIMEOUT), - }); + const connectRes = await fetch( + `${SERVER_URL}/api/v1/connections/blooio`, + { + method: "POST", + headers: getTestAuthHeaders(testData.apiKey.key), + body: JSON.stringify({ + apiKey: "test_key_disconnect_" + Date.now(), + }), + signal: AbortSignal.timeout(TIMEOUT), + }, + ); if (connectRes.status === 200) { // Disconnect - const disconnectRes = await fetch(`${SERVER_URL}/api/v1/connections/blooio`, { - method: "DELETE", - headers: getTestAuthHeaders(testData.apiKey.key), - signal: AbortSignal.timeout(TIMEOUT), - }); + const disconnectRes = await fetch( + `${SERVER_URL}/api/v1/connections/blooio`, + { + method: "DELETE", + headers: getTestAuthHeaders(testData.apiKey.key), + signal: AbortSignal.timeout(TIMEOUT), + }, + ); expect([200, 204]).toContain(disconnectRes.status); @@ -383,7 +404,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { }); const statusData = await statusRes.json(); - const blooio = statusData.services?.find((s: { id: string }) => s.id === "blooio"); + const blooio = statusData.services?.find( + (s: { id: string }) => s.id === "blooio", + ); // Should now be disconnected if (blooio) { @@ -437,7 +460,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { // Should only see own organization's connections // The other org's Blooio should not appear as connected for this org - const blooio = data.services?.find((s: { id: string }) => s.id === "blooio"); + const blooio = data.services?.find( + (s: { id: string }) => s.id === "blooio", + ); // Our org's Blooio should not be affected by other org's connection expect(blooio?.connected).toBe(false); @@ -460,8 +485,12 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { const data1 = await res1.json(); const data2 = await res2.json(); - const blooio1 = data1.services?.find((s: { id: string }) => s.id === "blooio"); - const _blooio2 = data2.services?.find((s: { id: string }) => s.id === "blooio"); + const blooio1 = data1.services?.find( + (s: { id: string }) => s.id === "blooio", + ); + const _blooio2 = data2.services?.find( + (s: { id: string }) => s.id === "blooio", + ); // First org should not be connected expect(blooio1?.connected).toBe(false); @@ -545,14 +574,17 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { describe("Secrets Storage Security", () => { test("credentials are not returned in plain text", async () => { // Connect a service first - const connectRes = await fetch(`${SERVER_URL}/api/v1/connections/blooio`, { - method: "POST", - headers: getTestAuthHeaders(testData.apiKey.key), - body: JSON.stringify({ - apiKey: "secret_api_key_" + Date.now(), - }), - signal: AbortSignal.timeout(TIMEOUT), - }); + const connectRes = await fetch( + `${SERVER_URL}/api/v1/connections/blooio`, + { + method: "POST", + headers: getTestAuthHeaders(testData.apiKey.key), + body: JSON.stringify({ + apiKey: "secret_api_key_" + Date.now(), + }), + signal: AbortSignal.timeout(TIMEOUT), + }, + ); if (connectRes.status === 200) { // Get status @@ -563,7 +595,9 @@ describe.skipIf(!shouldRun)("Connected Services E2E Tests", () => { }); const statusData = await statusRes.json(); - const blooio = statusData.services?.find((s: { id: string }) => s.id === "blooio"); + const blooio = statusData.services?.find( + (s: { id: string }) => s.id === "blooio", + ); // Should not contain the actual API key if (blooio) { diff --git a/packages/tests/integration/connection-apis.test.ts b/packages/tests/integration/connection-apis.test.ts index 514c966c0..bba01985d 100644 --- a/packages/tests/integration/connection-apis.test.ts +++ b/packages/tests/integration/connection-apis.test.ts @@ -18,8 +18,16 @@ import { const TEST_DB_URL = process.env.DATABASE_URL || ""; const BASE_URL = process.env.TEST_BASE_URL || "http://localhost:3000"; -const TWILIO_SECRET_NAMES = ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_PHONE_NUMBER"]; -const BLOOIO_SECRET_NAMES = ["BLOOIO_API_KEY", "BLOOIO_WEBHOOK_SECRET", "BLOOIO_FROM_NUMBER"]; +const TWILIO_SECRET_NAMES = [ + "TWILIO_ACCOUNT_SID", + "TWILIO_AUTH_TOKEN", + "TWILIO_PHONE_NUMBER", +]; +const BLOOIO_SECRET_NAMES = [ + "BLOOIO_API_KEY", + "BLOOIO_WEBHOOK_SECRET", + "BLOOIO_FROM_NUMBER", +]; let oauthRequestCounter = 0; @@ -40,10 +48,10 @@ async function deleteSecrets( organizationId: string, names: string[], ): Promise { - await client.query(`DELETE FROM secrets WHERE organization_id = $1 AND name = ANY($2::text[])`, [ - organizationId, - names, - ]); + await client.query( + `DELETE FROM secrets WHERE organization_id = $1 AND name = ANY($2::text[])`, + [organizationId, names], + ); } describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { @@ -66,9 +74,10 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { afterAll(async () => { // Clean up any platform credentials - await client.query(`DELETE FROM platform_credentials WHERE organization_id = $1`, [ - testData.organization.id, - ]); + await client.query( + `DELETE FROM platform_credentials WHERE organization_id = $1`, + [testData.organization.id], + ); await client.query(`DELETE FROM secrets WHERE organization_id = $1`, [ testData.organization.id, ]); @@ -85,11 +94,14 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { [testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - Authorization: `Bearer ${testData.apiKey.key}`, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + Authorization: `Bearer ${testData.apiKey.key}`, + }, }, - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -97,7 +109,9 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { }); it("should return 401 without authentication", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`); + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + ); expect(response.status).toBe(401); }); }); @@ -140,7 +154,11 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("Twilio Connection API", () => { describe("GET /api/v1/twilio/status", () => { it("should return disconnected status when not connected", async () => { - await deleteSecrets(client, testData.organization.id, TWILIO_SECRET_NAMES); + await deleteSecrets( + client, + testData.organization.id, + TWILIO_SECRET_NAMES, + ); const response = await fetch(`${BASE_URL}/api/v1/twilio/status`, { headers: { @@ -212,11 +230,15 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("DELETE /api/v1/twilio/disconnect", () => { it("should disconnect Twilio account", async () => { - await twilioAutomationService.storeCredentials(testData.organization.id, testData.user.id, { - accountSid: "ACtest", - authToken: "test_auth_token", - phoneNumber: "+15551234567", - }); + await twilioAutomationService.storeCredentials( + testData.organization.id, + testData.user.id, + { + accountSid: "ACtest", + authToken: "test_auth_token", + phoneNumber: "+15551234567", + }, + ); const response = await fetch(`${BASE_URL}/api/v1/twilio/disconnect`, { method: "DELETE", @@ -233,7 +255,11 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("Blooio Connection API", () => { describe("GET /api/v1/blooio/status", () => { it("should return disconnected status when not connected", async () => { - await deleteSecrets(client, testData.organization.id, BLOOIO_SECRET_NAMES); + await deleteSecrets( + client, + testData.organization.id, + BLOOIO_SECRET_NAMES, + ); const response = await fetch(`${BASE_URL}/api/v1/blooio/status`, { headers: { @@ -303,10 +329,14 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("DELETE /api/v1/blooio/disconnect", () => { it("should disconnect Blooio account", async () => { - await blooioAutomationService.storeCredentials(testData.organization.id, testData.user.id, { - apiKey: "blooio_test_api_key", - fromNumber: "+15559876543", - }); + await blooioAutomationService.storeCredentials( + testData.organization.id, + testData.user.id, + { + apiKey: "blooio_test_api_key", + fromNumber: "+15559876543", + }, + ); const response = await fetch(`${BASE_URL}/api/v1/blooio/disconnect`, { method: "DELETE", @@ -322,16 +352,24 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("Cross-Connection Scenarios", () => { it("should allow multiple services to be connected simultaneously", async () => { - await twilioAutomationService.storeCredentials(testData.organization.id, testData.user.id, { - accountSid: "ACtest", - authToken: "test_auth_token", - phoneNumber: "+15551234567", - }); + await twilioAutomationService.storeCredentials( + testData.organization.id, + testData.user.id, + { + accountSid: "ACtest", + authToken: "test_auth_token", + phoneNumber: "+15551234567", + }, + ); - await blooioAutomationService.storeCredentials(testData.organization.id, testData.user.id, { - apiKey: "blooio_test_api_key", - fromNumber: "+15559876543", - }); + await blooioAutomationService.storeCredentials( + testData.organization.id, + testData.user.id, + { + apiKey: "blooio_test_api_key", + fromNumber: "+15559876543", + }, + ); // Check all statuses const twilioStatus = await fetch(`${BASE_URL}/api/v1/twilio/status`, { @@ -346,7 +384,11 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { }); it("should handle disconnecting one service without affecting others", async () => { - await deleteSecrets(client, testData.organization.id, TWILIO_SECRET_NAMES); + await deleteSecrets( + client, + testData.organization.id, + TWILIO_SECRET_NAMES, + ); // Blooio should still be connected const blooioResult = await client.query( @@ -372,33 +414,40 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { }); it("should handle invalid API key", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - Authorization: "Bearer invalid_key_12345", + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + Authorization: "Bearer invalid_key_12345", + }, }, - }); + ); expect(response.status).toBe(401); }); it("should handle expired API key", async () => { - const { apiKey: expiredKey, plainKey: expiredKeyValue } = await apiKeysService.create({ - name: `Expired Key ${crypto.randomUUID()}`, - organization_id: testData.organization.id, - user_id: testData.user.id, - is_active: true, - }); + const { apiKey: expiredKey, plainKey: expiredKeyValue } = + await apiKeysService.create({ + name: `Expired Key ${crypto.randomUUID()}`, + organization_id: testData.organization.id, + user_id: testData.user.id, + is_active: true, + }); await client.query( `UPDATE api_keys SET expires_at = NOW() - INTERVAL '1 day' WHERE id = $1`, [expiredKey.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - Authorization: `Bearer ${expiredKeyValue}`, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + Authorization: `Bearer ${expiredKeyValue}`, + }, }, - }); + ); // Should be unauthorized due to expired key expect([401, 403]).toContain(response.status); @@ -537,11 +586,15 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { }); it("should handle concurrent disconnect attempts", async () => { - await twilioAutomationService.storeCredentials(testData.organization.id, testData.user.id, { - accountSid: "ACtest", - authToken: "test_auth_token", - phoneNumber: "+15551234567", - }); + await twilioAutomationService.storeCredentials( + testData.organization.id, + testData.user.id, + { + accountSid: "ACtest", + authToken: "test_auth_token", + phoneNumber: "+15551234567", + }, + ); const requests = Array(3) .fill(null) @@ -652,9 +705,12 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("Connection Status Response Format", () => { it("should return consistent structure for Google connections", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { Authorization: `Bearer ${testData.apiKey.key}` }, - }); + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { Authorization: `Bearer ${testData.apiKey.key}` }, + }, + ); expect(response.status).toBe(200); const data = await response.json(); @@ -697,7 +753,11 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { describe("Disconnect Idempotency", () => { it("should succeed when disconnecting already disconnected Twilio", async () => { - await deleteSecrets(client, testData.organization.id, TWILIO_SECRET_NAMES); + await deleteSecrets( + client, + testData.organization.id, + TWILIO_SECRET_NAMES, + ); const response = await fetch(`${BASE_URL}/api/v1/twilio/disconnect`, { method: "DELETE", @@ -708,7 +768,11 @@ describe.skipIf(!TEST_DB_URL)("Connection APIs E2E Tests", () => { }); it("should succeed when disconnecting already disconnected Blooio", async () => { - await deleteSecrets(client, testData.organization.id, BLOOIO_SECRET_NAMES); + await deleteSecrets( + client, + testData.organization.id, + BLOOIO_SECRET_NAMES, + ); const response = await fetch(`${BASE_URL}/api/v1/blooio/disconnect`, { method: "DELETE", diff --git a/packages/tests/integration/discord-connections.test.ts b/packages/tests/integration/discord-connections.test.ts index cdc488495..e109b241a 100644 --- a/packages/tests/integration/discord-connections.test.ts +++ b/packages/tests/integration/discord-connections.test.ts @@ -15,7 +15,8 @@ import { config } from "dotenv"; const preservedHarnessEnv = { CACHE_ENABLED: process.env.CACHE_ENABLED, DATABASE_URL: process.env.DATABASE_URL, - DISABLE_LOCAL_DOCKER_DB_FALLBACK: process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK, + DISABLE_LOCAL_DOCKER_DB_FALLBACK: + process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NODE_ENV: process.env.NODE_ENV, REDIS_URL: process.env.REDIS_URL, @@ -39,7 +40,9 @@ for (const [key, value] of Object.entries(preservedHarnessEnv)) { } const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const API_KEY = process.env.TEST_API_KEY; const TEST_CHARACTER_ID = process.env.TEST_CHARACTER_ID; @@ -51,7 +54,9 @@ let hasTestCharacter = false; beforeAll(async () => { if (!API_KEY) { - console.log("[Discord Connections Tests] No TEST_API_KEY set - auth tests will skip"); + console.log( + "[Discord Connections Tests] No TEST_API_KEY set - auth tests will skip", + ); return; } @@ -67,7 +72,9 @@ beforeAll(async () => { if (TEST_CHARACTER_ID) { hasTestCharacter = true; } else { - console.log("[Discord Connections Tests] No TEST_CHARACTER_ID set - creation tests will skip"); + console.log( + "[Discord Connections Tests] No TEST_CHARACTER_ID set - creation tests will skip", + ); } }); @@ -329,11 +336,15 @@ describe("Discord Connections Get by ID API", () => { // First create a connection const applicationId = `test-get-${Date.now()}`; - const createRes = await fetchWithAuth("/api/v1/discord/connections", "POST", { - applicationId, - botToken: `test-token-${Date.now()}`, - characterId: TEST_CHARACTER_ID, - }); + const createRes = await fetchWithAuth( + "/api/v1/discord/connections", + "POST", + { + applicationId, + botToken: `test-token-${Date.now()}`, + characterId: TEST_CHARACTER_ID, + }, + ); if (createRes.status !== 200) return; @@ -342,7 +353,9 @@ describe("Discord Connections Get by ID API", () => { createdConnectionIds.push(connectionId); // Now get the connection - const res = await fetchWithAuth(`/api/v1/discord/connections/${connectionId}`); + const res = await fetchWithAuth( + `/api/v1/discord/connections/${connectionId}`, + ); expect(res.status).toBe(200); const data = await res.json(); @@ -381,11 +394,15 @@ describe("Discord Connections Update API", () => { if (!apiKeyValid || !hasTestCharacter) return; // First create a connection - const createRes = await fetchWithAuth("/api/v1/discord/connections", "POST", { - applicationId: `test-update-${Date.now()}`, - botToken: `test-token-${Date.now()}`, - characterId: TEST_CHARACTER_ID, - }); + const createRes = await fetchWithAuth( + "/api/v1/discord/connections", + "POST", + { + applicationId: `test-update-${Date.now()}`, + botToken: `test-token-${Date.now()}`, + characterId: TEST_CHARACTER_ID, + }, + ); if (createRes.status !== 200) return; @@ -394,9 +411,13 @@ describe("Discord Connections Update API", () => { createdConnectionIds.push(connectionId); // Update the connection - const res = await fetchWithAuth(`/api/v1/discord/connections/${connectionId}`, "PATCH", { - isActive: false, - }); + const res = await fetchWithAuth( + `/api/v1/discord/connections/${connectionId}`, + "PATCH", + { + isActive: false, + }, + ); expect(res.status).toBe(200); const data = await res.json(); @@ -408,12 +429,16 @@ describe("Discord Connections Update API", () => { if (!apiKeyValid || !hasTestCharacter) return; // First create a connection - const createRes = await fetchWithAuth("/api/v1/discord/connections", "POST", { - applicationId: `test-update-meta-${Date.now()}`, - botToken: `test-token-${Date.now()}`, - characterId: TEST_CHARACTER_ID, - metadata: { responseMode: "always" }, - }); + const createRes = await fetchWithAuth( + "/api/v1/discord/connections", + "POST", + { + applicationId: `test-update-meta-${Date.now()}`, + botToken: `test-token-${Date.now()}`, + characterId: TEST_CHARACTER_ID, + metadata: { responseMode: "always" }, + }, + ); if (createRes.status !== 200) return; @@ -422,9 +447,13 @@ describe("Discord Connections Update API", () => { createdConnectionIds.push(connectionId); // Update the metadata - const res = await fetchWithAuth(`/api/v1/discord/connections/${connectionId}`, "PATCH", { - metadata: { responseMode: "mention" }, - }); + const res = await fetchWithAuth( + `/api/v1/discord/connections/${connectionId}`, + "PATCH", + { + metadata: { responseMode: "mention" }, + }, + ); expect(res.status).toBe(200); const data = await res.json(); @@ -456,11 +485,15 @@ describe("Discord Connections Delete API", () => { if (!apiKeyValid || !hasTestCharacter) return; // First create a connection - const createRes = await fetchWithAuth("/api/v1/discord/connections", "POST", { - applicationId: `test-delete-${Date.now()}`, - botToken: `test-token-${Date.now()}`, - characterId: TEST_CHARACTER_ID, - }); + const createRes = await fetchWithAuth( + "/api/v1/discord/connections", + "POST", + { + applicationId: `test-delete-${Date.now()}`, + botToken: `test-token-${Date.now()}`, + characterId: TEST_CHARACTER_ID, + }, + ); if (createRes.status !== 200) return; @@ -468,7 +501,10 @@ describe("Discord Connections Delete API", () => { const connectionId = createData.connection.id; // Delete the connection - const res = await fetchWithAuth(`/api/v1/discord/connections/${connectionId}`, "DELETE"); + const res = await fetchWithAuth( + `/api/v1/discord/connections/${connectionId}`, + "DELETE", + ); expect(res.status).toBe(200); const data = await res.json(); @@ -476,7 +512,9 @@ describe("Discord Connections Delete API", () => { expect(data).toHaveProperty("message"); // Verify it's deleted - const getRes = await fetchWithAuth(`/api/v1/discord/connections/${connectionId}`); + const getRes = await fetchWithAuth( + `/api/v1/discord/connections/${connectionId}`, + ); expect(getRes.status).toBe(404); }); }); diff --git a/packages/tests/integration/entity-settings-isolation.test.ts b/packages/tests/integration/entity-settings-isolation.test.ts index 5d4085d39..53a7a4227 100644 --- a/packages/tests/integration/entity-settings-isolation.test.ts +++ b/packages/tests/integration/entity-settings-isolation.test.ts @@ -37,12 +37,14 @@ type RunWithRequestContext = ( ) => Promise; const getRequestContext: () => RequestContext | undefined = - "getRequestContext" in elizaCore && typeof elizaCore.getRequestContext === "function" + "getRequestContext" in elizaCore && + typeof elizaCore.getRequestContext === "function" ? (elizaCore.getRequestContext as () => RequestContext | undefined) : () => undefined; const runWithRequestContext: RunWithRequestContext = - "runWithRequestContext" in elizaCore && typeof elizaCore.runWithRequestContext === "function" + "runWithRequestContext" in elizaCore && + typeof elizaCore.runWithRequestContext === "function" ? (elizaCore.runWithRequestContext as RunWithRequestContext) : async (_context, operation) => await operation(); @@ -159,13 +161,21 @@ async function cleanupTestFixtures(fixtures: TestFixtures): Promise { return; } - const userIds = [fixtures.userA?.id, fixtures.userB?.id, fixtures.userC?.id].filter(Boolean); + const userIds = [ + fixtures.userA?.id, + fixtures.userB?.id, + fixtures.userC?.id, + ].filter(Boolean); // Delete in reverse dependency order for (const userId of userIds) { try { - await dbWrite.delete(entitySettings).where(eq(entitySettings.user_id, userId as string)); - await dbWrite.delete(apiKeys).where(eq(apiKeys.user_id, userId as string)); + await dbWrite + .delete(entitySettings) + .where(eq(entitySettings.user_id, userId as string)); + await dbWrite + .delete(apiKeys) + .where(eq(apiKeys.user_id, userId as string)); await dbWrite.delete(users).where(eq(users.id, userId as string)); } catch (e) { console.warn(`[Cleanup] Error cleaning user ${userId}:`, e); @@ -174,7 +184,9 @@ async function cleanupTestFixtures(fixtures: TestFixtures): Promise { if (fixtures.organization?.id) { try { - await dbWrite.delete(organizations).where(eq(organizations.id, fixtures.organization.id)); + await dbWrite + .delete(organizations) + .where(eq(organizations.id, fixtures.organization.id)); } catch (e) { console.warn(`[Cleanup] Error cleaning organization:`, e); } @@ -216,14 +228,22 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { ); // Verify User A's settings - expect(resultA.settings.get("ELIZAOS_API_KEY")).toBe(fixtures.userA.apiKey); - expect(resultA.settings.get("CUSTOM_SETTING")).toBe(fixtures.userA.customSetting); + expect(resultA.settings.get("ELIZAOS_API_KEY")).toBe( + fixtures.userA.apiKey, + ); + expect(resultA.settings.get("CUSTOM_SETTING")).toBe( + fixtures.userA.customSetting, + ); expect(resultA.sources["ELIZAOS_API_KEY"]).toBe("api_keys"); expect(resultA.sources["CUSTOM_SETTING"]).toBe("entity_settings"); // Verify User B's settings are DIFFERENT - expect(resultB.settings.get("ELIZAOS_API_KEY")).toBe(fixtures.userB.apiKey); - expect(resultB.settings.get("CUSTOM_SETTING")).toBe(fixtures.userB.customSetting); + expect(resultB.settings.get("ELIZAOS_API_KEY")).toBe( + fixtures.userB.apiKey, + ); + expect(resultB.settings.get("CUSTOM_SETTING")).toBe( + fixtures.userB.customSetting, + ); // Critical assertion: settings are NOT the same expect(resultA.settings.get("ELIZAOS_API_KEY")).not.toBe( @@ -258,7 +278,9 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { }, () => { const ctx = getRequestContext(); - capturedApiKeyA = ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string; + capturedApiKeyA = ctx?.entitySettings?.get( + "ELIZAOS_API_KEY", + ) as string; }, ); @@ -272,7 +294,9 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { }, () => { const ctx = getRequestContext(); - capturedApiKeyB = ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string; + capturedApiKeyB = ctx?.entitySettings?.get( + "ELIZAOS_API_KEY", + ) as string; }, ); @@ -322,7 +346,9 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { await new Promise((r) => setTimeout(r, Math.random() * 50)); const ctx = getRequestContext(); - const actualApiKey = ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string; + const actualApiKey = ctx?.entitySettings?.get( + "ELIZAOS_API_KEY", + ) as string; results.push({ userId: user.id, @@ -379,17 +405,23 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { async () => { // Level 1 const ctx1 = getRequestContext(); - capturedKeys.push(ctx1?.entitySettings?.get("ELIZAOS_API_KEY") as string); + capturedKeys.push( + ctx1?.entitySettings?.get("ELIZAOS_API_KEY") as string, + ); // Level 2: nested async await Promise.resolve().then(async () => { const ctx2 = getRequestContext(); - capturedKeys.push(ctx2?.entitySettings?.get("ELIZAOS_API_KEY") as string); + capturedKeys.push( + ctx2?.entitySettings?.get("ELIZAOS_API_KEY") as string, + ); // Level 3: setTimeout equivalent await new Promise((resolve) => { const ctx3 = getRequestContext(); - capturedKeys.push(ctx3?.entitySettings?.get("ELIZAOS_API_KEY") as string); + capturedKeys.push( + ctx3?.entitySettings?.get("ELIZAOS_API_KEY") as string, + ); resolve(); }); }); @@ -398,12 +430,16 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { await Promise.all([ (async () => { const ctx = getRequestContext(); - capturedKeys.push(ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string); + capturedKeys.push( + ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string, + ); })(), (async () => { await new Promise((r) => setTimeout(r, 10)); const ctx = getRequestContext(); - capturedKeys.push(ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string); + capturedKeys.push( + ctx?.entitySettings?.get("ELIZAOS_API_KEY") as string, + ); })(), ]); }, @@ -437,8 +473,12 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { ); // Should return equivalent data - expect(result1.settings.get("ELIZAOS_API_KEY")).toBe(result2.settings.get("ELIZAOS_API_KEY")); - expect(result1.settings.get("CUSTOM_SETTING")).toBe(result2.settings.get("CUSTOM_SETTING")); + expect(result1.settings.get("ELIZAOS_API_KEY")).toBe( + result2.settings.get("ELIZAOS_API_KEY"), + ); + expect(result1.settings.get("CUSTOM_SETTING")).toBe( + result2.settings.get("CUSTOM_SETTING"), + ); }); test("cache invalidation forces fresh fetch", async () => { @@ -588,7 +628,9 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { fixtures.agentId, fixtures.organization.id, ); - expect(result2.settings.get("ELIZAOS_API_KEY")).toBe(fixtures.userA.apiKey); + expect(result2.settings.get("ELIZAOS_API_KEY")).toBe( + fixtures.userA.apiKey, + ); expect(result2.sources["ELIZAOS_API_KEY"]).toBe("api_keys"); }); }); @@ -651,11 +693,19 @@ describe.skipIf(!hasRequestContextApis)("Entity Settings Isolation", () => { }); // List global only - const globalSettings = await entitySettingsService.list(fixtures.userA.id, null); - expect(globalSettings.find((s) => s.key === "AGENT_ONLY")).toBeUndefined(); + const globalSettings = await entitySettingsService.list( + fixtures.userA.id, + null, + ); + expect( + globalSettings.find((s) => s.key === "AGENT_ONLY"), + ).toBeUndefined(); // List agent-specific only - const agentSettings = await entitySettingsService.list(fixtures.userA.id, fixtures.agentId); + const agentSettings = await entitySettingsService.list( + fixtures.userA.id, + fixtures.agentId, + ); expect(agentSettings.find((s) => s.key === "AGENT_ONLY")).toBeDefined(); // Cleanup @@ -680,7 +730,11 @@ describe.skipIf(!hasRequestContextApis)("Stress Test: High Concurrency", () => { }); test("100 concurrent requests maintain perfect isolation", async () => { - const users = [stressFixtures.userA, stressFixtures.userB, stressFixtures.userC]; + const users = [ + stressFixtures.userA, + stressFixtures.userB, + stressFixtures.userC, + ]; const errors: string[] = []; const results: Array<{ userId: string; match: boolean }> = []; @@ -724,7 +778,8 @@ describe.skipIf(!hasRequestContextApis)("Stress Test: High Concurrency", () => { })(), ]); - const apiKeyMatch = checks[0] === user.apiKey && checks[1] === user.apiKey; + const apiKeyMatch = + checks[0] === user.apiKey && checks[1] === user.apiKey; const customMatch = checks[2] === user.customSetting; if (!apiKeyMatch || !customMatch) { @@ -733,7 +788,10 @@ describe.skipIf(!hasRequestContextApis)("Stress Test: High Concurrency", () => { ); } - results.push({ userId: user.id, match: apiKeyMatch && customMatch }); + results.push({ + userId: user.id, + match: apiKeyMatch && customMatch, + }); }, ); } catch (error) { diff --git a/packages/tests/integration/financial/concurrent-operations.test.ts b/packages/tests/integration/financial/concurrent-operations.test.ts index b44cb58e2..246114b81 100644 --- a/packages/tests/integration/financial/concurrent-operations.test.ts +++ b/packages/tests/integration/financial/concurrent-operations.test.ts @@ -16,7 +16,10 @@ import { agentBudgetService } from "@/lib/services/agent-budgets"; import { creditsService } from "@/lib/services/credits"; import { organizationsService } from "@/lib/services/organizations"; import { getConnectionString } from "@/tests/helpers/local-database"; -import { cleanupTestData, createTestDataSet } from "@/tests/helpers/test-data-factory"; +import { + cleanupTestData, + createTestDataSet, +} from "@/tests/helpers/test-data-factory"; describe("Cross-Service Concurrent Operations", () => { let connectionString: string; @@ -52,9 +55,11 @@ describe("Cross-Service Concurrent Operations", () => { }), ]); - const directSuccess = directDeduct.status === "fulfilled" && directDeduct.value.success; + const directSuccess = + directDeduct.status === "fulfilled" && directDeduct.value.success; const allocSuccess = - budgetAllocation.status === "fulfilled" && budgetAllocation.value.success; + budgetAllocation.status === "fulfilled" && + budgetAllocation.value.success; // At most one can succeed with full $40 // (both could partially succeed if one gets less) @@ -109,8 +114,12 @@ describe("Cross-Service Concurrent Operations", () => { expect(alloc2.status === "fulfilled" && alloc2.value.success).toBe(true); // Verify no negative balances - const org1 = await organizationsService.getById(testData1.organization.id); - const org2 = await organizationsService.getById(testData2.organization.id); + const org1 = await organizationsService.getById( + testData1.organization.id, + ); + const org2 = await organizationsService.getById( + testData2.organization.id, + ); expect(Number(org1!.credit_balance)).toBeGreaterThanOrEqual(0); expect(Number(org2!.credit_balance)).toBeGreaterThanOrEqual(0); } finally { @@ -189,7 +198,9 @@ describe("Cross-Service Concurrent Operations", () => { ); // Invariant 3: Available budget >= 0 - const available = Number(finalBudget!.allocated_budget) - Number(finalBudget!.spent_budget); + const available = + Number(finalBudget!.allocated_budget) - + Number(finalBudget!.spent_budget); expect(available).toBeGreaterThanOrEqual(0); } finally { await cleanupTestData(connectionString, testData.organization.id); diff --git a/packages/tests/integration/financial/credits-budget-flow.test.ts b/packages/tests/integration/financial/credits-budget-flow.test.ts index f153f2508..636981f69 100644 --- a/packages/tests/integration/financial/credits-budget-flow.test.ts +++ b/packages/tests/integration/financial/credits-budget-flow.test.ts @@ -79,7 +79,9 @@ describe("Credits-Budget Flow Integration", () => { // Step 6: Verify budget decreased const updatedBudget = await agentBudgetService.getOrCreateBudget(agentId); - const remaining = Number(updatedBudget!.allocated_budget) - Number(updatedBudget!.spent_budget); + const remaining = + Number(updatedBudget!.allocated_budget) - + Number(updatedBudget!.spent_budget); expect(remaining).toBe(50); // 100 - 25 - 25 // Step 7: Allocate more budget @@ -117,7 +119,10 @@ describe("Credits-Budget Flow Integration", () => { }); // Check budget - should not allow operations - const checkAfterDepletion = await agentBudgetService.checkBudget(agentId, 1); + const checkAfterDepletion = await agentBudgetService.checkBudget( + agentId, + 1, + ); expect(checkAfterDepletion.canProceed).toBe(false); // Allocate more budget @@ -154,7 +159,10 @@ describe("Credits-Budget Flow Integration", () => { expect(result.success).toBe(false); expect(result.error).toBe("Insufficient organization credits"); } finally { - await cleanupTestData(connectionString, lowCreditTestData.organization.id); + await cleanupTestData( + connectionString, + lowCreditTestData.organization.id, + ); } }); diff --git a/packages/tests/integration/generic-oauth.test.ts b/packages/tests/integration/generic-oauth.test.ts index 142da3c76..e2a0a0d25 100644 --- a/packages/tests/integration/generic-oauth.test.ts +++ b/packages/tests/integration/generic-oauth.test.ts @@ -54,7 +54,9 @@ function isProviderConfigured(providerId: string): boolean { function shouldSkipIfProviderConfigured(providerId: string): boolean { if (isProviderConfigured(providerId)) { - console.log(`Skipping: ${providerId} provider is configured on test server`); + console.log( + `Skipping: ${providerId} provider is configured on test server`, + ); return true; } @@ -63,7 +65,9 @@ function shouldSkipIfProviderConfigured(providerId: string): boolean { function shouldSkipUnlessProviderConfigured(providerId: string): boolean { if (!isProviderConfigured(providerId)) { - console.log(`Skipping: ${providerId} provider is not configured on test server`); + console.log( + `Skipping: ${providerId} provider is not configured on test server`, + ); return true; } @@ -87,15 +91,20 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { client = new Client({ connectionString: TEST_DB_URL }); await client.connect(); - const providersResponse = await fetch(`${BASE_URL}/api/v1/oauth/providers`, { - signal: AbortSignal.timeout(TIMEOUT), - }); + const providersResponse = await fetch( + `${BASE_URL}/api/v1/oauth/providers`, + { + signal: AbortSignal.timeout(TIMEOUT), + }, + ); const providersData = await providersResponse.json(); providerConfigured = Object.fromEntries( - providersData.providers.map((provider: { id: string; configured: boolean }) => [ - provider.id, - provider.configured, - ]), + providersData.providers.map( + (provider: { id: string; configured: boolean }) => [ + provider.id, + provider.configured, + ], + ), ); }); @@ -104,9 +113,10 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { return; } - await client.query(`DELETE FROM platform_credentials WHERE organization_id = $1`, [ - testData.organization.id, - ]); + await client.query( + `DELETE FROM platform_credentials WHERE organization_id = $1`, + [testData.organization.id], + ); await client.end(); await cleanupTestData(TEST_DB_URL, testData.organization.id); }); @@ -375,7 +385,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { // Should not fail - uses defaults const data = await response.json(); - expect(response.status === 200 || data.error === "PLATFORM_NOT_CONFIGURED").toBe(true); + expect( + response.status === 200 || data.error === "PLATFORM_NOT_CONFIGURED", + ).toBe(true); }); it("should handle malformed JSON body gracefully", async () => { @@ -398,7 +410,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { // Should handle gracefully - empty body is fine per the route const data = await response.json(); - expect(response.status === 200 || data.error === "PLATFORM_NOT_CONFIGURED").toBe(true); + expect( + response.status === 200 || data.error === "PLATFORM_NOT_CONFIGURED", + ).toBe(true); }); it("should handle very long platform names", async () => { @@ -455,7 +469,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { // 307 = redirect (expected), 401 = routes not compiled, 429 = rate limited if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } if (response.status === 429) { @@ -478,7 +494,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } if (response.status === 429) { @@ -501,7 +519,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } @@ -529,7 +549,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } @@ -557,7 +579,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } @@ -580,7 +604,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } @@ -603,7 +629,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } @@ -627,7 +655,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); if (response.status === 401) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } @@ -641,15 +671,18 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { Array(15) .fill(null) .map(() => - fetch(`${BASE_URL}/api/v1/oauth/linear/callback?code=test&state=test`, { - ...withOAuthIp( - { - redirect: "manual", - signal: AbortSignal.timeout(TIMEOUT), - }, - "203.0.113.250", - ), - }), + fetch( + `${BASE_URL}/api/v1/oauth/linear/callback?code=test&state=test`, + { + ...withOAuthIp( + { + redirect: "manual", + signal: AbortSignal.timeout(TIMEOUT), + }, + "203.0.113.250", + ), + }, + ), ), ); @@ -657,12 +690,16 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { // If routes not compiled (401), skip if (statuses.every((s) => s === 401)) { - console.log("Skipping: [platform] routes not compiled - restart dev server"); + console.log( + "Skipping: [platform] routes not compiled - restart dev server", + ); return; } // Either rate limited (429), redirected (307), or routes not found - const allValid = statuses.every((s) => s === 429 || s === 307 || s === 401); + const allValid = statuses.every( + (s) => s === 429 || s === 307 || s === 401, + ); expect(allValid).toBe(true); }); }); @@ -739,12 +776,15 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { describe("Generic Adapter via Connections API", () => { it("should list connections for generic providers", async () => { // Test that the generic adapter correctly lists connections - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=linear`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=linear`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -765,12 +805,15 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -780,7 +823,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { expect(data.connection.scopes).toContain("read"); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should revoke generic provider credentials", async () => { @@ -792,26 +837,32 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); expect(data.success).toBe(true); // Verify status changed to revoked - const result = await client.query(`SELECT status FROM platform_credentials WHERE id = $1`, [ - credId, - ]); + const result = await client.query( + `SELECT status FROM platform_credentials WHERE id = $1`, + [credId], + ); expect(result.rows[0].status).toBe("revoked"); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should return 404 when requesting token for connection without secret", async () => { @@ -824,18 +875,23 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should return 404 when requesting token for revoked connection", async () => { @@ -847,18 +903,23 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should return 404 when requesting token for expired connection without refresh token", async () => { @@ -874,18 +935,23 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { [credId, testData.organization.id, fakeSecretId], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); }); @@ -915,7 +981,9 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { const genericProviders = ["linear", "notion", "github", "slack"]; for (const id of genericProviders) { - const provider = data.providers.find((p: { id: string }) => p.id === id); + const provider = data.providers.find( + (p: { id: string }) => p.id === id, + ); expect(provider).toBeDefined(); expect(provider.type).toBe("oauth2"); } @@ -926,11 +994,15 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { const data = await response.json(); // Check Linear - should match env var presence - const linearProvider = data.providers.find((p: { id: string }) => p.id === "linear"); + const linearProvider = data.providers.find( + (p: { id: string }) => p.id === "linear", + ); expect(linearProvider.configured).toBe(isProviderConfigured("linear")); // Check GitHub - should match env var presence - const githubProvider = data.providers.find((p: { id: string }) => p.id === "github"); + const githubProvider = data.providers.find( + (p: { id: string }) => p.id === "github", + ); expect(githubProvider.configured).toBe(isProviderConfigured("github")); }); }); @@ -954,17 +1026,22 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); // Try to access with our API key - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); await cleanupTestData(TEST_DB_URL, otherOrg.organization.id); }); @@ -983,24 +1060,30 @@ describe.skipIf(!TEST_DB_URL)("Generic OAuth Provider E2E Tests", () => { ); // Try to revoke with our API key - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); // Verify connection was NOT revoked - const result = await client.query(`SELECT status FROM platform_credentials WHERE id = $1`, [ - credId, - ]); + const result = await client.query( + `SELECT status FROM platform_credentials WHERE id = $1`, + [credId], + ); expect(result.rows[0].status).toBe("active"); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); await cleanupTestData(TEST_DB_URL, otherOrg.organization.id); }); }); diff --git a/packages/tests/integration/mcp-registry.test.ts b/packages/tests/integration/mcp-registry.test.ts index d19e6bf69..56cd365a3 100644 --- a/packages/tests/integration/mcp-registry.test.ts +++ b/packages/tests/integration/mcp-registry.test.ts @@ -13,11 +13,14 @@ import { test as bunTest, describe, expect } from "bun:test"; const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; // The registry route pulls in a large dependency graph and can exceed the // default request timeout on cold CI webpack compilations. const TIMEOUT = 30000; -const test = (name: string, fn: () => void | Promise) => bunTest(name, fn, TIMEOUT); +const test = (name: string, fn: () => void | Promise) => + bunTest(name, fn, TIMEOUT); interface McpRegistryEntry { id: string; @@ -53,7 +56,9 @@ interface McpRegistryResponse { isAuthenticated: boolean; } -async function fetchRegistry(params?: Record): Promise { +async function fetchRegistry( + params?: Record, +): Promise { const url = new URL(`${SERVER_URL}/api/mcp/registry`); if (params) { Object.entries(params).forEach(([key, value]) => { @@ -100,7 +105,9 @@ describe("MCP Registry API", () => { const res = await fetchRegistry(); const data: McpRegistryResponse = await res.json(); - const elizaPlatform = data.registry.find((entry) => entry.id === "eliza-platform"); + const elizaPlatform = data.registry.find( + (entry) => entry.id === "eliza-platform", + ); expect(elizaPlatform).toBeDefined(); expect(elizaPlatform!.status).toBe("coming_soon"); @@ -114,7 +121,9 @@ describe("MCP Registry API", () => { const data: McpRegistryResponse = await res.json(); // eliza-platform should NOT be in live results - const elizaPlatform = data.registry.find((entry) => entry.id === "eliza-platform"); + const elizaPlatform = data.registry.find( + (entry) => entry.id === "eliza-platform", + ); expect(elizaPlatform).toBeUndefined(); // All returned entries should be live @@ -127,7 +136,9 @@ describe("MCP Registry API", () => { const res = await fetchRegistry({ status: "coming_soon" }); const data: McpRegistryResponse = await res.json(); - const elizaPlatform = data.registry.find((entry) => entry.id === "eliza-platform"); + const elizaPlatform = data.registry.find( + (entry) => entry.id === "eliza-platform", + ); expect(elizaPlatform).toBeDefined(); // All returned entries should be coming_soon @@ -140,7 +151,9 @@ describe("MCP Registry API", () => { const res = await fetchRegistry({ category: "platform" }); const data: McpRegistryResponse = await res.json(); - const elizaPlatform = data.registry.find((entry) => entry.id === "eliza-platform"); + const elizaPlatform = data.registry.find( + (entry) => entry.id === "eliza-platform", + ); expect(elizaPlatform).toBeDefined(); // All returned entries should be in platform category @@ -153,7 +166,9 @@ describe("MCP Registry API", () => { const res = await fetchRegistry({ category: "finance" }); const data: McpRegistryResponse = await res.json(); - const cryptoPrices = data.registry.find((entry) => entry.id === "crypto-prices"); + const cryptoPrices = data.registry.find( + (entry) => entry.id === "crypto-prices", + ); expect(cryptoPrices).toBeDefined(); expect(cryptoPrices!.status).toBe("live"); }); diff --git a/packages/tests/integration/message-router.test.ts b/packages/tests/integration/message-router.test.ts index 1f77094cc..109f47d2d 100644 --- a/packages/tests/integration/message-router.test.ts +++ b/packages/tests/integration/message-router.test.ts @@ -5,7 +5,15 @@ * Covers: phone number registration, message routing, agent processing, response sending. */ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "bun:test"; import { Client } from "pg"; import { v4 as uuidv4 } from "uuid"; import { @@ -42,9 +50,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { (SELECT id FROM agent_phone_numbers WHERE organization_id = $1)`, [testData.organization.id], ); - await client.query(`DELETE FROM agent_phone_numbers WHERE organization_id = $1`, [ - testData.organization.id, - ]); + await client.query( + `DELETE FROM agent_phone_numbers WHERE organization_id = $1`, + [testData.organization.id], + ); await client.end(); await cleanupTestData(TEST_DB_URL, testData.organization.id); @@ -54,10 +63,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { it("should register a Twilio phone number for an agent", async () => { // Create a test agent first const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Test SMS Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Test SMS Agent"], + ); // Register phone number const phoneNumber = "+15551234567"; @@ -73,16 +82,18 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { expect(result.rows[0].id).toBeDefined(); // Cleanup - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [result.rows[0].id]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + result.rows[0].id, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); it("should register a Blooio phone number for iMessage", async () => { const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Test iMessage Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Test iMessage Agent"], + ); const phoneNumber = "+15559876543"; const result = await client.query( @@ -96,16 +107,18 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { expect(result.rows[0]).toBeDefined(); // Cleanup - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [result.rows[0].id]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + result.rows[0].id, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); it("should prevent duplicate phone numbers in same organization", async () => { const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Test Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Test Agent"], + ); const phoneNumber = "+15551111111"; @@ -134,7 +147,9 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { expect(duplicateError).toBe(true); // Cleanup - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [result1.rows[0].id]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + result1.rows[0].id, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); }); @@ -146,10 +161,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { beforeEach(async () => { agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Routing Test Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Routing Test Agent"], + ); const result = await client.query( `INSERT INTO agent_phone_numbers @@ -163,10 +178,13 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { afterEach(async () => { if (phoneNumberId) { - await client.query(`DELETE FROM phone_message_log WHERE phone_number_id = $1`, [ + await client.query( + `DELETE FROM phone_message_log WHERE phone_number_id = $1`, + [phoneNumberId], + ); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ phoneNumberId, ]); - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [phoneNumberId]); } if (agentId) { @@ -222,9 +240,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { const messageId = insertResult.rows[0].id; // Update status to processing - await client.query(`UPDATE phone_message_log SET status = 'processing' WHERE id = $1`, [ - messageId, - ]); + await client.query( + `UPDATE phone_message_log SET status = 'processing' WHERE id = $1`, + [messageId], + ); // Simulate agent response const agentResponse = "Thanks for your message! How can I help?"; @@ -263,10 +282,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { describe("Phone Number Capabilities", () => { it("should track SMS capabilities correctly", async () => { const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "SMS Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "SMS Agent"], + ); const result = await client.query( `INSERT INTO agent_phone_numbers @@ -283,16 +302,18 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { expect(result.rows[0].can_voice).toBe(false); // Cleanup - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [result.rows[0].id]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + result.rows[0].id, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); it("should track voice capabilities for Twilio", async () => { const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Voice Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Voice Agent"], + ); const result = await client.query( `INSERT INTO agent_phone_numbers @@ -307,7 +328,9 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { expect(result.rows[0].can_voice).toBe(true); // Cleanup - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [result.rows[0].id]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + result.rows[0].id, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); }); @@ -318,10 +341,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { beforeAll(async () => { agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Log Query Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Log Query Agent"], + ); const result = await client.query( `INSERT INTO agent_phone_numbers @@ -351,10 +374,13 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { }); afterAll(async () => { - await client.query(`DELETE FROM phone_message_log WHERE phone_number_id = $1`, [ + await client.query( + `DELETE FROM phone_message_log WHERE phone_number_id = $1`, + [phoneNumberId], + ); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ phoneNumberId, ]); - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [phoneNumberId]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); @@ -401,10 +427,10 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { describe("Rate Limiting", () => { it("should track rate limit settings", async () => { const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Rate Limited Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Rate Limited Agent"], + ); const result = await client.query( `INSERT INTO agent_phone_numbers @@ -419,7 +445,9 @@ describe.skipIf(!TEST_DB_URL)("MessageRouterService E2E Tests", () => { expect(result.rows[0].max_messages_per_day).toBe("500"); // Cleanup - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [result.rows[0].id]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + result.rows[0].id, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); }); }); diff --git a/packages/tests/integration/model-catalog-live-server.live.e2e.test.ts b/packages/tests/integration/model-catalog-live-server.live.e2e.test.ts index a613983fd..c225f8b4c 100644 --- a/packages/tests/integration/model-catalog-live-server.live.e2e.test.ts +++ b/packages/tests/integration/model-catalog-live-server.live.e2e.test.ts @@ -52,7 +52,8 @@ function hasUsableCacheConfig(): boolean { ); } -const shouldRun = hasUsableGatewayConfig() && hasUsableCronConfig() && hasUsableCacheConfig(); +const shouldRun = + hasUsableGatewayConfig() && hasUsableCronConfig() && hasUsableCacheConfig(); async function readCachedCatalog(): Promise { return await cache.get(MODEL_CACHE_KEY); @@ -121,7 +122,11 @@ describe.skipIf(!shouldRun)("Model catalog live server E2E", () => { expect(secondCached).not.toBeNull(); expect(secondCached?.cachedAt).toBe(firstCached?.cachedAt); - expect(statusBody.models.every((model: { available: boolean }) => model.available)).toBe(true); + expect( + statusBody.models.every( + (model: { available: boolean }) => model.available, + ), + ).toBe(true); }); test("refreshes the shared cache via the live cron endpoint", async () => { @@ -136,13 +141,16 @@ describe.skipIf(!shouldRun)("Model catalog live server E2E", () => { await new Promise((resolve) => setTimeout(resolve, 10)); - const cronResponse = await fetch(`${BASE_URL}/api/v1/cron/refresh-model-catalog`, { - method: "GET", - headers: { - Authorization: `Bearer ${process.env.CRON_SECRET}`, + const cronResponse = await fetch( + `${BASE_URL}/api/v1/cron/refresh-model-catalog`, + { + method: "GET", + headers: { + Authorization: `Bearer ${process.env.CRON_SECRET}`, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(cronResponse.status).toBe(200); diff --git a/packages/tests/integration/phone-mapping.integration.test.ts b/packages/tests/integration/phone-mapping.integration.test.ts index e22a58796..e6fdde8db 100644 --- a/packages/tests/integration/phone-mapping.integration.test.ts +++ b/packages/tests/integration/phone-mapping.integration.test.ts @@ -47,7 +47,10 @@ describe.skipIf(!TEST_DB_URL)("Agent Phone Number Mapping E2E Tests", () => { (SELECT id FROM agent_phone_numbers WHERE agent_id = $1)`, [agentId], ); - await client.query(`DELETE FROM agent_phone_numbers WHERE agent_id = $1`, [agentId]); + await client.query( + `DELETE FROM agent_phone_numbers WHERE agent_id = $1`, + [agentId], + ); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); } @@ -58,10 +61,10 @@ describe.skipIf(!TEST_DB_URL)("Agent Phone Number Mapping E2E Tests", () => { // Helper to create test agent async function createTestAgent(name: string): Promise { const agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - name, - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, name], + ); testAgents.push(agentId); return agentId; } @@ -299,9 +302,10 @@ describe.skipIf(!TEST_DB_URL)("Agent Phone Number Mapping E2E Tests", () => { expect(insertResult.rows[0].last_message_at).toBeNull(); // Update last_message_at - await client.query(`UPDATE agent_phone_numbers SET last_message_at = NOW() WHERE id = $1`, [ - phoneId, - ]); + await client.query( + `UPDATE agent_phone_numbers SET last_message_at = NOW() WHERE id = $1`, + [phoneId], + ); const selectResult = await client.query( `SELECT last_message_at FROM agent_phone_numbers WHERE id = $1`, @@ -444,10 +448,10 @@ describe.skipIf(!TEST_DB_URL)("Agent Phone Number Mapping E2E Tests", () => { const agentId1 = await createTestAgent("Org1 Agent"); const agentId2 = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId2, - "Org2 Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId2, "Org2 Agent"], + ); // Same phone number in different orgs (should be allowed) const phoneNumber = "+15551000060"; @@ -485,9 +489,10 @@ describe.skipIf(!TEST_DB_URL)("Agent Phone Number Mapping E2E Tests", () => { expect(result2.rows[0].agent_id).toBe(agentId2); // Cleanup org2 - await client.query(`DELETE FROM agent_phone_numbers WHERE organization_id = $1`, [ - org2.organization.id, - ]); + await client.query( + `DELETE FROM agent_phone_numbers WHERE organization_id = $1`, + [org2.organization.id], + ); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId2]); await cleanupTestData(TEST_DB_URL, org2.organization.id); }); @@ -559,7 +564,9 @@ describe.skipIf(!TEST_DB_URL)("Agent Phone Number Mapping E2E Tests", () => { expect(parseInt(beforeDelete.rows[0].count)).toBe(2); // Delete phone number (should cascade to message logs) - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [phoneId]); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + phoneId, + ]); // Verify logs are deleted const afterDelete = await client.query( diff --git a/packages/tests/integration/promotion-preview.test.ts b/packages/tests/integration/promotion-preview.test.ts index ca9871720..c8db45fd0 100644 --- a/packages/tests/integration/promotion-preview.test.ts +++ b/packages/tests/integration/promotion-preview.test.ts @@ -12,7 +12,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const API_KEY = process.env.TEST_API_KEY; const TIMEOUT = 30000; // 30 seconds for AI generation @@ -39,7 +41,9 @@ async function fetchWithAuth( // Validate API key before running tests beforeAll(async () => { if (!API_KEY) { - console.log("[Promotion Preview Tests] No TEST_API_KEY set - auth tests will skip"); + console.log( + "[Promotion Preview Tests] No TEST_API_KEY set - auth tests will skip", + ); return; } @@ -48,7 +52,9 @@ beforeAll(async () => { apiKeyValid = res.status !== 401; if (!apiKeyValid) { - console.log("[Promotion Preview Tests] TEST_API_KEY is invalid - auth tests will skip"); + console.log( + "[Promotion Preview Tests] TEST_API_KEY is invalid - auth tests will skip", + ); } else { console.log("[Promotion Preview Tests] TEST_API_KEY is valid"); } @@ -58,11 +64,14 @@ describe("Promotion Preview API", () => { const FAKE_APP_ID = "00000000-0000-0000-0000-000000000000"; test("returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/promote/preview`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ platforms: ["discord"], count: 1 }), - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/promote/preview`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ platforms: ["discord"], count: 1 }), + }, + ); expect(res.status).toBe(401); }); @@ -90,10 +99,14 @@ describe("Promotion Preview API", () => { return; } - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/promote/preview`, "POST", { - platforms: [], - count: 1, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/promote/preview`, + "POST", + { + platforms: [], + count: 1, + }, + ); expect(res.status).toBe(400); const data = await res.json(); @@ -106,10 +119,14 @@ describe("Promotion Preview API", () => { return; } - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/promote/preview`, "POST", { - platforms: ["invalid_platform"], - count: 1, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/promote/preview`, + "POST", + { + platforms: ["invalid_platform"], + count: 1, + }, + ); expect(res.status).toBe(400); const data = await res.json(); @@ -139,10 +156,14 @@ describe("Promotion Preview API", () => { return; } - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/promote/preview`, "POST", { - platforms: ["discord"], - count: 1, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/promote/preview`, + "POST", + { + platforms: ["discord"], + count: 1, + }, + ); expect(res.status).toBe(404); const data = await res.json(); @@ -160,10 +181,14 @@ describe("Promotion Preview API", () => { const validPlatforms = ["discord", "telegram", "twitter"]; for (const platform of validPlatforms) { - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/promote/preview`, "POST", { - platforms: [platform], - count: 1, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/promote/preview`, + "POST", + { + platforms: [platform], + count: 1, + }, + ); // Should fail with 404 (app not found), not 400 (invalid request) expect(res.status).toBe(404); } @@ -175,10 +200,14 @@ describe("Promotion Preview API", () => { return; } - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/promote/preview`, "POST", { - platforms: ["discord", "telegram", "twitter"], - count: 2, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/promote/preview`, + "POST", + { + platforms: ["discord", "telegram", "twitter"], + count: 2, + }, + ); // Should fail with 404 (app not found), not 400 (invalid request) expect(res.status).toBe(404); }); diff --git a/packages/tests/integration/promotion-pricing.test.ts b/packages/tests/integration/promotion-pricing.test.ts index 0d10afa44..5a4d37105 100644 --- a/packages/tests/integration/promotion-pricing.test.ts +++ b/packages/tests/integration/promotion-pricing.test.ts @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test"; const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const API_KEY = process.env.TEST_API_KEY; const TEST_APP_ID = process.env.TEST_APP_ID || "test-app-id"; @@ -141,16 +143,20 @@ describe("Promotion Pricing Constants", () => { }); test("PROMO_IMAGE_COST includes 20% markup", async () => { - const { PROMO_IMAGE_COST, PLATFORM_MARKUP } = await import("@/lib/promotion-pricing"); + const { PROMO_IMAGE_COST, PLATFORM_MARKUP } = await import( + "@/lib/promotion-pricing" + ); const { IMAGE_GENERATION_COST } = await import("@/lib/pricing-constants"); expect(PROMO_IMAGE_COST).toBe(IMAGE_GENERATION_COST * PLATFORM_MARKUP); }); test("estimateAssetGenerationCost returns correct values", async () => { - const { estimateAssetGenerationCost, PROMO_IMAGE_COST, AD_COPY_GENERATION_COST } = await import( - "@/lib/promotion-pricing" - ); + const { + estimateAssetGenerationCost, + PROMO_IMAGE_COST, + AD_COPY_GENERATION_COST, + } = await import("@/lib/promotion-pricing"); // Default: 1 image + 1 banner + copy const estimate = estimateAssetGenerationCost({}); @@ -171,9 +177,12 @@ describe("Promotion Pricing Constants", () => { }); test("getPostCost returns correct values for each platform", async () => { - const { getPostCost, DISCORD_POST_COST, TELEGRAM_POST_COST, TWITTER_POST_COST } = await import( - "@/lib/promotion-pricing" - ); + const { + getPostCost, + DISCORD_POST_COST, + TELEGRAM_POST_COST, + TWITTER_POST_COST, + } = await import("@/lib/promotion-pricing"); expect(getPostCost("discord")).toBe(DISCORD_POST_COST); expect(getPostCost("telegram")).toBe(TELEGRAM_POST_COST); diff --git a/packages/tests/integration/revenue-splits.test.ts b/packages/tests/integration/revenue-splits.test.ts index d24d1f5f3..351d67307 100644 --- a/packages/tests/integration/revenue-splits.test.ts +++ b/packages/tests/integration/revenue-splits.test.ts @@ -58,125 +58,132 @@ async function cleanupOrganization(id: string) { .catch(() => {}); } -describe.skipIf(!process.env.DATABASE_URL)("Revenue Splits & Multi-Tier Referrals", () => { - beforeAll(async () => { - mock.restore(); - ({ referralsService } = await import("../../lib/services/referrals")); - - userA = await createUser("User A", `usera-${Date.now()}@test.com`); - userB = await createUser("User B", `userb-${Date.now()}@test.com`); - userC = await createUser("Editor C", `userc-${Date.now()}@test.com`); - userD = await createUser("Creator D", `userd-${Date.now()}@test.com`); - unreferredUser = await createUser("Unreferred", `unref-${Date.now()}@test.com`); - buyer = await createUser("Buyer", `buyer-${Date.now()}@test.com`); - appOwner = await createUser("App Owner", `owner-${Date.now()}@test.com`); - }); +describe.skipIf(!process.env.DATABASE_URL)( + "Revenue Splits & Multi-Tier Referrals", + () => { + beforeAll(async () => { + mock.restore(); + ({ referralsService } = await import("../../lib/services/referrals")); + + userA = await createUser("User A", `usera-${Date.now()}@test.com`); + userB = await createUser("User B", `userb-${Date.now()}@test.com`); + userC = await createUser("Editor C", `userc-${Date.now()}@test.com`); + userD = await createUser("Creator D", `userd-${Date.now()}@test.com`); + unreferredUser = await createUser( + "Unreferred", + `unref-${Date.now()}@test.com`, + ); + buyer = await createUser("Buyer", `buyer-${Date.now()}@test.com`); + appOwner = await createUser("App Owner", `owner-${Date.now()}@test.com`); + }); - afterAll(async () => { - for (const id of allUserIds) { - await cleanupUser(id); - } - for (const id of allOrganizationIds) { - await cleanupOrganization(id); - } - }); + afterAll(async () => { + for (const id of allUserIds) { + await cleanupUser(id); + } + for (const id of allOrganizationIds) { + await cleanupOrganization(id); + } + }); - it("Test 1: First-Touch Immutability", async () => { - const codeX = await referralsService.getOrCreateCode(userB.id); - const codeY = await referralsService.getOrCreateCode(userC.id); - - // Apply Code X to User A - const res1 = await referralsService.applyReferralCode( - userA.id, - userA.organization_id, - codeX.code, - ); - expect(res1.success).toBe(true); - - // Verify it was correctly recorded - const signup = await referralSignupsRepository.findByReferredUserId(userA.id); - expect(signup?.referrer_user_id).toBe(userB.id); - - // Attempt to apply Code Y to User A (Should Fail) - const res2 = await referralsService.applyReferralCode( - userA.id, - userA.organization_id, - codeY.code, - ); - expect(res2.success).toBe(false); - expect(res2.message).toBe("Already used a referral code"); - - // Verify standard 50/40/10 split calculates properly - const { splits } = await referralsService.calculateRevenueSplits(userA.id, 100); - expect(splits).toContainEqual({ - userId: userB.id, - role: "creator", - amount: 10, + it("Test 1: First-Touch Immutability", async () => { + const codeX = await referralsService.getOrCreateCode(userB.id); + const codeY = await referralsService.getOrCreateCode(userC.id); + + // Apply Code X to User A + const res1 = await referralsService.applyReferralCode( + userA.id, + userA.organization_id, + codeX.code, + ); + expect(res1.success).toBe(true); + + // Verify it was correctly recorded + const signup = await referralSignupsRepository.findByReferredUserId( + userA.id, + ); + expect(signup?.referrer_user_id).toBe(userB.id); + + // Attempt to apply Code Y to User A (Should Fail) + const res2 = await referralsService.applyReferralCode( + userA.id, + userA.organization_id, + codeY.code, + ); + expect(res2.success).toBe(false); + expect(res2.message).toBe("Already used a referral code"); + + // Verify standard 50/40/10 split calculates properly + const { splits } = await referralsService.calculateRevenueSplits( + userA.id, + 100, + ); + expect(splits).toContainEqual({ + userId: userB.id, + role: "creator", + amount: 10, + }); }); - }); - it("Test 2: No Referrer (100% Platform)", async () => { - const { elizaCloudAmount, splits } = await referralsService.calculateRevenueSplits( - unreferredUser.id, - 100, - ); + it("Test 2: No Referrer (100% Platform)", async () => { + const { elizaCloudAmount, splits } = + await referralsService.calculateRevenueSplits(unreferredUser.id, 100); - expect(elizaCloudAmount).toBe(100); - expect(splits.length).toBe(0); - }); + expect(elizaCloudAmount).toBe(100); + expect(splits.length).toBe(0); + }); - it("Test 3: Multi-Tier Referrals (Nano Banana flow)", async () => { - // Editor code - const editorCode = await referralsService.getOrCreateCode(userC.id); - - // Creator code, linked to editor code via parent_referral_id - const creatorCode = await referralsService.getOrCreateCode(userD.id); - - // Update creator code to have the parent - const { referralCodes } = await import("@/db/schemas/referrals"); - await dbWrite - .update(referralCodes) - .set({ parent_referral_id: editorCode.id }) - .where(eq(referralCodes.id, creatorCode.id)); - - // Buyer signs up using Creator's nano banana code, context passes App Owner - const res = await referralsService.applyReferralCode( - buyer.id, - buyer.organization_id, - creatorCode.code, - { - appOwnerId: appOwner.id, - }, - ); - expect(res.success).toBe(true); - - // Calculate splits on $100 - const { elizaCloudAmount, splits } = await referralsService.calculateRevenueSplits( - buyer.id, - 100, - ); - - // Exact requested math: - // ElizaCloud: 50 - // App Owner: 40 - // Editor: 2 - // Creator: 8 - - expect(elizaCloudAmount).toBe(50); - - // Find app owner split - const appOwnerSplit = splits.find((s) => s.role === "app_owner"); - expect(appOwnerSplit!.userId).toBe(appOwner.id); - expect(appOwnerSplit!.amount).toBe(40); - - // Find editor split - const editorSplit = splits.find((s) => s.role === "editor"); - expect(editorSplit!.userId).toBe(userC.id); - expect(editorSplit!.amount).toBe(2); - - // Find creator split - const creatorSplit = splits.find((s) => s.role === "creator"); - expect(creatorSplit!.userId).toBe(userD.id); - expect(creatorSplit!.amount).toBe(8); - }); -}); + it("Test 3: Multi-Tier Referrals (Nano Banana flow)", async () => { + // Editor code + const editorCode = await referralsService.getOrCreateCode(userC.id); + + // Creator code, linked to editor code via parent_referral_id + const creatorCode = await referralsService.getOrCreateCode(userD.id); + + // Update creator code to have the parent + const { referralCodes } = await import("@/db/schemas/referrals"); + await dbWrite + .update(referralCodes) + .set({ parent_referral_id: editorCode.id }) + .where(eq(referralCodes.id, creatorCode.id)); + + // Buyer signs up using Creator's nano banana code, context passes App Owner + const res = await referralsService.applyReferralCode( + buyer.id, + buyer.organization_id, + creatorCode.code, + { + appOwnerId: appOwner.id, + }, + ); + expect(res.success).toBe(true); + + // Calculate splits on $100 + const { elizaCloudAmount, splits } = + await referralsService.calculateRevenueSplits(buyer.id, 100); + + // Exact requested math: + // ElizaCloud: 50 + // App Owner: 40 + // Editor: 2 + // Creator: 8 + + expect(elizaCloudAmount).toBe(50); + + // Find app owner split + const appOwnerSplit = splits.find((s) => s.role === "app_owner"); + expect(appOwnerSplit!.userId).toBe(appOwner.id); + expect(appOwnerSplit!.amount).toBe(40); + + // Find editor split + const editorSplit = splits.find((s) => s.role === "editor"); + expect(editorSplit!.userId).toBe(userC.id); + expect(editorSplit!.amount).toBe(2); + + // Find creator split + const creatorSplit = splits.find((s) => s.role === "creator"); + expect(creatorSplit!.userId).toBe(userD.id); + expect(creatorSplit!.amount).toBe(8); + }); + }, +); diff --git a/packages/tests/integration/services/agent-budgets.service.test.ts b/packages/tests/integration/services/agent-budgets.service.test.ts index 1088624b9..1e2f14ddb 100644 --- a/packages/tests/integration/services/agent-budgets.service.test.ts +++ b/packages/tests/integration/services/agent-budgets.service.test.ts @@ -13,11 +13,21 @@ * */ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { eq } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { dbWrite } from "@/db/client"; -import { agentBudgets, agentBudgetTransactions } from "@/db/schemas/agent-budgets"; +import { + agentBudgets, + agentBudgetTransactions, +} from "@/db/schemas/agent-budgets"; import { agentBudgetService } from "@/lib/services/agent-budgets"; import { getConnectionString } from "@/tests/helpers/local-database"; import { @@ -55,7 +65,9 @@ describe("AgentBudgetService", () => { await dbWrite .delete(agentBudgetTransactions) .where(eq(agentBudgetTransactions.agent_id, agentId)); - await dbWrite.delete(agentBudgets).where(eq(agentBudgets.agent_id, agentId)); + await dbWrite + .delete(agentBudgets) + .where(eq(agentBudgets.agent_id, agentId)); } // =========================================================================== @@ -511,8 +523,12 @@ describe("AgentBudgetService", () => { const results = await Promise.allSettled(promises); // Assert: Count successes and failures - const successes = results.filter((r) => r.status === "fulfilled" && r.value.success); - const failures = results.filter((r) => r.status === "fulfilled" && !r.value.success); + const successes = results.filter( + (r) => r.status === "fulfilled" && r.value.success, + ); + const failures = results.filter( + (r) => r.status === "fulfilled" && !r.value.success, + ); // Exactly 10 should succeed (we have $10, each deduction is $1) expect(successes.length).toBe(10); @@ -520,7 +536,8 @@ describe("AgentBudgetService", () => { // CRITICAL: Final balance must be exactly $0, NEVER negative const budget = await agentBudgetService.getBudget(agentId); - const finalBalance = Number(budget!.allocated_budget) - Number(budget!.spent_budget); + const finalBalance = + Number(budget!.allocated_budget) - Number(budget!.spent_budget); expect(finalBalance).toBe(0); expect(finalBalance).toBeGreaterThanOrEqual(0); @@ -696,7 +713,8 @@ describe("AgentBudgetService", () => { // Verify balance is below threshold before auto-refill let budget = await agentBudgetService.getBudget(agentId); - let balance = Number(budget!.allocated_budget) - Number(budget!.spent_budget); + let balance = + Number(budget!.allocated_budget) - Number(budget!.spent_budget); expect(balance).toBe(5); // 20 - 15 = 5 // Enable auto-refill AFTER the deduction @@ -931,7 +949,8 @@ describe("AgentBudgetService", () => { const fakeAgentId = uuidv4(); // Act - const transactions = await agentBudgetService.getTransactions(fakeAgentId); + const transactions = + await agentBudgetService.getTransactions(fakeAgentId); // Assert expect(transactions).toEqual([]); diff --git a/packages/tests/integration/services/api-keys.service.test.ts b/packages/tests/integration/services/api-keys.service.test.ts index 1f0aea305..51c5aed31 100644 --- a/packages/tests/integration/services/api-keys.service.test.ts +++ b/packages/tests/integration/services/api-keys.service.test.ts @@ -14,7 +14,14 @@ * */ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import crypto from "crypto"; import { v4 as uuidv4 } from "uuid"; import { API_KEY_PREFIX_LENGTH } from "@/lib/pricing"; @@ -93,7 +100,10 @@ describe("ApiKeysService", () => { const generated = apiKeysService.generateApiKey(); // Act - Manually compute hash - const manualHash = crypto.createHash("sha256").update(generated.key).digest("hex"); + const manualHash = crypto + .createHash("sha256") + .update(generated.key) + .digest("hex"); // Assert expect(generated.hash).toBe(manualHash); @@ -332,7 +342,10 @@ describe("ApiKeysService", () => { const { apiKey, plainKey } = await apiKeysService.create(keyData); // Assert - The stored key_hash should be SHA256 of plainKey - const expectedHash = crypto.createHash("sha256").update(plainKey).digest("hex"); + const expectedHash = crypto + .createHash("sha256") + .update(plainKey) + .digest("hex"); expect(apiKey.key_hash).toBe(expectedHash); // Cleanup diff --git a/packages/tests/integration/services/caching-speedup.test.ts b/packages/tests/integration/services/caching-speedup.test.ts index 620ebebc1..b3f893fc5 100644 --- a/packages/tests/integration/services/caching-speedup.test.ts +++ b/packages/tests/integration/services/caching-speedup.test.ts @@ -122,11 +122,17 @@ describe("Caching Speedup and Behavior End-to-End Tests", () => { await cache.del(`mcp:slug:${testData.organization.id}:${mcpSlug}:v1`); const t1 = performance.now(); - const mcp1 = await userMcpsService.getBySlug(mcpSlug, testData.organization.id); + const mcp1 = await userMcpsService.getBySlug( + mcpSlug, + testData.organization.id, + ); const t2 = performance.now(); const t3 = performance.now(); - const mcp2 = await userMcpsService.getBySlug(mcpSlug, testData.organization.id); + const mcp2 = await userMcpsService.getBySlug( + mcpSlug, + testData.organization.id, + ); const t4 = performance.now(); expect(mcp1).toBeDefined(); @@ -155,7 +161,10 @@ describe("Caching Speedup and Behavior End-to-End Tests", () => { name: "Affiliate Linker", }); - const newCode = await affiliatesService.getOrCreateAffiliateCode(testData.user.id, 20); + const newCode = await affiliatesService.getOrCreateAffiliateCode( + testData.user.id, + 20, + ); _affiliateCodeStr = newCode.code; // Create a second test user to be the referee diff --git a/packages/tests/integration/services/credits.service.test.ts b/packages/tests/integration/services/credits.service.test.ts index 0cc579726..4684124ff 100644 --- a/packages/tests/integration/services/credits.service.test.ts +++ b/packages/tests/integration/services/credits.service.test.ts @@ -13,7 +13,14 @@ * @see https://martinfowler.com/bliki/UnitTest.html (Sociable vs Solitary) */ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { eq } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { dbRead } from "@/db/client"; @@ -135,7 +142,9 @@ describe("CreditsService", () => { const org = await dbRead.query.organizations.findFirst({ where: eq(organizations.id, testData.organization.id), }); - expect(Number(org?.credit_balance)).toBe(testData.organization.creditBalance + amount); + expect(Number(org?.credit_balance)).toBe( + testData.organization.creditBalance + amount, + ); // Cleanup await cleanupTestData(connectionString, testData.organization.id); @@ -207,7 +216,9 @@ describe("CreditsService", () => { const org = await dbRead.query.organizations.findFirst({ where: eq(organizations.id, testData.organization.id), }); - expect(Number(org?.credit_balance)).toBe(testData.organization.creditBalance); + expect(Number(org?.credit_balance)).toBe( + testData.organization.creditBalance, + ); // Cleanup await cleanupTestData(connectionString, testData.organization.id); @@ -301,8 +312,12 @@ describe("CreditsService", () => { const results = await Promise.allSettled(promises); // Assert: Count successes and failures - const successes = results.filter((r) => r.status === "fulfilled" && r.value.success); - const failures = results.filter((r) => r.status === "fulfilled" && !r.value.success); + const successes = results.filter( + (r) => r.status === "fulfilled" && r.value.success, + ); + const failures = results.filter( + (r) => r.status === "fulfilled" && !r.value.success, + ); // Exactly 10 should succeed (we have $10, each deduction is $1) expect(successes.length).toBe(10); @@ -317,7 +332,10 @@ describe("CreditsService", () => { // Verify transaction count const transactions = await dbRead.query.creditTransactions.findMany({ - where: eq(creditTransactions.organization_id, raceTestData.organization.id), + where: eq( + creditTransactions.organization_id, + raceTestData.organization.id, + ), }); const debitTransactions = transactions.filter((t) => t.type === "debit"); expect(debitTransactions.length).toBe(10); @@ -370,7 +388,9 @@ describe("CreditsService", () => { // Assert expect(result.transaction.type).toBe("refund"); - expect(result.transaction.description).toBe("Refund for failed operation"); + expect(result.transaction.description).toBe( + "Refund for failed operation", + ); expect(result.transaction.metadata).toEqual({ reason: "operation_failed", }); @@ -412,7 +432,8 @@ describe("CreditsService", () => { description: "Reserve for test", }); - const balanceAfterReservation = testData.organization.creditBalance - reserved; + const balanceAfterReservation = + testData.organization.creditBalance - reserved; // Act await creditsService.reconcile({ @@ -426,7 +447,9 @@ describe("CreditsService", () => { const org = await dbRead.query.organizations.findFirst({ where: eq(organizations.id, testData.organization.id), }); - expect(Number(org?.credit_balance)).toBe(balanceAfterReservation + expectedRefund); + expect(Number(org?.credit_balance)).toBe( + balanceAfterReservation + expectedRefund, + ); // Cleanup await cleanupTestData(connectionString, testData.organization.id); @@ -445,7 +468,8 @@ describe("CreditsService", () => { description: "Reserve for test", }); - const balanceAfterReservation = testData.organization.creditBalance - reserved; + const balanceAfterReservation = + testData.organization.creditBalance - reserved; // Act await creditsService.reconcile({ @@ -459,7 +483,9 @@ describe("CreditsService", () => { const org = await dbRead.query.organizations.findFirst({ where: eq(organizations.id, testData.organization.id), }); - expect(Number(org?.credit_balance)).toBe(balanceAfterReservation - expectedOverage); + expect(Number(org?.credit_balance)).toBe( + balanceAfterReservation - expectedOverage, + ); // Cleanup await cleanupTestData(connectionString, testData.organization.id); @@ -477,7 +503,8 @@ describe("CreditsService", () => { description: "Reserve for test", }); - const balanceAfterReservation = testData.organization.creditBalance - reserved; + const balanceAfterReservation = + testData.organization.creditBalance - reserved; // Act await creditsService.reconcile({ diff --git a/packages/tests/integration/services/organizations.service.test.ts b/packages/tests/integration/services/organizations.service.test.ts index ba99aeb9b..471bde4fa 100644 --- a/packages/tests/integration/services/organizations.service.test.ts +++ b/packages/tests/integration/services/organizations.service.test.ts @@ -14,7 +14,14 @@ * */ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { v4 as uuidv4 } from "uuid"; import { organizationsService } from "@/lib/services/organizations"; import { getConnectionString } from "@/tests/helpers/local-database"; @@ -155,7 +162,9 @@ describe("OrganizationsService", () => { // The user created in testData should be associated expect(result!.users).toBeDefined(); expect(result!.users.length).toBeGreaterThanOrEqual(1); - expect(result!.users.map((u) => (u as { id: string }).id)).toContain(testData.user.id); + expect(result!.users.map((u) => (u as { id: string }).id)).toContain( + testData.user.id, + ); // Cleanup await cleanupTestData(connectionString, testData.organization.id); @@ -308,7 +317,10 @@ describe("OrganizationsService", () => { const amountToAdd = 50; // Act - const result = await organizationsService.updateCreditBalance(orgId, amountToAdd); + const result = await organizationsService.updateCreditBalance( + orgId, + amountToAdd, + ); // Assert expect(result.success).toBe(true); @@ -329,7 +341,10 @@ describe("OrganizationsService", () => { const amountToDeduct = -30; // Act - const result = await organizationsService.updateCreditBalance(orgId, amountToDeduct); + const result = await organizationsService.updateCreditBalance( + orgId, + amountToDeduct, + ); // Assert expect(result.success).toBe(true); @@ -345,7 +360,10 @@ describe("OrganizationsService", () => { const largeAmount = 500000; // Act - const result = await organizationsService.updateCreditBalance(orgId, largeAmount); + const result = await organizationsService.updateCreditBalance( + orgId, + largeAmount, + ); // Assert expect(result.success).toBe(true); diff --git a/packages/tests/integration/services/redeemable-earnings.service.test.ts b/packages/tests/integration/services/redeemable-earnings.service.test.ts index e51322847..3aeea2abe 100644 --- a/packages/tests/integration/services/redeemable-earnings.service.test.ts +++ b/packages/tests/integration/services/redeemable-earnings.service.test.ts @@ -15,11 +15,21 @@ * @see https://martinfowler.com/bliki/UnitTest.html (Sociable vs Solitary) */ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { eq } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { dbWrite } from "@/db/client"; -import { redeemableEarnings, redeemableEarningsLedger } from "@/db/schemas/redeemable-earnings"; +import { + redeemableEarnings, + redeemableEarningsLedger, +} from "@/db/schemas/redeemable-earnings"; import { redeemableEarningsService } from "@/lib/services/redeemable-earnings"; import { getConnectionString } from "@/tests/helpers/local-database"; import { @@ -55,7 +65,9 @@ describe("RedeemableEarningsService", () => { await dbWrite .delete(redeemableEarningsLedger) .where(eq(redeemableEarningsLedger.user_id, userId)); - await dbWrite.delete(redeemableEarnings).where(eq(redeemableEarnings.user_id, userId)); + await dbWrite + .delete(redeemableEarnings) + .where(eq(redeemableEarnings.user_id, userId)); } // =========================================================================== @@ -262,7 +274,10 @@ describe("RedeemableEarningsService", () => { expect(result.success).toBe(true); // Verify ledger entry - const history = await redeemableEarningsService.getLedgerHistory(userId, 1); + const history = await redeemableEarningsService.getLedgerHistory( + userId, + 1, + ); expect(history.length).toBe(1); expect(history[0].type).toBe("earning"); expect(history[0].amount).toBe(50); @@ -443,11 +458,15 @@ describe("RedeemableEarningsService", () => { // Assert // Successes: fulfilled promises with success: true - const successes = results.filter((r) => r.status === "fulfilled" && r.value.success); + const successes = results.filter( + (r) => r.status === "fulfilled" && r.value.success, + ); // Failures: Either rejected promises (thrown errors) OR fulfilled with success: false const failures = results.filter( - (r) => r.status === "rejected" || (r.status === "fulfilled" && !r.value.success), + (r) => + r.status === "rejected" || + (r.status === "fulfilled" && !r.value.success), ); // Should have exactly 10 successful locks (10 x $1 = $10) @@ -540,7 +559,9 @@ describe("RedeemableEarningsService", () => { // Assert - Check ledger has completion entry const history = await redeemableEarningsService.getLedgerHistory(userId); const completionEntry = history.find( - (e) => e.redemptionId === redemptionId && e.description.includes("completed"), + (e) => + e.redemptionId === redemptionId && + e.description.includes("completed"), ); expect(completionEntry).toBeDefined(); @@ -710,7 +731,10 @@ describe("RedeemableEarningsService", () => { } // Act - const history = await redeemableEarningsService.getLedgerHistory(userId, 3); + const history = await redeemableEarningsService.getLedgerHistory( + userId, + 3, + ); // Assert expect(history.length).toBe(3); @@ -739,7 +763,9 @@ describe("RedeemableEarningsService", () => { }); // Act - const isRedeemed = await redeemableEarningsService.hasBeenRedeemed(result.ledgerEntryId); + const isRedeemed = await redeemableEarningsService.hasBeenRedeemed( + result.ledgerEntryId, + ); // Assert expect(isRedeemed).toBe(false); diff --git a/packages/tests/integration/services/users-join-regression.test.ts b/packages/tests/integration/services/users-join-regression.test.ts index faee81ffd..74b1d7c24 100644 --- a/packages/tests/integration/services/users-join-regression.test.ts +++ b/packages/tests/integration/services/users-join-regression.test.ts @@ -5,7 +5,14 @@ * These assertions intentionally read from primary because they verify the * post-write hydrated shape, not replica propagation timing. */ -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { eq } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { dbWrite } from "@/db/helpers"; diff --git a/packages/tests/integration/services/users.service.test.ts b/packages/tests/integration/services/users.service.test.ts index 80995f74c..a8f610fa8 100644 --- a/packages/tests/integration/services/users.service.test.ts +++ b/packages/tests/integration/services/users.service.test.ts @@ -16,7 +16,14 @@ * */ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { eq } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { dbWrite } from "@/db/helpers"; @@ -295,10 +302,14 @@ describe("UsersService", () => { ]); expect( - [firstResult.status, secondResult.status].filter((status) => status === "fulfilled"), + [firstResult.status, secondResult.status].filter( + (status) => status === "fulfilled", + ), ).toHaveLength(1); expect( - [firstResult.status, secondResult.status].filter((status) => status === "rejected"), + [firstResult.status, secondResult.status].filter( + (status) => status === "rejected", + ), ).toHaveLength(1); const user = await usersService.getByPrivyId(privyId); @@ -618,7 +629,9 @@ describe("UsersService", () => { const fakeUserId = uuidv4(); // Act & Assert - await expect(usersService.delete(fakeUserId)).rejects.toThrow(`User ${fakeUserId} not found`); + await expect(usersService.delete(fakeUserId)).rejects.toThrow( + `User ${fakeUserId} not found`, + ); // Cleanup await cleanupTestData(connectionString, testData.organization.id); diff --git a/packages/tests/integration/telegram-automation.test.ts b/packages/tests/integration/telegram-automation.test.ts index 3f0150f52..5df3556e4 100644 --- a/packages/tests/integration/telegram-automation.test.ts +++ b/packages/tests/integration/telegram-automation.test.ts @@ -4,7 +4,8 @@ import { config } from "dotenv"; const preservedHarnessEnv = { CACHE_ENABLED: process.env.CACHE_ENABLED, DATABASE_URL: process.env.DATABASE_URL, - DISABLE_LOCAL_DOCKER_DB_FALLBACK: process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK, + DISABLE_LOCAL_DOCKER_DB_FALLBACK: + process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NODE_ENV: process.env.NODE_ENV, REDIS_URL: process.env.REDIS_URL, @@ -30,7 +31,9 @@ for (const [key, value] of Object.entries(preservedHarnessEnv)) { console.log("[Test Setup] Environment loaded"); const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const API_KEY = process.env.TEST_API_KEY; const TEST_APP_ID = process.env.TEST_APP_ID || "test_app_id"; @@ -133,32 +136,43 @@ describe("Telegram Disconnect API", () => { describe("App Telegram Automation API", () => { test("GET returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation`); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + ); expect(res.status).toBe(401); }); test("GET returns 404 for non-existent app", async () => { if (!apiKeyValid) return; - const res = await fetchWithAuth(`/api/v1/apps/non-existent-app/telegram-automation`); + const res = await fetchWithAuth( + `/api/v1/apps/non-existent-app/telegram-automation`, + ); expect(res.status).toBe(404); const data = await res.json(); expect(data).toHaveProperty("error", "App not found"); }); test("POST returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ enabled: true }), - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: true }), + }, + ); expect(res.status).toBe(401); }); test("POST validates announceIntervalMin must be >= 30", async () => { if (!apiKeyValid) return; - const res = await fetchWithAuth(`/api/v1/apps/${TEST_APP_ID}/telegram-automation`, "POST", { - announceIntervalMin: 10, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + "POST", + { + announceIntervalMin: 10, + }, + ); // Either 400 (validation) or 404 (app not found) expect([400, 404]).toContain(res.status); if (res.status === 400) { @@ -169,9 +183,13 @@ describe("App Telegram Automation API", () => { test("POST validates announceIntervalMax must be <= 1440", async () => { if (!apiKeyValid) return; - const res = await fetchWithAuth(`/api/v1/apps/${TEST_APP_ID}/telegram-automation`, "POST", { - announceIntervalMax: 2000, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + "POST", + { + announceIntervalMax: 2000, + }, + ); expect([400, 404]).toContain(res.status); if (res.status === 400) { const data = await res.json(); @@ -180,20 +198,26 @@ describe("App Telegram Automation API", () => { }); test("DELETE returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation`, { - method: "DELETE", - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + { + method: "DELETE", + }, + ); expect(res.status).toBe(401); }); }); describe("App Telegram Post API", () => { test("POST returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation/post`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: "Test message" }), - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${TEST_APP_ID}/telegram-automation/post`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Test message" }), + }, + ); expect(res.status).toBe(401); }); @@ -227,11 +251,14 @@ describe("App Telegram Post API", () => { describe("Telegram Webhook API", () => { test("returns 404 or 401 for unknown organization", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/telegram/webhook/unknown-org-id`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ update_id: 12345 }), - }); + const res = await fetch( + `${SERVER_URL}/api/v1/telegram/webhook/unknown-org-id`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ update_id: 12345 }), + }, + ); // Webhook may return auth failure, not found, or server error for unknown org expect([401, 404, 500]).toContain(res.status); }); @@ -259,9 +286,13 @@ describe("Telegram API Input Validation", () => { test("app automation validates welcomeMessage max length", async () => { if (!apiKeyValid) return; const longMessage = "a".repeat(501); - const res = await fetchWithAuth(`/api/v1/apps/${TEST_APP_ID}/telegram-automation`, "POST", { - welcomeMessage: longMessage, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + "POST", + { + welcomeMessage: longMessage, + }, + ); expect([400, 404]).toContain(res.status); if (res.status === 400) { const data = await res.json(); @@ -272,9 +303,13 @@ describe("Telegram API Input Validation", () => { test("app automation validates vibeStyle max length", async () => { if (!apiKeyValid) return; const longStyle = "a".repeat(101); - const res = await fetchWithAuth(`/api/v1/apps/${TEST_APP_ID}/telegram-automation`, "POST", { - vibeStyle: longStyle, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + "POST", + { + vibeStyle: longStyle, + }, + ); expect([400, 404]).toContain(res.status); if (res.status === 400) { const data = await res.json(); @@ -284,10 +319,14 @@ describe("Telegram API Input Validation", () => { test("app automation rejects min > max interval", async () => { if (!apiKeyValid) return; - const res = await fetchWithAuth(`/api/v1/apps/${TEST_APP_ID}/telegram-automation`, "POST", { - announceIntervalMin: 200, - announceIntervalMax: 100, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${TEST_APP_ID}/telegram-automation`, + "POST", + { + announceIntervalMin: 200, + announceIntervalMax: 100, + }, + ); expect([400, 404]).toContain(res.status); if (res.status === 400) { const data = await res.json(); diff --git a/packages/tests/integration/twitter-automation.test.ts b/packages/tests/integration/twitter-automation.test.ts index ca155d85d..5cc2841b1 100644 --- a/packages/tests/integration/twitter-automation.test.ts +++ b/packages/tests/integration/twitter-automation.test.ts @@ -16,7 +16,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; const SERVER_URL = - process.env.TEST_BASE_URL || process.env.TEST_SERVER_URL || "http://localhost:3000"; + process.env.TEST_BASE_URL || + process.env.TEST_SERVER_URL || + "http://localhost:3000"; const API_KEY = process.env.TEST_API_KEY; const TIMEOUT = 15000; @@ -52,7 +54,9 @@ beforeAll(async () => { apiKeyValid = res.status !== 401; if (!apiKeyValid) { - console.log("[Twitter Tests] TEST_API_KEY is invalid - auth tests will skip"); + console.log( + "[Twitter Tests] TEST_API_KEY is invalid - auth tests will skip", + ); } else { console.log("[Twitter Tests] TEST_API_KEY is valid"); } @@ -145,7 +149,9 @@ describe("App Twitter Automation API", () => { const FAKE_APP_ID = "00000000-0000-0000-0000-000000000000"; test("GET returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation`); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation`, + ); expect(res.status).toBe(401); }); @@ -155,17 +161,22 @@ describe("App Twitter Automation API", () => { return; } - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/twitter-automation`); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/twitter-automation`, + ); // App not found should return 500 (throws error internally) expect(res.status).toBe(500); }); test("POST returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ enabled: true }), - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: true }), + }, + ); expect(res.status).toBe(401); }); @@ -204,9 +215,12 @@ describe("App Twitter Automation API", () => { }); test("DELETE returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation`, { - method: "DELETE", - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation`, + { + method: "DELETE", + }, + ); expect(res.status).toBe(401); }); }); @@ -215,11 +229,14 @@ describe("App Twitter Post API", () => { const FAKE_APP_ID = "00000000-0000-0000-0000-000000000000"; test("POST returns 401 without auth", async () => { - const res = await fetch(`${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation/post`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); + const res = await fetch( + `${SERVER_URL}/api/v1/apps/${FAKE_APP_ID}/twitter-automation/post`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }, + ); expect(res.status).toBe(401); }); @@ -230,9 +247,13 @@ describe("App Twitter Post API", () => { } const longText = "a".repeat(281); - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/twitter-automation/post`, "POST", { - text: longText, - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/twitter-automation/post`, + "POST", + { + text: longText, + }, + ); expect(res.status).toBe(400); const data = await res.json(); @@ -245,9 +266,13 @@ describe("App Twitter Post API", () => { return; } - const res = await fetchWithAuth(`/api/v1/apps/${FAKE_APP_ID}/twitter-automation/post`, "POST", { - text: "Test tweet", - }); + const res = await fetchWithAuth( + `/api/v1/apps/${FAKE_APP_ID}/twitter-automation/post`, + "POST", + { + text: "Test tweet", + }, + ); expect(res.status).toBe(404); const data = await res.json(); diff --git a/packages/tests/integration/unified-oauth-api.test.ts b/packages/tests/integration/unified-oauth-api.test.ts index cab50186d..18152bc99 100644 --- a/packages/tests/integration/unified-oauth-api.test.ts +++ b/packages/tests/integration/unified-oauth-api.test.ts @@ -41,7 +41,8 @@ const TIMEOUT = 60000; const encryptionService = createEncryptionService(); let secretsClient: Client | null = null; -const it = (name: string, fn: () => void | Promise) => bunIt(name, fn, TIMEOUT); +const it = (name: string, fn: () => void | Promise) => + bunIt(name, fn, TIMEOUT); describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { let testData: TestDataSet; @@ -64,9 +65,10 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { afterAll(async () => { // Clean up platform credentials and secrets - await client.query(`DELETE FROM platform_credentials WHERE organization_id = $1`, [ - testData.organization.id, - ]); + await client.query( + `DELETE FROM platform_credentials WHERE organization_id = $1`, + [testData.organization.id], + ); await client.query(`DELETE FROM secrets WHERE organization_id = $1`, [ testData.organization.id, ]); @@ -118,7 +120,9 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const response = await fetch(`${BASE_URL}/api/v1/oauth/providers`); const data = await response.json(); - const googleProvider = data.providers.find((p: { id: string }) => p.id === "google"); + const googleProvider = data.providers.find( + (p: { id: string }) => p.id === "google", + ); // Google should be configured if GOOGLE_CLIENT_ID is set if (process.env.GOOGLE_CLIENT_ID) { @@ -138,7 +142,9 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { }; for (const [id, expectedType] of Object.entries(providerTypes)) { - const provider = data.providers.find((p: { id: string }) => p.id === id); + const provider = data.providers.find( + (p: { id: string }) => p.id === id, + ); expect(provider).toBeDefined(); expect(provider.type).toBe(expectedType); } @@ -148,8 +154,12 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const response = await fetch(`${BASE_URL}/api/v1/oauth/providers`); const data = await response.json(); - const twilioProvider = data.providers.find((p: { id: string }) => p.id === "twilio"); - const blooioProvider = data.providers.find((p: { id: string }) => p.id === "blooio"); + const twilioProvider = data.providers.find( + (p: { id: string }) => p.id === "twilio", + ); + const blooioProvider = data.providers.find( + (p: { id: string }) => p.id === "blooio", + ); // API key providers are always "configured" since users provide their own credentials expect(twilioProvider.configured).toBe(true); @@ -160,7 +170,9 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const response = await fetch(`${BASE_URL}/api/v1/oauth/providers`); const data = await response.json(); - const googleProvider = data.providers.find((p: { id: string }) => p.id === "google"); + const googleProvider = data.providers.find( + (p: { id: string }) => p.id === "google", + ); expect(googleProvider).toHaveProperty("defaultScopes"); expect(Array.isArray(googleProvider.defaultScopes)).toBe(true); expect(googleProvider.defaultScopes.length).toBeGreaterThan(0); @@ -395,12 +407,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { }); it("should filter by platform when specified", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -438,24 +453,31 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); expect(data.connections.length).toBeGreaterThan(0); - const googleConn = data.connections.find((c: { id: string }) => c.id === credId); + const googleConn = data.connections.find( + (c: { id: string }) => c.id === credId, + ); expect(googleConn).toBeDefined(); expect(googleConn.platform).toBe("google"); expect(googleConn.source).toBe("platform_credentials"); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should return connections from secrets (Twitter)", async () => { @@ -479,12 +501,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { "testuser", ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=twitter`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=twitter`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -518,12 +543,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { "test_auth_token", ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=twilio`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=twilio`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -550,12 +578,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { "test_blooio_key", ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=blooio`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=blooio`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -593,12 +624,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId2, testData.organization.id, now], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -608,10 +642,10 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const firstConn = data.connections[0]; expect(firstConn.platformUserId).toBe("user-recent"); - await client.query(`DELETE FROM platform_credentials WHERE id IN ($1, $2)`, [ - credId1, - credId2, - ]); + await client.query( + `DELETE FROM platform_credentials WHERE id IN ($1, $2)`, + [credId1, credId2], + ); }); it("should return connections with all expected fields", async () => { @@ -627,12 +661,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -651,7 +688,9 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { expect(conn.lastUsedAt).toBeDefined(); expect(conn.source).toBe("platform_credentials"); - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should include tokenExpired field correctly", async () => { @@ -666,18 +705,23 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, testData.organization.id, expiredTime], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); expect(data.connection.tokenExpired).toBe(true); - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); }); @@ -686,20 +730,26 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { // ============================================================================ describe("GET /api/v1/oauth/connections/:id", () => { it("should return 401 without authentication", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/some-id`, { - signal: AbortSignal.timeout(TIMEOUT), - }); + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/some-id`, + { + signal: AbortSignal.timeout(TIMEOUT), + }, + ); expect(response.status).toBe(401); }); it("should return 404 for non-existent connection", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${crypto.randomUUID()}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${crypto.randomUUID()}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); const data = await response.json(); @@ -707,12 +757,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { }); it("should return 404 for malformed UUID connection ID", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/not-a-valid-uuid`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/not-a-valid-uuid`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); }); @@ -739,12 +792,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -754,7 +810,9 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { expect(data.connection.email).toBe("test2@gmail.com"); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should return connection for secrets-based adapter (Twitter)", async () => { @@ -772,12 +830,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { ); const connectionId = `twitter:${testData.organization.id}`; - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${connectionId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${connectionId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -807,17 +868,22 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { ); // Try to access with our API key - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); await cleanupTestData(TEST_DB_URL, otherOrg.organization.id); }); }); @@ -827,22 +893,28 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { // ============================================================================ describe("DELETE /api/v1/oauth/connections/:id", () => { it("should return 401 without authentication", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/some-id`, { - method: "DELETE", - signal: AbortSignal.timeout(TIMEOUT), - }); + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/some-id`, + { + method: "DELETE", + signal: AbortSignal.timeout(TIMEOUT), + }, + ); expect(response.status).toBe(401); }); it("should return 404 for non-existent connection", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${crypto.randomUUID()}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${crypto.randomUUID()}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); }); @@ -857,13 +929,16 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); const data = await response.json(); @@ -878,7 +953,9 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { expect(result.rows[0].revoked_at).toBeDefined(); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should delete Twitter secrets when revoking", async () => { @@ -897,13 +974,16 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { ); const connectionId = `twitter:${testData.organization.id}`; - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${connectionId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${connectionId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); @@ -936,13 +1016,16 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { ); const connectionId = `twilio:${testData.organization.id}`; - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${connectionId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${connectionId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); @@ -968,13 +1051,16 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { ); const connectionId = `blooio:${testData.organization.id}`; - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${connectionId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${connectionId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(200); @@ -999,24 +1085,30 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, otherOrg.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}`, { - method: "DELETE", - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}`, + { + method: "DELETE", + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); // Verify connection was not deleted - const result = await client.query(`SELECT status FROM platform_credentials WHERE id = $1`, [ - credId, - ]); + const result = await client.query( + `SELECT status FROM platform_credentials WHERE id = $1`, + [credId], + ); expect(result.rows[0].status).toBe("active"); // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); await cleanupTestData(TEST_DB_URL, otherOrg.organization.id); }); }); @@ -1026,9 +1118,12 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { // ============================================================================ describe("GET /api/v1/oauth/connections/:id/token", () => { it("should return 401 without authentication", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/some-id/token`, { - signal: AbortSignal.timeout(TIMEOUT), - }); + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/some-id/token`, + { + signal: AbortSignal.timeout(TIMEOUT), + }, + ); expect(response.status).toBe(401); }); @@ -1063,12 +1158,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { "oauth1_access_secret", ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${connectionId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${connectionId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); @@ -1087,26 +1185,34 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { [credId, testData.organization.id], ); - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${credId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${credId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); - await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [credId]); + await client.query(`DELETE FROM platform_credentials WHERE id = $1`, [ + credId, + ]); }); it("should return 404 for malformed connection IDs", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/not-a-valid-uuid/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/not-a-valid-uuid/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); @@ -1138,12 +1244,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { }); it("should return 404 for unsupported platforms", async () => { - const response = await fetch(`${BASE_URL}/api/v1/oauth/token/unsupported`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/token/unsupported`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); @@ -1168,12 +1277,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { describe("Error Handling", () => { it("should return removed-route payload for authenticated token requests", async () => { const connectionId = `twitter:${testData.organization.id}`; - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${connectionId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${connectionId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); @@ -1227,12 +1339,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const invalidIds = ["invalid", "12345", "null", "undefined"]; for (const invalidId of invalidIds) { - const response = await fetch(`${BASE_URL}/api/v1/oauth/connections/${invalidId}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + const response = await fetch( + `${BASE_URL}/api/v1/oauth/connections/${invalidId}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); @@ -1248,12 +1363,15 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const requests = Array(5) .fill(null) .map(() => - fetch(`${BASE_URL}/api/v1/oauth/connections/${crypto.randomUUID()}/token`, { - headers: { - "X-API-Key": testData.apiKey.key, + fetch( + `${BASE_URL}/api/v1/oauth/connections/${crypto.randomUUID()}/token`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }), + ), ); const results = await Promise.all(requests); @@ -1305,25 +1423,32 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { } // List connections should return all statuses - const listResponse = await fetch(`${BASE_URL}/api/v1/oauth/connections?platform=google`, { - headers: { - "X-API-Key": testData.apiKey.key, + const listResponse = await fetch( + `${BASE_URL}/api/v1/oauth/connections?platform=google`, + { + headers: { + "X-API-Key": testData.apiKey.key, + }, + signal: AbortSignal.timeout(TIMEOUT), }, - signal: AbortSignal.timeout(TIMEOUT), - }); + ); expect(listResponse.status).toBe(200); const listData = await listResponse.json(); for (const status of statuses) { const conn = listData.connections.find( - (c: { platformUserId: string }) => c.platformUserId === `user-${status}`, + (c: { platformUserId: string }) => + c.platformUserId === `user-${status}`, ); expect(conn).toBeDefined(); expect(conn.status).toBe(status); } // Cleanup - await client.query(`DELETE FROM platform_credentials WHERE id = ANY($1)`, [credIds]); + await client.query( + `DELETE FROM platform_credentials WHERE id = ANY($1)`, + [credIds], + ); }); it("should keep token-by-platform route disabled even with active connections", async () => { @@ -1353,10 +1478,10 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: "Not Found" }); - await client.query(`DELETE FROM platform_credentials WHERE id IN ($1, $2)`, [ - activeCredId, - revokedCredId, - ]); + await client.query( + `DELETE FROM platform_credentials WHERE id IN ($1, $2)`, + [activeCredId, revokedCredId], + ); }); }); @@ -1419,7 +1544,10 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { const response = await fetch(`${BASE_URL}${path}`, { method, headers: { "Content-Type": "application/json" }, - body: method === "POST" ? JSON.stringify({ platform: "google" }) : undefined, + body: + method === "POST" + ? JSON.stringify({ platform: "google" }) + : undefined, signal: AbortSignal.timeout(TIMEOUT), }); @@ -1428,7 +1556,12 @@ describe.skipIf(!TEST_DB_URL)("Unified OAuth API E2E Tests", () => { }); it("should not allow invalid API keys", async () => { - const invalidKeys = ["invalid_key", "ek_test_invalid", "", "Bearer invalid"]; + const invalidKeys = [ + "invalid_key", + "ek_test_invalid", + "", + "Bearer invalid", + ]; for (const key of invalidKeys) { const response = await fetch(`${BASE_URL}/api/v1/oauth/connections`, { @@ -1463,10 +1596,10 @@ async function createTestSecret( const encrypted = await encryptionService.encrypt(value); - await secretsClient.query(`DELETE FROM secrets WHERE organization_id = $1 AND name = $2`, [ - organizationId, - name, - ]); + await secretsClient.query( + `DELETE FROM secrets WHERE organization_id = $1 AND name = $2`, + [organizationId, name], + ); await secretsClient.query( `INSERT INTO secrets diff --git a/packages/tests/integration/webhooks.integration.test.ts b/packages/tests/integration/webhooks.integration.test.ts index d46a26a55..d0dc76014 100644 --- a/packages/tests/integration/webhooks.integration.test.ts +++ b/packages/tests/integration/webhooks.integration.test.ts @@ -20,11 +20,21 @@ const TEST_DB_URL = process.env.DATABASE_URL || ""; const BASE_URL = process.env.TEST_BASE_URL || "http://localhost:3000"; const TWILIO_AUTH_TOKEN = "test_token"; const BLOOIO_WEBHOOK_SECRET = "webhook_secret_123"; -const TWILIO_SECRET_NAMES = ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_PHONE_NUMBER"]; -const BLOOIO_SECRET_NAMES = ["BLOOIO_API_KEY", "BLOOIO_WEBHOOK_SECRET", "BLOOIO_FROM_NUMBER"]; +const TWILIO_SECRET_NAMES = [ + "TWILIO_ACCOUNT_SID", + "TWILIO_AUTH_TOKEN", + "TWILIO_PHONE_NUMBER", +]; +const BLOOIO_SECRET_NAMES = [ + "BLOOIO_API_KEY", + "BLOOIO_WEBHOOK_SECRET", + "BLOOIO_FROM_NUMBER", +]; const WEBHOOK_RATE_LIMIT_IP = "198.51.100.250"; const encryptionService = createEncryptionService(); -const forwardOriginalFetchPreconnect: NonNullable = (...args) => { +const forwardOriginalFetchPreconnect: NonNullable< + typeof originalFetch.preconnect +> = (...args) => { if (typeof originalFetch.preconnect === "function") { originalFetch.preconnect(...args); } @@ -36,10 +46,10 @@ async function deleteSecrets( organizationId: string, names: string[], ): Promise { - await client.query(`DELETE FROM secrets WHERE organization_id = $1 AND name = ANY($2::text[])`, [ - organizationId, - names, - ]); + await client.query( + `DELETE FROM secrets WHERE organization_id = $1 AND name = ANY($2::text[])`, + [organizationId, names], + ); } async function upsertSecret( @@ -51,10 +61,10 @@ async function upsertSecret( ): Promise { const encrypted = await encryptionService.encrypt(value); - await client.query(`DELETE FROM secrets WHERE organization_id = $1 AND name = $2`, [ - organizationId, - name, - ]); + await client.query( + `DELETE FROM secrets WHERE organization_id = $1 AND name = $2`, + [organizationId, name], + ); await client.query( `INSERT INTO secrets @@ -88,7 +98,10 @@ function getRequestUrl(input: RequestInfo | URL): string { return input.url; } -function createTwilioSignature(url: string, body: URLSearchParams | string): string { +function createTwilioSignature( + url: string, + body: URLSearchParams | string, +): string { const params = typeof body === "string" ? new URLSearchParams(body) : body; const sortedParams = Array.from(params.entries()) .sort(([left], [right]) => left.localeCompare(right)) @@ -138,11 +151,14 @@ const signedWebhookFetch: typeof fetch = Object.assign( init?.headers ?? (input instanceof Request ? input.headers : undefined), ); const isWebhookRequest = - url.includes("/api/webhooks/twilio/") || url.includes("/api/webhooks/blooio/"); + url.includes("/api/webhooks/twilio/") || + url.includes("/api/webhooks/blooio/"); if (isWebhookRequest) { const fixedIp = - headers.get("X-Test-Rate-Limit-IP") === "true" ? WEBHOOK_RATE_LIMIT_IP : undefined; + headers.get("X-Test-Rate-Limit-IP") === "true" + ? WEBHOOK_RATE_LIMIT_IP + : undefined; headers.delete("X-Test-Rate-Limit-IP"); withWebhookIpHeaders(headers, fixedIp); } @@ -152,14 +168,20 @@ const signedWebhookFetch: typeof fetch = Object.assign( return originalFetch(input, { ...init, headers }); } - if (url.includes("/api/webhooks/twilio/") && !headers.has("X-Twilio-Signature")) { + if ( + url.includes("/api/webhooks/twilio/") && + !headers.has("X-Twilio-Signature") + ) { const body = init?.body; if (body instanceof URLSearchParams || typeof body === "string") { headers.set("X-Twilio-Signature", createTwilioSignature(url, body)); } } - if (url.includes("/api/webhooks/blooio/") && !headers.has("X-Blooio-Signature")) { + if ( + url.includes("/api/webhooks/blooio/") && + !headers.has("X-Blooio-Signature") + ) { const body = init?.body; if (typeof body === "string") { headers.set( @@ -197,10 +219,10 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { // Create test agent agentId = uuidv4(); - await client.query(`INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, [ - agentId, - "Webhook Test Agent", - ]); + await client.query( + `INSERT INTO agents (id, name, enabled) VALUES ($1, $2, true)`, + [agentId, "Webhook Test Agent"], + ); // Register phone number for agent const phoneResult = await client.query( @@ -257,8 +279,13 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { return; } - await client.query(`DELETE FROM phone_message_log WHERE phone_number_id = $1`, [phoneNumberId]); - await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [phoneNumberId]); + await client.query( + `DELETE FROM phone_message_log WHERE phone_number_id = $1`, + [phoneNumberId], + ); + await client.query(`DELETE FROM agent_phone_numbers WHERE id = $1`, [ + phoneNumberId, + ]); await client.query(`DELETE FROM agents WHERE id = $1`, [agentId]); await deleteSecrets(client, testData.organization.id, TWILIO_SECRET_NAMES); await deleteSecrets(client, testData.organization.id, BLOOIO_SECRET_NAMES); @@ -354,13 +381,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("To", "+15551234567"); formData.append("Body", "Test"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/invalid-org-id`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/invalid-org-id`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); // May return 400 or 500 depending on validation expect([400, 500]).toContain(response.status); @@ -448,7 +478,9 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { text: "Check this out!", timestamp: Date.now(), protocol: "imessage", - attachments: [{ url: "https://example.com/image.jpg", name: "image.jpg" }], + attachments: [ + { url: "https://example.com/image.jpg", name: "image.jpg" }, + ], }; const response = await fetch( @@ -622,14 +654,17 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("NumMedia", "0"); // Send with invalid signature - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Twilio-Signature": "invalid_signature", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Twilio-Signature": "invalid_signature", + }, + body: formData, }, - body: formData, - }); + ); expect(response.status).toBe(401); }); @@ -644,14 +679,17 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { }; // Send with invalid signature - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Blooio-Signature": "invalid_signature", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Blooio-Signature": "invalid_signature", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(401); }); @@ -666,13 +704,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("To", "+15551234567"); formData.append("Body", "Test"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${nonExistentOrgId}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${nonExistentOrgId}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); // Should handle gracefully, not crash expect([200, 400, 404, 500]).toContain(response.status); @@ -690,13 +731,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { protocol: "imessage", }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); // Should not return 5xx for valid requests expect(response.status).toBeLessThan(500); @@ -743,13 +787,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("NumMedia", "0"); const startTime = Date.now(); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); const duration = Date.now() - startTime; expect(response.status).toBe(200); @@ -765,13 +812,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("To", "+15551234567"); formData.append("Body", "No MessageSid"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); // Should handle gracefully expect([200, 400]).toContain(response.status); @@ -786,13 +836,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("AccountSid", "ACtest123"); formData.append("NumMedia", "0"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); expect(response.status).toBe(200); }); @@ -806,13 +859,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("AccountSid", "ACtest123"); formData.append("NumMedia", "0"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); expect(response.status).toBe(200); }); @@ -832,13 +888,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("MediaContentType1", "image/jpeg"); formData.append("MediaContentType2", "image/jpeg"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); expect(response.status).toBe(200); }); @@ -851,13 +910,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("From", "+15559876543"); formData.append("AccountSid", "ACtest123"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData, }, - body: formData, - }); + ); expect([200, 400]).toContain(response.status); }); @@ -873,13 +935,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { protocol: "imessage", }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); // Should handle gracefully expect([200, 400]).toContain(response.status); @@ -892,13 +957,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { timestamp: Date.now(), }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(200); }); @@ -911,16 +979,21 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { text: "", timestamp: Date.now(), protocol: "imessage", - attachments: [{ url: "https://example.com/document.pdf", name: "document.pdf" }], + attachments: [ + { url: "https://example.com/document.pdf", name: "document.pdf" }, + ], }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(200); }); @@ -937,13 +1010,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { participants: ["+15559876543", "+15551111111"], }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(200); }); @@ -955,13 +1031,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { timestamp: Date.now(), }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(200); }); @@ -976,13 +1055,16 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { protocol: "imessage", }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(200); }); @@ -998,14 +1080,17 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("AccountSid", "ACtest123"); formData.append("NumMedia", "0"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Test-Skip-Signature": "true", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Test-Skip-Signature": "true", + }, + body: formData, }, - body: formData, - }); + ); expect(response.status).toBe(401); }); @@ -1019,14 +1104,17 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData.append("AccountSid", "ACtest123"); formData.append("NumMedia", "0"); - const response = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Twilio-Signature": "", + const response = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Twilio-Signature": "", + }, + body: formData, }, - body: formData, - }); + ); expect(response.status).toBe(401); }); @@ -1040,15 +1128,18 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { timestamp: Date.now(), }; - const response = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Blooio-Signature": "not-a-valid-signature", - "X-Blooio-Timestamp": Math.floor(Date.now() / 1000).toString(), + const response = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Blooio-Signature": "not-a-valid-signature", + "X-Blooio-Timestamp": Math.floor(Date.now() / 1000).toString(), + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); expect(response.status).toBe(401); }); @@ -1107,21 +1198,27 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { formData2.append("AccountSid", "ACtest123"); formData2.append("NumMedia", "0"); - const response1 = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response1 = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData1, }, - body: formData1, - }); + ); - const response2 = await fetch(`${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response2 = await fetch( + `${BASE_URL}/api/webhooks/twilio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData2, }, - body: formData2, - }); + ); // Both should succeed (webhook should be idempotent) expect(response1.status).toBe(200); @@ -1147,21 +1244,27 @@ describe.skipIf(!TEST_DB_URL)("Webhook Handlers E2E Tests", () => { timestamp: Date.now(), }; - const response1 = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response1 = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload1), }, - body: JSON.stringify(payload1), - }); + ); - const response2 = await fetch(`${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response2 = await fetch( + `${BASE_URL}/api/webhooks/blooio/${testData.organization.id}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload2), }, - body: JSON.stringify(payload2), - }); + ); // Both should succeed (webhook should be idempotent) expect(response1.status).toBe(200); diff --git a/packages/tests/integration/whatsapp-webhook-e2e.test.ts b/packages/tests/integration/whatsapp-webhook-e2e.test.ts index 1de3c6913..227ee31ee 100644 --- a/packages/tests/integration/whatsapp-webhook-e2e.test.ts +++ b/packages/tests/integration/whatsapp-webhook-e2e.test.ts @@ -16,11 +16,15 @@ import { describe, expect, it } from "bun:test"; import * as crypto from "crypto"; const BASE_URL = process.env.TEST_BASE_URL || "http://localhost:3000"; -const APP_SECRET = process.env.ELIZA_APP_WHATSAPP_APP_SECRET || "test_app_secret"; -const VERIFY_TOKEN = process.env.ELIZA_APP_WHATSAPP_VERIFY_TOKEN || "test_verify_token"; +const APP_SECRET = + process.env.ELIZA_APP_WHATSAPP_APP_SECRET || "test_app_secret"; +const VERIFY_TOKEN = + process.env.ELIZA_APP_WHATSAPP_VERIFY_TOKEN || "test_verify_token"; function makeSignature(body: string, secret: string): string { - return "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex"); + return ( + "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex") + ); } describe("WhatsApp Webhook E2E Tests", () => { diff --git a/packages/tests/load-env.ts b/packages/tests/load-env.ts index 8c72835d9..6ad96158a 100644 --- a/packages/tests/load-env.ts +++ b/packages/tests/load-env.ts @@ -4,7 +4,10 @@ */ import { config } from "dotenv"; import { resolve } from "path"; -import { applyDatabaseUrlFallback, getLocalDockerDatabaseUrl } from "@/db/database-url"; +import { + applyDatabaseUrlFallback, + getLocalDockerDatabaseUrl, +} from "@/db/database-url"; const root = resolve(import.meta.dir, ".."); const workspaceRoot = resolve(root, ".."); @@ -30,7 +33,8 @@ if (process.env.SKIP_DB_DEPENDENT === "1") { delete process.env.TEST_DATABASE_URL; } else { const shouldPreferLocalDockerDb = - process.env.CI !== "true" && process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK !== "1"; + process.env.CI !== "true" && + process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK !== "1"; const localDockerDatabaseUrl = getLocalDockerDatabaseUrl({ ...process.env, LOCAL_DOCKER_DB_HOST: process.env.LOCAL_DOCKER_DB_HOST || "localhost", @@ -38,7 +42,9 @@ if (process.env.SKIP_DB_DEPENDENT === "1") { const testDatabaseUrl = process.env.TEST_DATABASE_URL || - (shouldPreferLocalDockerDb ? localDockerDatabaseUrl : process.env.DATABASE_URL); + (shouldPreferLocalDockerDb + ? localDockerDatabaseUrl + : process.env.DATABASE_URL); if (testDatabaseUrl) { process.env.TEST_DATABASE_URL = testDatabaseUrl; diff --git a/packages/tests/playwright/auth-real.spec.ts b/packages/tests/playwright/auth-real.spec.ts index 6e69799ce..bf4066bcd 100644 --- a/packages/tests/playwright/auth-real.spec.ts +++ b/packages/tests/playwright/auth-real.spec.ts @@ -13,7 +13,10 @@ async function isVisible(locator: Locator): Promise { } } -async function waitForFirstVisible(locators: Locator[], timeout: number): Promise { +async function waitForFirstVisible( + locators: Locator[], + timeout: number, +): Promise { await Promise.any( locators.map(async (locator) => { await locator.waitFor({ state: "visible", timeout }); @@ -27,7 +30,9 @@ test.describe("Real Authentication", () => { "Set PLAYWRIGHT_REAL_AUTH=true to run the real auth smoke test.", ); - test("real login redirects to dashboard without auth 401s @auth-real", async ({ page }) => { + test("real login redirects to dashboard without auth 401s @auth-real", async ({ + page, + }) => { test.setTimeout(REAL_AUTH_TIMEOUT_MS); const authFailures: Array<{ url: string; status: number }> = []; @@ -52,7 +57,9 @@ test.describe("Real Authentication", () => { await waitForFirstVisible([configError, loginHeading], 30_000); if (await isVisible(configError)) { - throw new Error("Real auth requires a valid NEXT_PUBLIC_PRIVY_APP_ID in .env.local."); + throw new Error( + "Real auth requires a valid NEXT_PUBLIC_PRIVY_APP_ID in .env.local.", + ); } await expect(loginHeading).toBeVisible(); @@ -71,7 +78,9 @@ test.describe("Real Authentication", () => { timeout: REAL_AUTH_TIMEOUT_MS - 30_000, }); - await expect(page.getByRole("heading", { name: "Infrastructure" })).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Infrastructure" }), + ).toBeVisible(); await expect(page.getByRole("link", { name: "Instances" })).toBeVisible(); await expect(page.getByRole("link", { name: "My Agents" })).toBeVisible(); await expect(page.getByRole("link", { name: "Containers" })).toHaveCount(0); diff --git a/packages/tests/playwright/auth-routing.spec.ts b/packages/tests/playwright/auth-routing.spec.ts index 288d6227e..25c10cbc6 100644 --- a/packages/tests/playwright/auth-routing.spec.ts +++ b/packages/tests/playwright/auth-routing.spec.ts @@ -14,12 +14,16 @@ const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000"; test.describe("Authentication Routing", () => { test.describe("Login Page - returnTo Parameter", () => { - test("login page URL should accept returnTo parameter", async ({ page }) => { + test("login page URL should accept returnTo parameter", async ({ + page, + }) => { await page.goto(`${BASE_URL}/login?returnTo=/dashboard/settings`); expect(page.url()).toContain("returnTo"); }); - test("login page should render without errors when returnTo is provided", async ({ page }) => { + test("login page should render without errors when returnTo is provided", async ({ + page, + }) => { await page.goto(`${BASE_URL}/login?returnTo=/dashboard/settings`); // Check that the page loaded without JavaScript errors const errors: string[] = []; @@ -28,12 +32,16 @@ test.describe("Authentication Routing", () => { // Filter out known non-critical errors const criticalErrors = errors.filter( (e) => - !e.includes("WalletConnect") && !e.includes("hydration") && !e.includes("ResizeObserver"), + !e.includes("WalletConnect") && + !e.includes("hydration") && + !e.includes("ResizeObserver"), ); expect(criticalErrors).toHaveLength(0); }); - test("returnTo parameter should be URL-safe with encoded characters", async ({ page }) => { + test("returnTo parameter should be URL-safe with encoded characters", async ({ + page, + }) => { const complexPath = "/dashboard/chat?characterId=abc-123&mode=test"; const encodedPath = encodeURIComponent(complexPath); await page.goto(`${BASE_URL}/login?returnTo=${encodedPath}`); @@ -42,7 +50,9 @@ test.describe("Authentication Routing", () => { expect(page.url()).toContain("returnTo"); }); - test("should reject potentially unsafe returnTo values", async ({ page }) => { + test("should reject potentially unsafe returnTo values", async ({ + page, + }) => { // Try to inject an external URL await page.goto(`${BASE_URL}/login?returnTo=https://evil.com`); await page.waitForLoadState("networkidle"); @@ -76,14 +86,18 @@ test.describe("Authentication Routing", () => { await page.waitForLoadState("networkidle"); // Store initial history length after dashboard load - const initialHistoryLength = await page.evaluate(() => window.history.length); + const initialHistoryLength = await page.evaluate( + () => window.history.length, + ); // Navigate to login with returnTo await page.goto(`${BASE_URL}/login?returnTo=/dashboard`); await page.waitForLoadState("networkidle"); // History length should only increase by 1 (not create multiple entries) - const finalHistoryLength = await page.evaluate(() => window.history.length); + const finalHistoryLength = await page.evaluate( + () => window.history.length, + ); expect(finalHistoryLength).toBeLessThanOrEqual(initialHistoryLength + 2); }); }); @@ -98,7 +112,9 @@ test.describe("Authentication Routing", () => { ]; for (const maliciousUrl of maliciousUrls) { - await page.goto(`${BASE_URL}/login?returnTo=${encodeURIComponent(maliciousUrl)}`); + await page.goto( + `${BASE_URL}/login?returnTo=${encodeURIComponent(maliciousUrl)}`, + ); await page.waitForLoadState("networkidle"); // Should still be on our domain expect(page.url()).toMatch( @@ -107,7 +123,9 @@ test.describe("Authentication Routing", () => { } }); - test("returnTo should handle special characters safely", async ({ page }) => { + test("returnTo should handle special characters safely", async ({ + page, + }) => { const specialPaths = [ "/dashboard/chat?q=test&foo=bar", "/dashboard/settings#section", @@ -115,7 +133,9 @@ test.describe("Authentication Routing", () => { ]; for (const path of specialPaths) { - await page.goto(`${BASE_URL}/login?returnTo=${encodeURIComponent(path)}`); + await page.goto( + `${BASE_URL}/login?returnTo=${encodeURIComponent(path)}`, + ); await page.waitForLoadState("networkidle"); // Should load without errors expect(page.url()).toContain("login"); @@ -130,7 +150,9 @@ test.describe("Authentication Routing", () => { }); test("login page with returnTo returns 200", async ({ request }) => { - const response = await request.get(`${BASE_URL}/login?returnTo=/dashboard/settings`); + const response = await request.get( + `${BASE_URL}/login?returnTo=/dashboard/settings`, + ); expect(response.status()).toBe(200); }); @@ -147,14 +169,18 @@ test.describe("Authentication Routing", () => { test.describe("Invite Flow Routing", () => { test("invite accept page should be accessible", async ({ request }) => { - const response = await request.get(`${BASE_URL}/invite/accept?token=test-token`); + const response = await request.get( + `${BASE_URL}/invite/accept?token=test-token`, + ); expect(response.status()).toBe(200); }); }); }); test.describe("Console Error Monitoring", () => { - test("login page should not have critical console errors", async ({ page }) => { + test("login page should not have critical console errors", async ({ + page, + }) => { const consoleErrors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { @@ -184,7 +210,9 @@ test.describe("Console Error Monitoring", () => { expect(criticalErrors).toHaveLength(0); }); - test("dashboard should not have critical console errors", async ({ page }) => { + test("dashboard should not have critical console errors", async ({ + page, + }) => { const consoleErrors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { diff --git a/packages/tests/playwright/fixtures/auth.fixture.ts b/packages/tests/playwright/fixtures/auth.fixture.ts index bce670ef5..74870fb6b 100644 --- a/packages/tests/playwright/fixtures/auth.fixture.ts +++ b/packages/tests/playwright/fixtures/auth.fixture.ts @@ -1,10 +1,17 @@ -import { type APIRequestContext, type BrowserContext, expect, test } from "@playwright/test"; +import { + type APIRequestContext, + type BrowserContext, + expect, + test, +} from "@playwright/test"; import { ensureLocalTestAuth } from "../../infrastructure/local-test-auth"; const PLAYWRIGHT_TEST_AUTH_MARKER_COOKIE_NAME = "eliza-test-auth"; function resolveBaseUrl(baseUrl?: string): URL { - return new URL(baseUrl || process.env.TEST_BASE_URL || "http://localhost:3000"); + return new URL( + baseUrl || process.env.TEST_BASE_URL || "http://localhost:3000", + ); } export function hasApiKey(): boolean { diff --git a/packages/tests/playwright/fixtures/page-helpers.ts b/packages/tests/playwright/fixtures/page-helpers.ts index 89b5f346b..c0c4834b8 100644 --- a/packages/tests/playwright/fixtures/page-helpers.ts +++ b/packages/tests/playwright/fixtures/page-helpers.ts @@ -4,10 +4,15 @@ export async function smokeTestPage(page: Page, path: string): Promise { const response = await page.goto(path, { waitUntil: "domcontentloaded" }); expect(response?.status(), `unexpected status for ${path}`).not.toBe(500); await expect(page.locator("html")).toBeAttached(); - await expect.poll(() => page.evaluate(() => document.readyState)).toMatch(/interactive|complete/); + await expect + .poll(() => page.evaluate(() => document.readyState)) + .toMatch(/interactive|complete/); } -export async function strictSmokeTestPage(page: Page, path: string): Promise { +export async function strictSmokeTestPage( + page: Page, + path: string, +): Promise { const pageErrors: Error[] = []; page.on("pageerror", (error) => { pageErrors.push(error); diff --git a/packages/tests/playwright/flows/agent-lifecycle.spec.ts b/packages/tests/playwright/flows/agent-lifecycle.spec.ts index 421597f44..5fed1b93e 100644 --- a/packages/tests/playwright/flows/agent-lifecycle.spec.ts +++ b/packages/tests/playwright/flows/agent-lifecycle.spec.ts @@ -1,8 +1,16 @@ // @ts-nocheck — auth fixture pending implementation import type { Locator, Page } from "@playwright/test"; -import { authenticateBrowserContext, expect, hasApiKey, test } from "../fixtures/auth.fixture"; +import { + authenticateBrowserContext, + expect, + hasApiKey, + test, +} from "../fixtures/auth.fixture"; -async function waitForFirstVisible(locators: Locator[], timeout = 10_000): Promise { +async function waitForFirstVisible( + locators: Locator[], + timeout = 10_000, +): Promise { await Promise.any( locators.map(async (locator) => { await locator.waitFor({ state: "visible", timeout }); @@ -11,7 +19,9 @@ async function waitForFirstVisible(locators: Locator[], timeout = 10_000): Promi } async function expectInstancesPageContent(page: Page): Promise { - await expect(page.getByRole("main").getByRole("heading", { name: "Instances" })).toBeVisible(); + await expect( + page.getByRole("main").getByRole("heading", { name: "Instances" }), + ).toBeVisible(); await expect(page.getByRole("button", { name: "New Agent" })).toBeVisible(); await waitForFirstVisible( @@ -32,7 +42,10 @@ test.describe("Milady agent lifecycle", () => { await authenticateBrowserContext(request, page.context(), baseURL); }); - test("instances dashboard renders an authenticated Milady session", async ({ page, baseURL }) => { + test("instances dashboard renders an authenticated Milady session", async ({ + page, + baseURL, + }) => { const baseUrl = baseURL ?? "http://localhost:3000"; const response = await page.goto(`${baseUrl}/dashboard/milady`); expect(response?.status()).toBe(200); @@ -52,7 +65,9 @@ test.describe("Milady agent lifecycle", () => { const agentsResponse = await page.goto(`${baseUrl}/dashboard/my-agents`); expect(agentsResponse?.status()).toBe(200); - await expect(page.getByRole("heading", { name: /My Agents/i })).toBeVisible(); + await expect( + page.getByRole("heading", { name: /My Agents/i }), + ).toBeVisible(); expect(page.url()).toBe(`${baseUrl}/dashboard/my-agents`); const returnResponse = await page.goto(`${baseUrl}/dashboard/milady`); @@ -69,7 +84,9 @@ test.describe("Milady agent lifecycle", () => { `${baseUrl}/dashboard/milady/agents/00000000-0000-4000-8000-000000000000`, ); - expect(response?.status(), `unexpected status for ${page.url()}`).not.toBe(500); + expect(response?.status(), `unexpected status for ${page.url()}`).not.toBe( + 500, + ); expect(page.url()).not.toContain("/login"); }); }); diff --git a/packages/tests/playwright/flows/anonymous-chat.spec.ts b/packages/tests/playwright/flows/anonymous-chat.spec.ts index 4b97a764c..8dbceadf1 100644 --- a/packages/tests/playwright/flows/anonymous-chat.spec.ts +++ b/packages/tests/playwright/flows/anonymous-chat.spec.ts @@ -40,7 +40,9 @@ test.describe("Anonymous Chat Flow", () => { const criticalErrors = errors.filter( (e) => - !e.includes("WalletConnect") && !e.includes("hydration") && !e.includes("ResizeObserver"), + !e.includes("WalletConnect") && + !e.includes("hydration") && + !e.includes("ResizeObserver"), ); expect(criticalErrors).toHaveLength(0); }); diff --git a/packages/tests/playwright/flows/billing-and-tools.spec.ts b/packages/tests/playwright/flows/billing-and-tools.spec.ts index 500a01e52..8b92f095f 100644 --- a/packages/tests/playwright/flows/billing-and-tools.spec.ts +++ b/packages/tests/playwright/flows/billing-and-tools.spec.ts @@ -39,7 +39,9 @@ test.describe("API Key Management Flow", () => { const criticalErrors = errors.filter( (e) => - !e.includes("WalletConnect") && !e.includes("hydration") && !e.includes("ResizeObserver"), + !e.includes("WalletConnect") && + !e.includes("hydration") && + !e.includes("ResizeObserver"), ); expect(criticalErrors).toHaveLength(0); }); diff --git a/packages/tests/playwright/generate-image.spec.ts b/packages/tests/playwright/generate-image.spec.ts index 3fc4a0e2c..34d7be1d3 100644 --- a/packages/tests/playwright/generate-image.spec.ts +++ b/packages/tests/playwright/generate-image.spec.ts @@ -37,7 +37,9 @@ async function generateImage( ): Promise>> { const { authenticated = true, timeout = IMAGE_GENERATION_TIMEOUT } = options; return request.post(`${CLOUD_URL}/api/v1/generate-image`, { - headers: authenticated ? authHeaders() : { "Content-Type": "application/json" }, + headers: authenticated + ? authHeaders() + : { "Content-Type": "application/json" }, data, timeout, }); @@ -56,7 +58,9 @@ async function getCreditBalance(request: APIRequestContext): Promise { test.describe("Image Generation API - /api/v1/generate-image", () => { test.describe("Authentication", () => { - test("handles unauthenticated requests without crashing", async ({ request }) => { + test("handles unauthenticated requests without crashing", async ({ + request, + }) => { const response = await generateImage( request, { prompt: "A simple test image" }, @@ -128,7 +132,9 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { expect(ACCEPTED_GENERATION_STATUSES).toContain(response.status()); }); - test("falls back to default model for invalid model", async ({ request }) => { + test("falls back to default model for invalid model", async ({ + request, + }) => { const response = await generateImage(request, { prompt: "A purple pentagon", model: "invalid/model-name", @@ -168,14 +174,19 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { test.describe("CORS Headers", () => { test("OPTIONS returns correct CORS headers", async ({ request }) => { - const response = await request.fetch(`${CLOUD_URL}/api/v1/generate-image`, { - method: "OPTIONS", - headers: { Origin: "https://example.com" }, - }); + const response = await request.fetch( + `${CLOUD_URL}/api/v1/generate-image`, + { + method: "OPTIONS", + headers: { Origin: "https://example.com" }, + }, + ); expect(response.status()).toBe(204); expect(response.headers()["access-control-allow-origin"]).toBe("*"); - expect(response.headers()["access-control-allow-methods"]).toContain("POST"); + expect(response.headers()["access-control-allow-methods"]).toContain( + "POST", + ); }); }); @@ -183,7 +194,9 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { test.skip(() => !API_KEY, "TEST_API_KEY required"); test.setTimeout(IMAGE_GENERATION_TIMEOUT); - test("returns correct response structure on success", async ({ request }) => { + test("returns correct response structure on success", async ({ + request, + }) => { const response = await generateImage(request, { prompt: "A simple icon", }); @@ -239,9 +252,13 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { console.log(`Generated ${body.numImages} image(s)`); expect(body.numImages).toBeGreaterThan(0); expect(body.images.length).toBeGreaterThan(0); - console.log("SUCCESS: streamText() works with string model ID via AI Gateway"); + console.log( + "SUCCESS: streamText() works with string model ID via AI Gateway", + ); } else if (status === 402) { - console.log("Test skipped: Insufficient credits (but no runtime error occurred)"); + console.log( + "Test skipped: Insufficient credits (but no runtime error occurred)", + ); } else if (status === 500) { const body = await response.json(); console.log(`Error response: ${JSON.stringify(body)}`); @@ -254,7 +271,9 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { expect([200, 402, 500, 503]).toContain(status); }); - test("generates image successfully with OpenAI model (string ID)", async ({ request }) => { + test("generates image successfully with OpenAI model (string ID)", async ({ + request, + }) => { const response = await generateImage(request, { prompt: "A minimalist red icon", model: "openai/gpt-5-nano", @@ -270,7 +289,9 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { expect(body.numImages).toBeGreaterThan(0); console.log("SUCCESS: streamText() works with OpenAI string model ID"); } else if (status === 402) { - console.log("Test passed: Model ID accepted, just insufficient credits"); + console.log( + "Test passed: Model ID accepted, just insufficient credits", + ); } else if (status === 500) { const body = await response.json(); const errorMsg = body.error?.toLowerCase() || ""; @@ -286,7 +307,9 @@ test.describe("Image Generation API - /api/v1/generate-image", () => { test.skip(() => !API_KEY, "TEST_API_KEY required"); test.setTimeout(IMAGE_GENERATION_TIMEOUT); - test("accepts sourceImage for image-to-image generation", async ({ request }) => { + test("accepts sourceImage for image-to-image generation", async ({ + request, + }) => { const minimalPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; diff --git a/packages/tests/playwright/global-setup.cjs b/packages/tests/playwright/global-setup.cjs index d30eaede5..74dbd0e67 100644 --- a/packages/tests/playwright/global-setup.cjs +++ b/packages/tests/playwright/global-setup.cjs @@ -54,7 +54,9 @@ function getFirstExternalIpv4Address() { function getLocalDockerDatabaseUrl(env = process.env) { const host = - env.LOCAL_DOCKER_DB_HOST || getFirstExternalIpv4Address() || DEFAULT_LOCAL_DOCKER_DB_HOST; + env.LOCAL_DOCKER_DB_HOST || + getFirstExternalIpv4Address() || + DEFAULT_LOCAL_DOCKER_DB_HOST; const port = env.LOCAL_DOCKER_DB_PORT || DEFAULT_LOCAL_DOCKER_DB_PORT; return `postgresql://${LOCAL_DOCKER_DB_USER}:${LOCAL_DOCKER_DB_PASSWORD}@${host}:${port}/${LOCAL_DOCKER_DB_NAME}`; @@ -106,14 +108,17 @@ function loadPlaywrightEnv() { } const shouldPreferLocalDockerDb = - process.env.CI !== "true" && process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK !== "1"; + process.env.CI !== "true" && + process.env.DISABLE_LOCAL_DOCKER_DB_FALLBACK !== "1"; const localDockerDatabaseUrl = getLocalDockerDatabaseUrl({ ...process.env, LOCAL_DOCKER_DB_HOST: process.env.LOCAL_DOCKER_DB_HOST || "localhost", }); const testDatabaseUrl = process.env.TEST_DATABASE_URL || - (shouldPreferLocalDockerDb ? localDockerDatabaseUrl : process.env.DATABASE_URL); + (shouldPreferLocalDockerDb + ? localDockerDatabaseUrl + : process.env.DATABASE_URL); if (testDatabaseUrl) { process.env.TEST_DATABASE_URL = testDatabaseUrl; @@ -125,9 +130,12 @@ function loadPlaywrightEnv() { } function getDatabaseUrl() { - const connectionString = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL; + const connectionString = + process.env.TEST_DATABASE_URL || process.env.DATABASE_URL; if (!connectionString) { - throw new Error("TEST_DATABASE_URL or DATABASE_URL is required for live auth bootstrap"); + throw new Error( + "TEST_DATABASE_URL or DATABASE_URL is required for live auth bootstrap", + ); } return connectionString; } @@ -140,7 +148,11 @@ function getApiKeyPrefix(key) { return key.slice(0, 12); } -function createPlaywrightTestSessionToken(userId, organizationId, env = process.env) { +function createPlaywrightTestSessionToken( + userId, + organizationId, + env = process.env, +) { const secret = env.PLAYWRIGHT_TEST_AUTH_SECRET?.trim(); if (env.PLAYWRIGHT_TEST_AUTH !== "true" || !secret || secret.length < 16) { throw new Error("Playwright test auth is not enabled"); @@ -152,7 +164,10 @@ function createPlaywrightTestSessionToken(userId, organizationId, env = process. exp: Math.floor(Date.now() / 1000) + PLAYWRIGHT_TEST_SESSION_TTL_SECONDS, }; const payload = Buffer.from(JSON.stringify(claims)).toString("base64url"); - const signature = crypto.createHmac("sha256", secret).update(payload).digest("base64url"); + const signature = crypto + .createHmac("sha256", secret) + .update(payload) + .digest("base64url"); return `${payload}.${signature}`; } @@ -227,7 +242,13 @@ async function upsertUser(client, organizationId) { updated_at = NOW() WHERE id = $1 RETURNING id`, - [existingUsers.rows[0].id, TEST_USER_EMAIL, TEST_USER_NAME, organizationId, TEST_USER_WALLET], + [ + existingUsers.rows[0].id, + TEST_USER_EMAIL, + TEST_USER_NAME, + organizationId, + TEST_USER_WALLET, + ], ); return result.rows[0].id; @@ -249,7 +270,13 @@ async function upsertUser(client, organizationId) { ) VALUES ($1, $2, $3, $4, 'owner', false, true, true, $5, 'evm', true) RETURNING id`, - [TEST_USER_ID, TEST_USER_EMAIL, TEST_USER_NAME, organizationId, TEST_USER_WALLET], + [ + TEST_USER_ID, + TEST_USER_EMAIL, + TEST_USER_NAME, + organizationId, + TEST_USER_WALLET, + ], ); return result.rows[0].id; @@ -335,7 +362,10 @@ async function bootstrapLocalTestAuth() { await client.query("COMMIT"); - const sessionToken = createPlaywrightTestSessionToken(userId, organizationId); + const sessionToken = createPlaywrightTestSessionToken( + userId, + organizationId, + ); process.env.TEST_API_KEY = apiKey; process.env.TEST_USER_ID = userId; diff --git a/packages/tests/playwright/pages/admin-pages.spec.ts b/packages/tests/playwright/pages/admin-pages.spec.ts index e83ecd49c..1e35f7306 100644 --- a/packages/tests/playwright/pages/admin-pages.spec.ts +++ b/packages/tests/playwright/pages/admin-pages.spec.ts @@ -18,7 +18,10 @@ test.describe("Admin Pages", () => { test(`${path} loads without 500`, async ({ page }) => { const response = await page.goto(`http://localhost:3000${path}`); // Admin pages redirect to login or show forbidden — but never 500 - expect(response?.status(), `${path} returned ${response?.status()}`).not.toBe(500); + expect( + response?.status(), + `${path} returned ${response?.status()}`, + ).not.toBe(500); }); } }); diff --git a/packages/tests/playwright/pages/dashboard-pages.spec.ts b/packages/tests/playwright/pages/dashboard-pages.spec.ts index ae6104485..24f4f539e 100644 --- a/packages/tests/playwright/pages/dashboard-pages.spec.ts +++ b/packages/tests/playwright/pages/dashboard-pages.spec.ts @@ -57,27 +57,36 @@ test.describe("Dashboard Pages", () => { const response = await page.goto(`http://localhost:3000${path}`); // Dashboard pages should either render (200) or redirect to login (302) // but NEVER return 500 - expect(response?.status(), `${path} returned ${response?.status()}`).not.toBe(500); + expect( + response?.status(), + `${path} returned ${response?.status()}`, + ).not.toBe(500); expect([200, 301, 302, 304]).toContain(response?.status() ?? 0); }); } test.describe("Dynamic Dashboard Pages", () => { - test("/dashboard/containers/[id] handles nonexistent ID", async ({ page }) => { + test("/dashboard/containers/[id] handles nonexistent ID", async ({ + page, + }) => { const response = await page.goto( "http://localhost:3000/dashboard/containers/00000000-0000-4000-8000-000000000000", ); expect(response?.status()).not.toBe(500); }); - test("/dashboard/containers/agents/[id] handles nonexistent ID", async ({ page }) => { + test("/dashboard/containers/agents/[id] handles nonexistent ID", async ({ + page, + }) => { const response = await page.goto( "http://localhost:3000/dashboard/containers/agents/00000000-0000-4000-8000-000000000000", ); expect(response?.status()).not.toBe(500); }); - test("/dashboard/milady/agents/[id] handles nonexistent ID", async ({ page }) => { + test("/dashboard/milady/agents/[id] handles nonexistent ID", async ({ + page, + }) => { const response = await page.goto( "http://localhost:3000/dashboard/milady/agents/00000000-0000-4000-8000-000000000000", ); @@ -91,7 +100,9 @@ test.describe("Dashboard Pages", () => { expect(response?.status()).not.toBe(500); }); - test("/dashboard/invoices/[id] handles nonexistent ID", async ({ page }) => { + test("/dashboard/invoices/[id] handles nonexistent ID", async ({ + page, + }) => { const response = await page.goto( "http://localhost:3000/dashboard/invoices/00000000-0000-4000-8000-000000000000", ); diff --git a/packages/tests/playwright/pages/public-pages.spec.ts b/packages/tests/playwright/pages/public-pages.spec.ts index 64b464e10..de5f67649 100644 --- a/packages/tests/playwright/pages/public-pages.spec.ts +++ b/packages/tests/playwright/pages/public-pages.spec.ts @@ -35,7 +35,9 @@ test.describe("Public Pages", () => { // Blog slug pages depend on content existing — just verify no 500 test("/blog/[slug] handles missing slug gracefully", async ({ page }) => { - const response = await page.goto("http://localhost:3000/blog/nonexistent-post"); + const response = await page.goto( + "http://localhost:3000/blog/nonexistent-post", + ); expect(response?.status()).not.toBe(500); }); }); @@ -62,12 +64,16 @@ test.describe("Public Pages", () => { test.describe("Public Chat", () => { // Chat with a known characterId — should load even if character doesn't exist - test("/chat/[characterId] handles nonexistent character", async ({ page }) => { + test("/chat/[characterId] handles nonexistent character", async ({ + page, + }) => { const response = await page.goto( "http://localhost:3000/chat/00000000-0000-4000-8000-000000000000", { waitUntil: "domcontentloaded" }, ); - await expect(page.locator("body")).toContainText("This page could not be found."); + await expect(page.locator("body")).toContainText( + "This page could not be found.", + ); expect(response?.status()).not.toBe(500); expect([200, 304, 404]).toContain(response?.status() ?? 0); }); @@ -75,7 +81,9 @@ test.describe("Public Pages", () => { test.describe("OAuth & App Auth", () => { test("/app-auth/authorize loads without crashing", async ({ page }) => { - const response = await page.goto("http://localhost:3000/app-auth/authorize"); + const response = await page.goto( + "http://localhost:3000/app-auth/authorize", + ); expect(response?.status()).not.toBe(500); }); }); @@ -86,7 +94,9 @@ test.describe("Public Pages", () => { }); test("/invite/accept loads with test token", async ({ page }) => { - const response = await page.goto("http://localhost:3000/invite/accept?token=test-token"); + const response = await page.goto( + "http://localhost:3000/invite/accept?token=test-token", + ); expect(response?.status()).not.toBe(500); expect([200, 304]).toContain(response?.status() ?? 0); }); diff --git a/packages/tests/playwright/toctou-race-condition.spec.ts b/packages/tests/playwright/toctou-race-condition.spec.ts index bf226bec5..41ee7a231 100644 --- a/packages/tests/playwright/toctou-race-condition.spec.ts +++ b/packages/tests/playwright/toctou-race-condition.spec.ts @@ -51,7 +51,9 @@ test.describe("TOCTOU Race Condition - Credit Deduction", () => { * ACTUAL (bug): All 5 requests succeed, balance goes negative or * some deductions fail silently */ - test("parallel requests should not over-consume credits", async ({ request }) => { + test("parallel requests should not over-consume credits", async ({ + request, + }) => { // 1. Get initial balance const initialBalance = await getCreditBalance(request); @@ -66,7 +68,9 @@ test.describe("TOCTOU Race Condition - Credit Deduction", () => { // 2. Calculate how many requests SHOULD fit in the balance // Actual cost per request is ~$0.02 for gpt-4o-mini short message const actualCostPerRequest = 0.02; - const maxPossibleRequests = Math.floor(initialBalance / actualCostPerRequest); + const maxPossibleRequests = Math.floor( + initialBalance / actualCostPerRequest, + ); // We'll send 50 parallel requests to maximize race condition window // With TOCTOU bug: many more than maxPossibleRequests may succeed @@ -97,8 +101,12 @@ test.describe("TOCTOU Race Condition - Credit Deduction", () => { // 5. Count successes vs failures const successes = responses.filter((r) => r.status() === 200).length; - const insufficientCredits = responses.filter((r) => r.status() === 402).length; - const otherErrors = responses.filter((r) => r.status() !== 200 && r.status() !== 402).length; + const insufficientCredits = responses.filter( + (r) => r.status() === 402, + ).length; + const otherErrors = responses.filter( + (r) => r.status() !== 200 && r.status() !== 402, + ).length; console.log(`✅ Successes: ${successes}`); console.log(`❌ Insufficient credits (402): ${insufficientCredits}`); @@ -109,7 +117,9 @@ test.describe("TOCTOU Race Condition - Credit Deduction", () => { const finalBalance = await getCreditBalance(request); console.log(`📊 Final balance: $${finalBalance.toFixed(4)}`); - console.log(`💰 Total deducted: $${(initialBalance - finalBalance).toFixed(4)}`); + console.log( + `💰 Total deducted: $${(initialBalance - finalBalance).toFixed(4)}`, + ); // 7. THE BUG ASSERTION // If TOCTOU bug exists: successes > maxPossibleRequests (over-consumption) @@ -145,7 +155,9 @@ test.describe("TOCTOU Race Condition - Credit Deduction", () => { /** * Test specifically for streaming endpoints where the window is larger */ - test("streaming requests should deduct credits atomically", async ({ request }) => { + test("streaming requests should deduct credits atomically", async ({ + request, + }) => { // Get initial balance const initialBalance = await getCreditBalance(request); @@ -184,9 +196,10 @@ test.describe("TOCTOU Race Condition - Credit Deduction", () => { console.log(`📊 Final balance: $${finalBalance.toFixed(4)}`); // Balance should not be negative - expect(finalBalance, "Balance should not go negative after streaming").toBeGreaterThanOrEqual( - -0.01, - ); + expect( + finalBalance, + "Balance should not go negative after streaming", + ).toBeGreaterThanOrEqual(-0.01); }); }); @@ -197,7 +210,9 @@ test.describe("MCP Endpoint - Correct Pattern (Reference)", () => { * This test shows that /api/mcp correctly handles concurrent requests * because it uses the deduct-before pattern */ - test("MCP deduct-before pattern handles concurrency correctly", async ({ request }) => { + test("MCP deduct-before pattern handles concurrency correctly", async ({ + request, + }) => { // This is the reference implementation that works correctly // The other endpoints should be fixed to match this pattern console.log("ℹ️ /api/mcp uses deduct-before pattern - this is the target"); diff --git a/packages/tests/playwright/uuid-sanitization.spec.ts b/packages/tests/playwright/uuid-sanitization.spec.ts index b0f012730..80ac1f9b4 100644 --- a/packages/tests/playwright/uuid-sanitization.spec.ts +++ b/packages/tests/playwright/uuid-sanitization.spec.ts @@ -26,12 +26,17 @@ async function gotoDashboardChat(page: Page, search: string) { test.describe("UUID Sanitization - Dashboard Chat", () => { test.describe("Malformed characterId Handling", () => { - test("dashboard chat handles characterId with trailing backslash", async ({ page }) => { + test("dashboard chat handles characterId with trailing backslash", async ({ + page, + }) => { // This is the exact pattern from production error logs // URL: ?characterId=17c8b876-86a0-465d-9794-2aea244f4239%5C const malformedId = "17c8b876-86a0-465d-9794-2aea244f4239%5C"; // %5C = backslash - const response = await gotoDashboardChat(page, `?characterId=${malformedId}`); + const response = await gotoDashboardChat( + page, + `?characterId=${malformedId}`, + ); // Should NOT return 500 (the original bug) expect(response?.status()).not.toBe(500); @@ -40,26 +45,41 @@ test.describe("UUID Sanitization - Dashboard Chat", () => { expect(response?.status()).toBe(200); }); - test("dashboard chat handles characterId with double backslash", async ({ page }) => { + test("dashboard chat handles characterId with double backslash", async ({ + page, + }) => { const malformedId = "17c8b876-86a0-465d-9794-2aea244f4239%5C%5C"; - const response = await gotoDashboardChat(page, `?characterId=${malformedId}`); + const response = await gotoDashboardChat( + page, + `?characterId=${malformedId}`, + ); expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); }); - test("dashboard chat handles characterId with trailing forward slash", async ({ page }) => { + test("dashboard chat handles characterId with trailing forward slash", async ({ + page, + }) => { const malformedId = "17c8b876-86a0-465d-9794-2aea244f4239%2F"; // %2F = forward slash - const response = await gotoDashboardChat(page, `?characterId=${malformedId}`); + const response = await gotoDashboardChat( + page, + `?characterId=${malformedId}`, + ); expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); }); - test("dashboard chat handles completely invalid characterId", async ({ page }) => { - const response = await gotoDashboardChat(page, "?characterId=not-a-valid-uuid"); + test("dashboard chat handles completely invalid characterId", async ({ + page, + }) => { + const response = await gotoDashboardChat( + page, + "?characterId=not-a-valid-uuid", + ); expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); @@ -71,11 +91,16 @@ test.describe("UUID Sanitization - Dashboard Chat", () => { expect(response?.status()).toBe(200); }); - test("dashboard chat works with valid characterId format", async ({ page }) => { + test("dashboard chat works with valid characterId format", async ({ + page, + }) => { // Valid UUID that likely doesn't exist - should still not 500 const validUuid = "00000000-0000-4000-8000-000000000000"; - const response = await gotoDashboardChat(page, `?characterId=${validUuid}`); + const response = await gotoDashboardChat( + page, + `?characterId=${validUuid}`, + ); expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); @@ -83,7 +108,9 @@ test.describe("UUID Sanitization - Dashboard Chat", () => { }); test.describe("Malformed roomId Handling", () => { - test("dashboard chat handles roomId with trailing backslash", async ({ page }) => { + test("dashboard chat handles roomId with trailing backslash", async ({ + page, + }) => { const malformedId = "17c8b876-86a0-465d-9794-2aea244f4239%5C"; const response = await gotoDashboardChat(page, `?roomId=${malformedId}`); @@ -101,7 +128,9 @@ test.describe("UUID Sanitization - Dashboard Chat", () => { }); test.describe("No Console Errors on Malformed Input", () => { - test("no critical JS errors with malformed characterId", async ({ page }) => { + test("no critical JS errors with malformed characterId", async ({ + page, + }) => { const errors: string[] = []; page.on("pageerror", (err) => errors.push(err.message)); @@ -124,7 +153,9 @@ test.describe("UUID Sanitization - Dashboard Chat", () => { }); test.describe("SQL Injection Prevention", () => { - test("dashboard chat safely handles SQL-like characterId", async ({ page }) => { + test("dashboard chat safely handles SQL-like characterId", async ({ + page, + }) => { // Attempt SQL injection via characterId parameter const maliciousId = "'; DROP TABLE users; --"; @@ -138,7 +169,9 @@ test.describe("UUID Sanitization - Dashboard Chat", () => { expect(response?.status()).toBe(200); }); - test("dashboard chat safely handles unicode in characterId", async ({ page }) => { + test("dashboard chat safely handles unicode in characterId", async ({ + page, + }) => { const unicodeId = "test-id-\u0000-null-byte"; const response = await gotoDashboardChat( @@ -160,7 +193,9 @@ test.describe("UUID Sanitization - API Response", () => { expect(data).toHaveProperty("registry"); }); - test("MCP registry shows eliza-platform as coming_soon", async ({ request }) => { + test("MCP registry shows eliza-platform as coming_soon", async ({ + request, + }) => { const response = await request.get(`${BASE_URL}/api/mcp/registry`); const data = await response.json(); diff --git a/packages/tests/properties/financial-invariants.test.ts b/packages/tests/properties/financial-invariants.test.ts index 50c68c259..b7a27147a 100644 --- a/packages/tests/properties/financial-invariants.test.ts +++ b/packages/tests/properties/financial-invariants.test.ts @@ -18,14 +18,23 @@ import { eq } from "drizzle-orm"; import fc from "fast-check"; import { v4 as uuidv4 } from "uuid"; import { dbRead, dbWrite } from "@/db/client"; -import { agentBudgets, agentBudgetTransactions } from "@/db/schemas/agent-budgets"; +import { + agentBudgets, + agentBudgetTransactions, +} from "@/db/schemas/agent-budgets"; import { organizations } from "@/db/schemas/organizations"; -import { redeemableEarnings, redeemableEarningsLedger } from "@/db/schemas/redeemable-earnings"; +import { + redeemableEarnings, + redeemableEarningsLedger, +} from "@/db/schemas/redeemable-earnings"; import { agentBudgetService } from "@/lib/services/agent-budgets"; import { creditsService } from "@/lib/services/credits"; import { redeemableEarningsService } from "@/lib/services/redeemable-earnings"; import { getConnectionString } from "@/tests/helpers/local-database"; -import { cleanupTestData, createTestDataSet } from "@/tests/helpers/test-data-factory"; +import { + cleanupTestData, + createTestDataSet, +} from "@/tests/helpers/test-data-factory"; /** * Property test configuration @@ -131,7 +140,10 @@ describe("Financial Invariants (Property-Based)", () => { return true; } finally { // Cleanup - await cleanupTestData(connectionString, testData.organization.id); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), @@ -175,11 +187,13 @@ describe("Financial Invariants (Property-Based)", () => { expectedBalance += op.amount; } } else { - const result = await creditsService.reserveAndDeductCredits({ - organizationId: testData.organization.id, - amount: op.amount, - description: "Sum test deduct", - }); + const result = await creditsService.reserveAndDeductCredits( + { + organizationId: testData.organization.id, + amount: op.amount, + description: "Sum test deduct", + }, + ); if (result.success) { expectedBalance -= op.amount; } @@ -196,12 +210,17 @@ describe("Financial Invariants (Property-Based)", () => { // INVARIANT: Calculated balance should match actual. // Round the drift itself to avoid binary floating-point noise // turning an allowed 0.02 tolerance into 0.020000000000038654. - const balanceDrift = normalizeDrift(Math.abs(finalBalance - expectedBalance)); + const balanceDrift = normalizeDrift( + Math.abs(finalBalance - expectedBalance), + ); expect(balanceDrift).toBeLessThanOrEqual(0.02); return true; } finally { - await cleanupTestData(connectionString, testData.organization.id); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), @@ -272,8 +291,13 @@ describe("Financial Invariants (Property-Based)", () => { await dbWrite .delete(agentBudgetTransactions) .where(eq(agentBudgetTransactions.agent_id, agentId)); - await dbWrite.delete(agentBudgets).where(eq(agentBudgets.agent_id, agentId)); - await cleanupTestData(connectionString, testData.organization.id); + await dbWrite + .delete(agentBudgets) + .where(eq(agentBudgets.agent_id, agentId)); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), @@ -338,8 +362,13 @@ describe("Financial Invariants (Property-Based)", () => { await dbWrite .delete(agentBudgetTransactions) .where(eq(agentBudgetTransactions.agent_id, agentId)); - await dbWrite.delete(agentBudgets).where(eq(agentBudgets.agent_id, agentId)); - await cleanupTestData(connectionString, testData.organization.id); + await dbWrite + .delete(agentBudgets) + .where(eq(agentBudgets.agent_id, agentId)); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), @@ -364,9 +393,11 @@ describe("Financial Invariants (Property-Based)", () => { fc.record({ type: fc.constantFrom("earn", "lock"), amount: dbSafeAmount(1, 20), - source: fc.constantFrom("miniapp", "agent", "mcp") as fc.Arbitrary< - "miniapp" | "agent" | "mcp" - >, + source: fc.constantFrom( + "miniapp", + "agent", + "mcp", + ) as fc.Arbitrary<"miniapp" | "agent" | "mcp">, }), { minLength: 1, maxLength: 20 }, ), @@ -413,21 +444,27 @@ describe("Financial Invariants (Property-Based)", () => { } // Verify invariants - const balance = await redeemableEarningsService.getBalance(userId); + const balance = + await redeemableEarningsService.getBalance(userId); if (balance) { // INVARIANT: Available balance must never be negative - expect(normalizeDrift(balance.availableBalance)).toBeGreaterThanOrEqual(0); + expect( + normalizeDrift(balance.availableBalance), + ).toBeGreaterThanOrEqual(0); // INVARIANT: total_earned >= total_redeemed + total_pending const earningsCoverageDrift = normalizeDrift( - balance.totalEarned - (balance.totalRedeemed + balance.totalPending), + balance.totalEarned - + (balance.totalRedeemed + balance.totalPending), ); expect(earningsCoverageDrift).toBeGreaterThanOrEqual(-0.01); // INVARIANT: available = earned - redeemed - pending const calculatedAvailable = - balance.totalEarned - balance.totalRedeemed - balance.totalPending; + balance.totalEarned - + balance.totalRedeemed - + balance.totalPending; const availableBalanceDrift = normalizeDrift( Math.abs(balance.availableBalance - calculatedAvailable), ); @@ -442,7 +479,10 @@ describe("Financial Invariants (Property-Based)", () => { await dbWrite .delete(redeemableEarnings) .where(eq(redeemableEarnings.user_id, userId)); - await cleanupTestData(connectionString, testData.organization.id); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), @@ -460,9 +500,11 @@ describe("Financial Invariants (Property-Based)", () => { fc.array( fc.record({ amount: dbSafeAmount(1, 30), - source: fc.constantFrom("miniapp", "agent", "mcp") as fc.Arbitrary< - "miniapp" | "agent" | "mcp" - >, + source: fc.constantFrom( + "miniapp", + "agent", + "mcp", + ) as fc.Arbitrary<"miniapp" | "agent" | "mcp">, }), { minLength: 1, maxLength: 15 }, ), @@ -486,11 +528,14 @@ describe("Financial Invariants (Property-Based)", () => { } // Verify - const balance = await redeemableEarningsService.getBalance(userId); + const balance = + await redeemableEarningsService.getBalance(userId); if (balance) { const sumOfSources = - balance.breakdown.miniapps + balance.breakdown.agents + balance.breakdown.mcps; + balance.breakdown.miniapps + + balance.breakdown.agents + + balance.breakdown.mcps; // INVARIANT: Sum of sources = total earned const sourceBreakdownDrift = normalizeDrift( @@ -507,7 +552,10 @@ describe("Financial Invariants (Property-Based)", () => { await dbWrite .delete(redeemableEarnings) .where(eq(redeemableEarnings.user_id, userId)); - await cleanupTestData(connectionString, testData.organization.id); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), @@ -572,15 +620,22 @@ describe("Financial Invariants (Property-Based)", () => { // INVARIANT: Neither can be negative expect(normalizeDrift(orgBalance)).toBeGreaterThanOrEqual(0); - expect(normalizeDrift(budgetAllocated)).toBeGreaterThanOrEqual(0); + expect(normalizeDrift(budgetAllocated)).toBeGreaterThanOrEqual( + 0, + ); return true; } finally { await dbWrite .delete(agentBudgetTransactions) .where(eq(agentBudgetTransactions.agent_id, agentId)); - await dbWrite.delete(agentBudgets).where(eq(agentBudgets.agent_id, agentId)); - await cleanupTestData(connectionString, testData.organization.id); + await dbWrite + .delete(agentBudgets) + .where(eq(agentBudgets.agent_id, agentId)); + await cleanupTestData( + connectionString, + testData.organization.id, + ); } }, ), diff --git a/packages/tests/runtime/integration/cloud-bootstrap/web-search-multi-step.test.ts b/packages/tests/runtime/integration/cloud-bootstrap/web-search-multi-step.test.ts index b25b5c7ce..e322cc4a6 100644 --- a/packages/tests/runtime/integration/cloud-bootstrap/web-search-multi-step.test.ts +++ b/packages/tests/runtime/integration/cloud-bootstrap/web-search-multi-step.test.ts @@ -89,183 +89,191 @@ let runtime: TestRuntime; let testUser: TestUserContext; const timings: Record = {}; -describe.skipIf(skipLiveModelSuite)("CloudBootstrapMessageService - Web Search Multi-Step", () => { - beforeAll(async () => { - console.log("\n" + "=".repeat(70)); - console.log("MULTI-STEP WEB SEARCH INTEGRATION TESTS"); - console.log("Using Claude Sonnet + Real Web Search (Tavily)"); - console.log("=".repeat(70)); - - const connected = await verifyConnection(); - if (!connected) { - throw new Error("Cannot connect to database. Ensure server is running."); - } - connectionString = getConnectionString(); - console.log("✓ Database connected"); - - testData = await createTestDataSet(connectionString, { - organizationName: "Web Search Multi-Step Test Org", - userName: "Web Search Test User", - userEmail: `web-search-multistep-${Date.now()}@eliza.test`, - creditBalance: 100.0, - includeCharacter: true, - characterName: webSearchCharacter.name, - characterData: webSearchCharacter as Record, - characterSettings: webSearchCharacter.settings as Record, +describe.skipIf(skipLiveModelSuite)( + "CloudBootstrapMessageService - Web Search Multi-Step", + () => { + beforeAll(async () => { + console.log("\n" + "=".repeat(70)); + console.log("MULTI-STEP WEB SEARCH INTEGRATION TESTS"); + console.log("Using Claude Sonnet + Real Web Search (Tavily)"); + console.log("=".repeat(70)); + + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Ensure server is running.", + ); + } + connectionString = getConnectionString(); + console.log("✓ Database connected"); + + testData = await createTestDataSet(connectionString, { + organizationName: "Web Search Multi-Step Test Org", + userName: "Web Search Test User", + userEmail: `web-search-multistep-${Date.now()}@eliza.test`, + creditBalance: 100.0, + includeCharacter: true, + characterName: webSearchCharacter.name, + characterData: webSearchCharacter as Record, + characterSettings: webSearchCharacter.settings as Record< + string, + unknown + >, + }); + + console.log(`✓ Test data created`); + console.log(` API Key: ${testData.apiKey.keyPrefix}...`); + console.log(` Credits: $${testData.organization.creditBalance}`); + console.log("=".repeat(70) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\n" + "=".repeat(70)); + console.log("CLEANING UP"); + console.log("=".repeat(70)); + + if (runtime) { + await invalidateRuntime(runtime.agentId as string).catch((err) => + console.warn(`Runtime cleanup: ${err}`), + ); + } + if (testData && connectionString) { + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup: ${err}`), + ); + } + logTimings("Web Search Multi-Step Tests", timings); }); - console.log(`✓ Test data created`); - console.log(` API Key: ${testData.apiKey.keyPrefix}...`); - console.log(` Credits: $${testData.organization.creditBalance}`); - console.log("=".repeat(70) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\n" + "=".repeat(70)); - console.log("CLEANING UP"); - console.log("=".repeat(70)); - - if (runtime) { - await invalidateRuntime(runtime.agentId as string).catch((err) => - console.warn(`Runtime cleanup: ${err}`), - ); - } - if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup: ${err}`), - ); - } - logTimings("Web Search Multi-Step Tests", timings); - }); - - describe("Runtime Setup with Web Search", () => { - it("should create runtime with Claude Sonnet and web search enabled", async () => { - startTimer("runtime_create"); - - const userContext = buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - characterId: testData.character?.id, - webSearchEnabled: true, - modelPreferences: { - smallModel: "anthropic/claude-sonnet-4", - largeModel: "anthropic/claude-sonnet-4", - }, - }); + describe("Runtime Setup with Web Search", () => { + it("should create runtime with Claude Sonnet and web search enabled", async () => { + startTimer("runtime_create"); + + const userContext = buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + characterId: testData.character?.id, + webSearchEnabled: true, + modelPreferences: { + smallModel: "anthropic/claude-sonnet-4", + largeModel: "anthropic/claude-sonnet-4", + }, + }); + + runtime = await runtimeFactory.createRuntimeForUser(userContext); + timings.runtimeCreate = endTimer("runtime_create"); + + expect(runtime).toBeDefined(); + expect(runtime.character?.name).toBe("CryptoResearcher"); + + // Verify CloudBootstrapMessageService is installed + const serviceName = runtime.messageService?.constructor?.name; + console.log(`\n✓ Runtime created in ${timings.runtimeCreate}ms`); + console.log(` Character: ${runtime.character?.name}`); + console.log(` Message Service: ${serviceName}`); + console.log(` Agent ID: ${runtime.agentId}`); + + expect(serviceName).toBe("CloudBootstrapMessageService"); + }, 120000); + + it("should verify Tavily service is available", async () => { + const tavilyService = runtime.getService("TAVILY"); + expect(tavilyService).toBeDefined(); + console.log("✓ Tavily service initialized"); + }, 10000); + + it("should create test user with entities", async () => { + startTimer("user_create"); + testUser = await createTestUser(runtime, "WebSearchTestUser"); + timings.userCreate = endTimer("user_create"); + + expect(testUser.entityId).toBeDefined(); + expect(testUser.roomId).toBeDefined(); + + console.log(`\n✓ Test user created in ${timings.userCreate}ms`); + console.log(` Entity ID: ${testUser.entityId}`); + console.log(` Room ID: ${testUser.roomId}`); + }, 30000); + }); - runtime = await runtimeFactory.createRuntimeForUser(userContext); - timings.runtimeCreate = endTimer("runtime_create"); - - expect(runtime).toBeDefined(); - expect(runtime.character?.name).toBe("CryptoResearcher"); - - // Verify CloudBootstrapMessageService is installed - const serviceName = runtime.messageService?.constructor?.name; - console.log(`\n✓ Runtime created in ${timings.runtimeCreate}ms`); - console.log(` Character: ${runtime.character?.name}`); - console.log(` Message Service: ${serviceName}`); - console.log(` Agent ID: ${runtime.agentId}`); - - expect(serviceName).toBe("CloudBootstrapMessageService"); - }, 120000); - - it("should verify Tavily service is available", async () => { - const tavilyService = runtime.getService("TAVILY"); - expect(tavilyService).toBeDefined(); - console.log("✓ Tavily service initialized"); - }, 10000); - - it("should create test user with entities", async () => { - startTimer("user_create"); - testUser = await createTestUser(runtime, "WebSearchTestUser"); - timings.userCreate = endTimer("user_create"); - - expect(testUser.entityId).toBeDefined(); - expect(testUser.roomId).toBeDefined(); - - console.log(`\n✓ Test user created in ${timings.userCreate}ms`); - console.log(` Entity ID: ${testUser.entityId}`); - console.log(` Room ID: ${testUser.roomId}`); - }, 30000); - }); - - describe("Web Search Action Execution", () => { - it("should search for Ethereum news and return real results", async () => { - console.log("\n" + "-".repeat(50)); - console.log("TEST: Search for Ethereum news"); - console.log("-".repeat(50)); - - startTimer("eth_search"); - const result = await sendTestMessage( - runtime, - testUser, - "What is the latest news about Ethereum? Search for recent ETH updates.", - testData, - { timeoutMs: 300000 }, // 5 minutes - real API calls take time - ); - timings.ethSearch = endTimer("eth_search"); - - console.log(`\nDuration: ${timings.ethSearch}ms`); - console.log(`Did respond: ${result.didRespond}`); - - if (result.response?.text) { - console.log(`\nResponse (${result.response.text.length} chars):`); - console.log("-".repeat(40)); - console.log(result.response.text.substring(0, 500)); - if (result.response.text.length > 500) { - console.log("... [truncated]"); + describe("Web Search Action Execution", () => { + it("should search for Ethereum news and return real results", async () => { + console.log("\n" + "-".repeat(50)); + console.log("TEST: Search for Ethereum news"); + console.log("-".repeat(50)); + + startTimer("eth_search"); + const result = await sendTestMessage( + runtime, + testUser, + "What is the latest news about Ethereum? Search for recent ETH updates.", + testData, + { timeoutMs: 300000 }, // 5 minutes - real API calls take time + ); + timings.ethSearch = endTimer("eth_search"); + + console.log(`\nDuration: ${timings.ethSearch}ms`); + console.log(`Did respond: ${result.didRespond}`); + + if (result.response?.text) { + console.log(`\nResponse (${result.response.text.length} chars):`); + console.log("-".repeat(40)); + console.log(result.response.text.substring(0, 500)); + if (result.response.text.length > 500) { + console.log("... [truncated]"); + } + console.log("-".repeat(40)); } - console.log("-".repeat(40)); - } - if (result.error) { - console.log(`Error: ${result.error}`); - } + if (result.error) { + console.log(`Error: ${result.error}`); + } - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - expect(result.response?.text).toBeTruthy(); - expect(result.response?.text?.length).toBeGreaterThan(50); - - // Verify response contains relevant content - const responseText = result.response?.text?.toLowerCase() || ""; - const hasEthContent = - responseText.includes("ethereum") || - responseText.includes("eth") || - responseText.includes("blockchain") || - responseText.includes("crypto"); - expect(hasEthContent).toBe(true); - }, 360000); // 6 minute timeout - - it("should search for Hyperliquid news with finance topic", async () => { - console.log("\n" + "-".repeat(50)); - console.log("TEST: Search for Hyperliquid news"); - console.log("-".repeat(50)); - - startTimer("hype_search"); - const result = await sendTestMessage( - runtime, - testUser, - "Search the web for the latest Hyperliquid news and updates. What's happening with HYPE token?", - testData, - { timeoutMs: 300000 }, - ); - timings.hypeSearch = endTimer("hype_search"); - - console.log(`\nDuration: ${timings.hypeSearch}ms`); - console.log(`Did respond: ${result.didRespond}`); - - if (result.response?.text) { - console.log(`\nResponse (${result.response.text.length} chars):`); - console.log("-".repeat(40)); - console.log(result.response.text.substring(0, 500)); - if (result.response.text.length > 500) { - console.log("... [truncated]"); + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + expect(result.response?.text).toBeTruthy(); + expect(result.response?.text?.length).toBeGreaterThan(50); + + // Verify response contains relevant content + const responseText = result.response?.text?.toLowerCase() || ""; + const hasEthContent = + responseText.includes("ethereum") || + responseText.includes("eth") || + responseText.includes("blockchain") || + responseText.includes("crypto"); + expect(hasEthContent).toBe(true); + }, 360000); // 6 minute timeout + + it("should search for Hyperliquid news with finance topic", async () => { + console.log("\n" + "-".repeat(50)); + console.log("TEST: Search for Hyperliquid news"); + console.log("-".repeat(50)); + + startTimer("hype_search"); + const result = await sendTestMessage( + runtime, + testUser, + "Search the web for the latest Hyperliquid news and updates. What's happening with HYPE token?", + testData, + { timeoutMs: 300000 }, + ); + timings.hypeSearch = endTimer("hype_search"); + + console.log(`\nDuration: ${timings.hypeSearch}ms`); + console.log(`Did respond: ${result.didRespond}`); + + if (result.response?.text) { + console.log(`\nResponse (${result.response.text.length} chars):`); + console.log("-".repeat(40)); + console.log(result.response.text.substring(0, 500)); + if (result.response.text.length > 500) { + console.log("... [truncated]"); + } + console.log("-".repeat(40)); } - console.log("-".repeat(40)); - } - expect(result.didRespond).toBe(true); - expect(result.response?.text).toBeTruthy(); - }, 360000); - }); -}); + expect(result.didRespond).toBe(true); + expect(result.response?.text).toBeTruthy(); + }, 360000); + }); + }, +); diff --git a/packages/tests/runtime/integration/message-handler/basic-message.test.ts b/packages/tests/runtime/integration/message-handler/basic-message.test.ts index 050072d5f..ac823085e 100644 --- a/packages/tests/runtime/integration/message-handler/basic-message.test.ts +++ b/packages/tests/runtime/integration/message-handler/basic-message.test.ts @@ -42,185 +42,195 @@ let testUserContext: TestUserContext; const timings: Record = {}; const skipLiveModelSuite = !hasDatabaseUrl || !hasRuntimeModelCredentials; -describe.skipIf(skipLiveModelSuite)("Message Handler - Basic Message Processing", () => { - beforeAll(async () => { - console.log("\n" + "=".repeat(60)); - console.log("SETTING UP BASIC MESSAGE TEST ENVIRONMENT"); - console.log("=".repeat(60)); - - // Verify database connection - const connected = await verifyConnection(); - if (!connected) { - throw new Error( - "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", - ); - } - connectionString = getConnectionString(); - console.log("Database connected"); - - // Create test data with unique identifiers - testData = await createTestDataSet(connectionString, { - organizationName: "Basic Message Test Org", - userName: "Basic Message Test User", - userEmail: `basic-message-test-${Date.now()}@eliza.test`, - creditBalance: 1000.0, - includeCharacter: true, - characterName: "Mira", - characterData: mcpTestCharacter as unknown as Record, - characterSettings: mcpTestCharacter.settings as Record, +describe.skipIf(skipLiveModelSuite)( + "Message Handler - Basic Message Processing", + () => { + beforeAll(async () => { + console.log("\n" + "=".repeat(60)); + console.log("SETTING UP BASIC MESSAGE TEST ENVIRONMENT"); + console.log("=".repeat(60)); + + // Verify database connection + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", + ); + } + connectionString = getConnectionString(); + console.log("Database connected"); + + // Create test data with unique identifiers + testData = await createTestDataSet(connectionString, { + organizationName: "Basic Message Test Org", + userName: "Basic Message Test User", + userEmail: `basic-message-test-${Date.now()}@eliza.test`, + creditBalance: 1000.0, + includeCharacter: true, + characterName: "Mira", + characterData: mcpTestCharacter as unknown as Record, + characterSettings: mcpTestCharacter.settings as Record, + }); + console.log("Test data created"); + console.log(` API Key: ${testData.apiKey.keyPrefix}...`); + console.log(` Credits: $${testData.organization.creditBalance}`); + console.log("=".repeat(60) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\nCleaning up basic message test..."); + if (testRuntimeResult) { + await testRuntimeResult.cleanup(); + } + if (testData && connectionString) { + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), + ); + } + logTimings("Basic Message Tests", timings); }); - console.log("Test data created"); - console.log(` API Key: ${testData.apiKey.keyPrefix}...`); - console.log(` Credits: $${testData.organization.creditBalance}`); - console.log("=".repeat(60) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\nCleaning up basic message test..."); - if (testRuntimeResult) { - await testRuntimeResult.cleanup(); - } - if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + + it("should create runtime for message testing", async () => { + startTimer("runtime_creation"); + + testRuntimeResult = await createTestRuntime({ + testData, + characterId: testData.character?.id, + agentMode: "ASSISTANT" as any, + webSearchEnabled: false, + }); + + timings.runtimeCreation = endTimer("runtime_creation"); + + expect(testRuntimeResult).toBeDefined(); + expect(testRuntimeResult.runtime).toBeDefined(); + expect(testRuntimeResult.runtime.agentId).toBeDefined(); + + console.log(`\nRuntime created in ${timings.runtimeCreation}ms`); + console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); + console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); + }, 120000); + + it("should create test user with elizaOS entities", async () => { + startTimer("user_creation"); + + testUserContext = await createTestUser( + testRuntimeResult.runtime, + "BasicMessageTestUser", ); - } - logTimings("Basic Message Tests", timings); - }); - - it("should create runtime for message testing", async () => { - startTimer("runtime_creation"); - - testRuntimeResult = await createTestRuntime({ - testData, - characterId: testData.character?.id, - agentMode: "ASSISTANT" as any, - webSearchEnabled: false, - }); - timings.runtimeCreation = endTimer("runtime_creation"); - - expect(testRuntimeResult).toBeDefined(); - expect(testRuntimeResult.runtime).toBeDefined(); - expect(testRuntimeResult.runtime.agentId).toBeDefined(); - - console.log(`\nRuntime created in ${timings.runtimeCreation}ms`); - console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); - console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); - }, 120000); - - it("should create test user with elizaOS entities", async () => { - startTimer("user_creation"); - - testUserContext = await createTestUser(testRuntimeResult.runtime, "BasicMessageTestUser"); - - timings.userCreation = endTimer("user_creation"); - - expect(testUserContext).toBeDefined(); - expect(testUserContext.entityId).toBeDefined(); - expect(testUserContext.roomId).toBeDefined(); - expect(testUserContext.worldId).toBeDefined(); - - console.log(`\nTest user created in ${timings.userCreation}ms`); - console.log(` Entity ID: ${testUserContext.entityId}`); - console.log(` Room ID: ${testUserContext.roomId}`); - }, 30000); - - it("should process a simple greeting message", async () => { - startTimer("greeting_message"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Hello! How are you?", - testData, - { - timeoutMs: 60000, - }, - ); - - timings.greetingMessage = endTimer("greeting_message"); - - console.log(`\nGreeting processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(` Response: ${result.response.text?.substring(0, 100)}...`); - } - if (result.error) { - console.log(` Error: ${result.error}`); - } - - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - expect(result.error).toBeUndefined(); - }, 120000); - - it("should process a question message", async () => { - startTimer("question_message"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "What can you help me with?", - testData, - { - timeoutMs: 60000, - }, - ); - - timings.questionMessage = endTimer("question_message"); - - console.log(`\nQuestion processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(` Response: ${result.response.text?.substring(0, 100)}...`); - } - if (result.error) { - console.log(` Error: ${result.error}`); - } - - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - expect(result.error).toBeUndefined(); - }, 120000); - - it("should handle multiple messages in sequence", async () => { - const messages = [ - "Tell me about yourself.", - "What do you specialize in?", - "Thanks for the help!", - ]; - - const results: Array<{ - message: string; - didRespond: boolean; - duration: number; - }> = []; - - for (const message of messages) { - startTimer(`seq_message_${results.length}`); + timings.userCreation = endTimer("user_creation"); + + expect(testUserContext).toBeDefined(); + expect(testUserContext.entityId).toBeDefined(); + expect(testUserContext.roomId).toBeDefined(); + expect(testUserContext.worldId).toBeDefined(); + + console.log(`\nTest user created in ${timings.userCreation}ms`); + console.log(` Entity ID: ${testUserContext.entityId}`); + console.log(` Room ID: ${testUserContext.roomId}`); + }, 30000); + + it("should process a simple greeting message", async () => { + startTimer("greeting_message"); + const result = await sendTestMessage( testRuntimeResult.runtime, testUserContext, - message, + "Hello! How are you?", testData, { timeoutMs: 60000, }, ); - const duration = endTimer(`seq_message_${results.length}`); - results.push({ message, didRespond: result.didRespond, duration }); - } - - console.log("\nSequential message processing:"); - for (const r of results) { - console.log( - ` "${r.message.substring(0, 30)}..." - responded: ${r.didRespond}, ${r.duration}ms`, + + timings.greetingMessage = endTimer("greeting_message"); + + console.log(`\nGreeting processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); + if (result.response) { + console.log( + ` Response: ${result.response.text?.substring(0, 100)}...`, + ); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } + + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + expect(result.error).toBeUndefined(); + }, 120000); + + it("should process a question message", async () => { + startTimer("question_message"); + + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "What can you help me with?", + testData, + { + timeoutMs: 60000, + }, ); - } - for (const r of results) { - expect(r.didRespond).toBe(true); - } - }, 300000); -}); + timings.questionMessage = endTimer("question_message"); + + console.log(`\nQuestion processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); + if (result.response) { + console.log( + ` Response: ${result.response.text?.substring(0, 100)}...`, + ); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } + + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + expect(result.error).toBeUndefined(); + }, 120000); + + it("should handle multiple messages in sequence", async () => { + const messages = [ + "Tell me about yourself.", + "What do you specialize in?", + "Thanks for the help!", + ]; + + const results: Array<{ + message: string; + didRespond: boolean; + duration: number; + }> = []; + + for (const message of messages) { + startTimer(`seq_message_${results.length}`); + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + message, + testData, + { + timeoutMs: 60000, + }, + ); + const duration = endTimer(`seq_message_${results.length}`); + results.push({ message, didRespond: result.didRespond, duration }); + } + + console.log("\nSequential message processing:"); + for (const r of results) { + console.log( + ` "${r.message.substring(0, 30)}..." - responded: ${r.didRespond}, ${r.duration}ms`, + ); + } + + for (const r of results) { + expect(r.didRespond).toBe(true); + } + }, 300000); + }, +); diff --git a/packages/tests/runtime/integration/message-handler/mcp-tools.test.ts b/packages/tests/runtime/integration/message-handler/mcp-tools.test.ts index 73cb9d797..3f7b91dc3 100644 --- a/packages/tests/runtime/integration/message-handler/mcp-tools.test.ts +++ b/packages/tests/runtime/integration/message-handler/mcp-tools.test.ts @@ -49,405 +49,427 @@ import { const skipLiveModelSuite = !hasDatabaseUrl || !hasRuntimeModelCredentials; -describe.skipIf(skipLiveModelSuite)("MCP Plugin Loading - Production Flow", () => { - // Local test state (isolated to this describe block) - let connectionString: string; - let testData: TestDataSet; - let testRuntimeResult: TestRuntimeResult; - let testUserContext: TestUserContext; - const timings: Record = {}; - - beforeAll(async () => { - console.log("\n" + "=".repeat(60)); - console.log("SETTING UP MCP PLUGIN LOADING TEST ENVIRONMENT"); - console.log("=".repeat(60)); - - // Verify database connection - const connected = await verifyConnection(); - if (!connected) { - throw new Error( - "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", - ); - } - connectionString = getConnectionString(); - console.log("Database connected"); - - // Create test data with unique identifiers - testData = await createTestDataSet(connectionString, { - organizationName: "MCP Plugin Loading Test Org", - userName: "MCP Plugin Loading Test User", - userEmail: `mcp-loading-test-${Date.now()}@eliza.test`, - creditBalance: 1000.0, - includeCharacter: true, - characterName: "Mira", - characterData: mcpTestCharacter as unknown as Record, - characterSettings: mcpTestCharacter.settings as Record, +describe.skipIf(skipLiveModelSuite)( + "MCP Plugin Loading - Production Flow", + () => { + // Local test state (isolated to this describe block) + let connectionString: string; + let testData: TestDataSet; + let testRuntimeResult: TestRuntimeResult; + let testUserContext: TestUserContext; + const timings: Record = {}; + + beforeAll(async () => { + console.log("\n" + "=".repeat(60)); + console.log("SETTING UP MCP PLUGIN LOADING TEST ENVIRONMENT"); + console.log("=".repeat(60)); + + // Verify database connection + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", + ); + } + connectionString = getConnectionString(); + console.log("Database connected"); + + // Create test data with unique identifiers + testData = await createTestDataSet(connectionString, { + organizationName: "MCP Plugin Loading Test Org", + userName: "MCP Plugin Loading Test User", + userEmail: `mcp-loading-test-${Date.now()}@eliza.test`, + creditBalance: 1000.0, + includeCharacter: true, + characterName: "Mira", + characterData: mcpTestCharacter as unknown as Record, + characterSettings: mcpTestCharacter.settings as Record, + }); + console.log("Test data created"); + console.log(` API Key: ${testData.apiKey.keyPrefix}...`); + console.log(` Credits: $${testData.organization.creditBalance}`); + console.log("=".repeat(60) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\nCleaning up MCP plugin loading test..."); + if (testRuntimeResult) { + await testRuntimeResult.cleanup(); + } + if (testData && connectionString) { + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), + ); + } + logTimings("MCP Plugin Loading Tests", timings); }); - console.log("Test data created"); - console.log(` API Key: ${testData.apiKey.keyPrefix}...`); - console.log(` Credits: $${testData.organization.creditBalance}`); - console.log("=".repeat(60) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\nCleaning up MCP plugin loading test..."); - if (testRuntimeResult) { - await testRuntimeResult.cleanup(); - } - if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), - ); - } - logTimings("MCP Plugin Loading Tests", timings); - }); - it("should create runtime with MCP plugin using RuntimeFactory", async () => { - startTimer("runtime_creation"); + it("should create runtime with MCP plugin using RuntimeFactory", async () => { + startTimer("runtime_creation"); - testRuntimeResult = await createTestRuntime({ - testData, - characterId: testData.character?.id, - agentMode: "ASSISTANT" as any, - webSearchEnabled: false, - }); + testRuntimeResult = await createTestRuntime({ + testData, + characterId: testData.character?.id, + agentMode: "ASSISTANT" as any, + webSearchEnabled: false, + }); - timings.runtimeCreation = endTimer("runtime_creation"); + timings.runtimeCreation = endTimer("runtime_creation"); - expect(testRuntimeResult).toBeDefined(); - expect(testRuntimeResult.runtime).toBeDefined(); - expect(testRuntimeResult.runtime.agentId).toBeDefined(); + expect(testRuntimeResult).toBeDefined(); + expect(testRuntimeResult.runtime).toBeDefined(); + expect(testRuntimeResult.runtime.agentId).toBeDefined(); - console.log(`\nRuntime created in ${timings.runtimeCreation}ms`); - console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); - console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); - }, 120000); + console.log(`\nRuntime created in ${timings.runtimeCreation}ms`); + console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); + console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); + }, 120000); - it("should have MCP service available", async () => { - startTimer("mcp_service_check"); + it("should have MCP service available", async () => { + startTimer("mcp_service_check"); - const mcpService = getMcpService(testRuntimeResult.runtime); + const mcpService = getMcpService(testRuntimeResult.runtime); - timings.mcpServiceCheck = endTimer("mcp_service_check"); + timings.mcpServiceCheck = endTimer("mcp_service_check"); - expect(mcpService).toBeDefined(); - console.log(`\nMCP service found in ${timings.mcpServiceCheck}ms`); + expect(mcpService).toBeDefined(); + console.log(`\nMCP service found in ${timings.mcpServiceCheck}ms`); - if (mcpService?.getServers) { - const servers = mcpService.getServers(); - console.log(` Servers: ${servers?.length || 0}`); - } - }, 10000); + if (mcpService?.getServers) { + const servers = mcpService.getServers(); + console.log(` Servers: ${servers?.length || 0}`); + } + }, 10000); - it("should wait for MCP initialization", async () => { - startTimer("mcp_init_wait"); + it("should wait for MCP initialization", async () => { + startTimer("mcp_init_wait"); - const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); + const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); - timings.mcpInitWait = endTimer("mcp_init_wait"); + timings.mcpInitWait = endTimer("mcp_init_wait"); - expect(isReady).toBe(true); - console.log(`\nMCP initialized in ${timings.mcpInitWait}ms`); + expect(isReady).toBe(true); + console.log(`\nMCP initialized in ${timings.mcpInitWait}ms`); - const mcpService = getMcpService(testRuntimeResult.runtime); - if (mcpService?.getTools) { - const tools = mcpService.getTools(); - console.log(` Tools available: ${tools?.length || 0}`); - } - }, 30000); + const mcpService = getMcpService(testRuntimeResult.runtime); + if (mcpService?.getTools) { + const tools = mcpService.getTools(); + console.log(` Tools available: ${tools?.length || 0}`); + } + }, 30000); - it("should create test user with elizaOS entities", async () => { - startTimer("user_creation"); + it("should create test user with elizaOS entities", async () => { + startTimer("user_creation"); - testUserContext = await createTestUser(testRuntimeResult.runtime, "MCPLoadingTestUser"); + testUserContext = await createTestUser( + testRuntimeResult.runtime, + "MCPLoadingTestUser", + ); - timings.userCreation = endTimer("user_creation"); + timings.userCreation = endTimer("user_creation"); - expect(testUserContext).toBeDefined(); - expect(testUserContext.entityId).toBeDefined(); - expect(testUserContext.roomId).toBeDefined(); - expect(testUserContext.worldId).toBeDefined(); + expect(testUserContext).toBeDefined(); + expect(testUserContext.entityId).toBeDefined(); + expect(testUserContext.roomId).toBeDefined(); + expect(testUserContext.worldId).toBeDefined(); - console.log(`\nTest user created in ${timings.userCreation}ms`); - console.log(` Entity ID: ${testUserContext.entityId}`); - console.log(` Room ID: ${testUserContext.roomId}`); - }, 30000); + console.log(`\nTest user created in ${timings.userCreation}ms`); + console.log(` Entity ID: ${testUserContext.entityId}`); + console.log(` Room ID: ${testUserContext.roomId}`); + }, 30000); - it("should process a message through the runtime", async () => { - startTimer("message_processing"); + it("should process a message through the runtime", async () => { + startTimer("message_processing"); - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Hello! What can you help me with?", - testData, - { - timeoutMs: 60000, - }, - ); + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "Hello! What can you help me with?", + testData, + { + timeoutMs: 60000, + }, + ); - timings.messageProcessing = endTimer("message_processing"); + timings.messageProcessing = endTimer("message_processing"); - console.log(`\nMessage processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(` Response: ${result.response.text?.substring(0, 100)}...`); - } - if (result.error) { - console.log(` Error: ${result.error}`); - } + console.log(`\nMessage processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); + if (result.response) { + console.log( + ` Response: ${result.response.text?.substring(0, 100)}...`, + ); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - expect(result.error).toBeUndefined(); - }, 120000); -}); + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + expect(result.error).toBeUndefined(); + }, 120000); + }, +); // ============================================================================ // MCP Assistant Trending Tokens Tests // ============================================================================ -describe.skipIf(skipLiveModelSuite)("MCP Assistant - Trending Tokens Query", () => { - // Local test state (isolated to this describe block) - let connectionString: string; - let testData: TestDataSet; - let testRuntimeResult: TestRuntimeResult; - let testUserContext: TestUserContext; - const timings: Record = {}; - - beforeAll(async () => { - console.log("\n" + "=".repeat(60)); - console.log("SETTING UP MCP TRENDING TOKENS TEST ENVIRONMENT"); - console.log("=".repeat(60)); - - // Check debug tracing - const debugEnabled = isDebugTracingEnabled(); - console.log(`\nDebug Tracing: ${debugEnabled ? "ENABLED" : "DISABLED"}`); - if (!debugEnabled) { - console.log(" Set DEBUG_TRACING=true in .env to enable debug output"); - } +describe.skipIf(skipLiveModelSuite)( + "MCP Assistant - Trending Tokens Query", + () => { + // Local test state (isolated to this describe block) + let connectionString: string; + let testData: TestDataSet; + let testRuntimeResult: TestRuntimeResult; + let testUserContext: TestUserContext; + const timings: Record = {}; + + beforeAll(async () => { + console.log("\n" + "=".repeat(60)); + console.log("SETTING UP MCP TRENDING TOKENS TEST ENVIRONMENT"); + console.log("=".repeat(60)); + + // Check debug tracing + const debugEnabled = isDebugTracingEnabled(); + console.log(`\nDebug Tracing: ${debugEnabled ? "ENABLED" : "DISABLED"}`); + if (!debugEnabled) { + console.log(" Set DEBUG_TRACING=true in .env to enable debug output"); + } - // Verify database connection - const connected = await verifyConnection(); - if (!connected) { - throw new Error( - "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", - ); - } - connectionString = getConnectionString(); - console.log("Database connected"); - - // Create test data with unique identifiers - testData = await createTestDataSet(connectionString, { - organizationName: "MCP Trending Test Org", - userName: "MCP Trending Test User", - userEmail: `mcp-trending-test-${Date.now()}@eliza.test`, - creditBalance: 1000.0, - includeCharacter: true, - characterName: "Mira", - characterData: mcpTestCharacter as unknown as Record, - characterSettings: mcpTestCharacter.settings as Record, + // Verify database connection + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", + ); + } + connectionString = getConnectionString(); + console.log("Database connected"); + + // Create test data with unique identifiers + testData = await createTestDataSet(connectionString, { + organizationName: "MCP Trending Test Org", + userName: "MCP Trending Test User", + userEmail: `mcp-trending-test-${Date.now()}@eliza.test`, + creditBalance: 1000.0, + includeCharacter: true, + characterName: "Mira", + characterData: mcpTestCharacter as unknown as Record, + characterSettings: mcpTestCharacter.settings as Record, + }); + console.log("Test data created"); + console.log(` API Key: ${testData.apiKey.keyPrefix}...`); + console.log(` Credits: $${testData.organization.creditBalance}`); + console.log("=".repeat(60) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\nCleaning up MCP trending tokens test..."); + if (testRuntimeResult) { + await testRuntimeResult.cleanup(); + } + if (testData && connectionString) { + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), + ); + } + logTimings("MCP Trending Tokens Tests", timings); }); - console.log("Test data created"); - console.log(` API Key: ${testData.apiKey.keyPrefix}...`); - console.log(` Credits: $${testData.organization.creditBalance}`); - console.log("=".repeat(60) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\nCleaning up MCP trending tokens test..."); - if (testRuntimeResult) { - await testRuntimeResult.cleanup(); - } - if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), - ); - } - logTimings("MCP Trending Tokens Tests", timings); - }); - it("should create runtime with MCP plugin", async () => { - startTimer("runtime_creation"); + it("should create runtime with MCP plugin", async () => { + startTimer("runtime_creation"); - testRuntimeResult = await createTestRuntime({ - testData, - characterId: testData.character?.id, - agentMode: "ASSISTANT" as any, - webSearchEnabled: false, - }); + testRuntimeResult = await createTestRuntime({ + testData, + characterId: testData.character?.id, + agentMode: "ASSISTANT" as any, + webSearchEnabled: false, + }); - timings.runtimeCreation = endTimer("runtime_creation"); + timings.runtimeCreation = endTimer("runtime_creation"); - expect(testRuntimeResult).toBeDefined(); - expect(testRuntimeResult.runtime).toBeDefined(); - expect(testRuntimeResult.runtime.agentId).toBeDefined(); + expect(testRuntimeResult).toBeDefined(); + expect(testRuntimeResult.runtime).toBeDefined(); + expect(testRuntimeResult.runtime.agentId).toBeDefined(); - console.log(`\nRuntime created in ${timings.runtimeCreation}ms`); - console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); - console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); - }, 120000); + console.log(`\nRuntime created in ${timings.runtimeCreation}ms`); + console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); + console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); + }, 120000); - it("should have MCP service available", async () => { - startTimer("mcp_service_check"); + it("should have MCP service available", async () => { + startTimer("mcp_service_check"); - const mcpService = getMcpService(testRuntimeResult.runtime); + const mcpService = getMcpService(testRuntimeResult.runtime); - timings.mcpServiceCheck = endTimer("mcp_service_check"); + timings.mcpServiceCheck = endTimer("mcp_service_check"); - expect(mcpService).toBeDefined(); - console.log(`\nMCP service found in ${timings.mcpServiceCheck}ms`); + expect(mcpService).toBeDefined(); + console.log(`\nMCP service found in ${timings.mcpServiceCheck}ms`); - if (mcpService?.getServers) { - const servers = mcpService.getServers(); - console.log(` Servers: ${servers?.length || 0}`); - } - }, 10000); + if (mcpService?.getServers) { + const servers = mcpService.getServers(); + console.log(` Servers: ${servers?.length || 0}`); + } + }, 10000); - it("should wait for MCP initialization", async () => { - startTimer("mcp_init_wait"); + it("should wait for MCP initialization", async () => { + startTimer("mcp_init_wait"); - const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); + const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); - timings.mcpInitWait = endTimer("mcp_init_wait"); + timings.mcpInitWait = endTimer("mcp_init_wait"); - expect(isReady).toBe(true); - console.log(`\nMCP initialized in ${timings.mcpInitWait}ms`); + expect(isReady).toBe(true); + console.log(`\nMCP initialized in ${timings.mcpInitWait}ms`); - const mcpService = getMcpService(testRuntimeResult.runtime); - if (mcpService?.getTools) { - const tools = mcpService.getTools(); - console.log(` Tools available: ${tools?.length || 0}`); - if (tools && tools.length > 0) { - console.log( - ` Sample tools: ${tools - .slice(0, 5) - .map((t: any) => t.name || t) - .join(", ")}`, - ); + const mcpService = getMcpService(testRuntimeResult.runtime); + if (mcpService?.getTools) { + const tools = mcpService.getTools(); + console.log(` Tools available: ${tools?.length || 0}`); + if (tools && tools.length > 0) { + console.log( + ` Sample tools: ${tools + .slice(0, 5) + .map((t: any) => t.name || t) + .join(", ")}`, + ); + } } - } - }, 30000); + }, 30000); - it("should create test user with elizaOS entities", async () => { - startTimer("user_creation"); + it("should create test user with elizaOS entities", async () => { + startTimer("user_creation"); - testUserContext = await createTestUser(testRuntimeResult.runtime, "TrendingTestUser"); + testUserContext = await createTestUser( + testRuntimeResult.runtime, + "TrendingTestUser", + ); - timings.userCreation = endTimer("user_creation"); + timings.userCreation = endTimer("user_creation"); - expect(testUserContext).toBeDefined(); - expect(testUserContext.entityId).toBeDefined(); - expect(testUserContext.roomId).toBeDefined(); - expect(testUserContext.worldId).toBeDefined(); + expect(testUserContext).toBeDefined(); + expect(testUserContext.entityId).toBeDefined(); + expect(testUserContext.roomId).toBeDefined(); + expect(testUserContext.worldId).toBeDefined(); - console.log(`\nTest user created in ${timings.userCreation}ms`); - console.log(` Entity ID: ${testUserContext.entityId}`); - console.log(` Room ID: ${testUserContext.roomId}`); - }, 30000); + console.log(`\nTest user created in ${timings.userCreation}ms`); + console.log(` Entity ID: ${testUserContext.entityId}`); + console.log(` Room ID: ${testUserContext.roomId}`); + }, 30000); - it("should process 'can you get trending tokens' with debug tracing", async () => { - const debugEnabled = isDebugTracingEnabled(); - console.log(`\nProcessing message with debug tracing ${debugEnabled ? "ENABLED" : "DISABLED"}`); - - // Clear any existing traces - clearDebugTraces(); - - startTimer("trending_query"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Can you get trending tokens?", - testData, - { - timeoutMs: 120000, - debug: { - enabled: debugEnabled, - renderView: "full", - storeTrace: true, + it("should process 'can you get trending tokens' with debug tracing", async () => { + const debugEnabled = isDebugTracingEnabled(); + console.log( + `\nProcessing message with debug tracing ${debugEnabled ? "ENABLED" : "DISABLED"}`, + ); + + // Clear any existing traces + clearDebugTraces(); + + startTimer("trending_query"); + + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "Can you get trending tokens?", + testData, + { + timeoutMs: 120000, + debug: { + enabled: debugEnabled, + renderView: "full", + storeTrace: true, + }, }, - }, - ); + ); - timings.trendingQuery = endTimer("trending_query"); + timings.trendingQuery = endTimer("trending_query"); - console.log(`\nMessage processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); + console.log(`\nMessage processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(`\nResponse:`); - console.log(` ${result.response.text?.substring(0, 500)}...`); - } + if (result.response) { + console.log(`\nResponse:`); + console.log(` ${result.response.text?.substring(0, 500)}...`); + } - if (result.error) { - console.log(`\nError: ${result.error}`); - } + if (result.error) { + console.log(`\nError: ${result.error}`); + } - // Output debug trace if available - if (debugEnabled) { - const trace = getLatestDebugTrace(); - if (trace) { - console.log("\n" + "=".repeat(60)); - console.log("DEBUG TRACE"); - console.log("=".repeat(60)); - - const markdown = renderDebugTrace(trace, "full"); - console.log(markdown); - - console.log("\n" + "=".repeat(60)); - console.log("TRACE SUMMARY"); - console.log("=".repeat(60)); - console.log(` Run ID: ${trace.runId}`); - console.log(` Status: ${trace.status}`); - console.log(` Agent Mode: ${trace.agentMode}`); - console.log(` Steps: ${trace.steps?.length || 0}`); - console.log(` Duration: ${trace.endedAt ? trace.endedAt - trace.startedAt : "N/A"}ms`); - - const fail = trace.failures[0]; - if (fail) { - console.log(`\nFailure detected:`); - console.log(` Type: ${fail.type}`); - console.log(` Message: ${fail.message}`); - console.log(` Step: ${fail.stepIndex}`); + // Output debug trace if available + if (debugEnabled) { + const trace = getLatestDebugTrace(); + if (trace) { + console.log("\n" + "=".repeat(60)); + console.log("DEBUG TRACE"); + console.log("=".repeat(60)); + + const markdown = renderDebugTrace(trace, "full"); + console.log(markdown); + + console.log("\n" + "=".repeat(60)); + console.log("TRACE SUMMARY"); + console.log("=".repeat(60)); + console.log(` Run ID: ${trace.runId}`); + console.log(` Status: ${trace.status}`); + console.log(` Agent Mode: ${trace.agentMode}`); + console.log(` Steps: ${trace.steps?.length || 0}`); + console.log( + ` Duration: ${trace.endedAt ? trace.endedAt - trace.startedAt : "N/A"}ms`, + ); + + const fail = trace.failures[0]; + if (fail) { + console.log(`\nFailure detected:`); + console.log(` Type: ${fail.type}`); + console.log(` Message: ${fail.message}`); + console.log(` Step: ${fail.stepIndex}`); + } + } else { + console.log( + "\nNo debug trace captured (trace may not have been generated)", + ); } - } else { - console.log("\nNo debug trace captured (trace may not have been generated)"); } - } - // The test passes if we got a response OR we got a specific MCP-related interaction - expect(result.didRespond || result.error === undefined).toBe(true); - }, 180000); - - it("should also handle a simpler greeting to verify basic functionality", async () => { - startTimer("greeting_test"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Hello! What can you help me with?", - testData, - { - timeoutMs: 60000, - }, - ); + // The test passes if we got a response OR we got a specific MCP-related interaction + expect(result.didRespond || result.error === undefined).toBe(true); + }, 180000); - timings.greetingTest = endTimer("greeting_test"); + it("should also handle a simpler greeting to verify basic functionality", async () => { + startTimer("greeting_test"); - console.log(`\nGreeting processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(` Response: ${result.response.text?.substring(0, 200)}...`); - } - if (result.error) { - console.log(` Error: ${result.error}`); - } + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "Hello! What can you help me with?", + testData, + { + timeoutMs: 60000, + }, + ); - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - }, 120000); -}); + timings.greetingTest = endTimer("greeting_test"); + + console.log(`\nGreeting processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); + if (result.response) { + console.log( + ` Response: ${result.response.text?.substring(0, 200)}...`, + ); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } + + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + }, 120000); + }, +); // ============================================================================ // Debug Tracing Status (Informational) @@ -461,7 +483,9 @@ describe.skipIf(!hasDatabaseUrl)("Debug Tracing Status", () => { console.log("DEBUG TRACING STATUS"); console.log("=".repeat(60)); console.log(`Enabled: ${debugEnabled ? "YES" : "NO"}`); - console.log(`Environment: DEBUG_TRACING=${process.env.DEBUG_TRACING || "not set"}`); + console.log( + `Environment: DEBUG_TRACING=${process.env.DEBUG_TRACING || "not set"}`, + ); console.log("=".repeat(60)); if (!debugEnabled) { diff --git a/packages/tests/runtime/integration/performance/embedding-init.test.ts b/packages/tests/runtime/integration/performance/embedding-init.test.ts index 40e0da6f8..171440317 100644 --- a/packages/tests/runtime/integration/performance/embedding-init.test.ts +++ b/packages/tests/runtime/integration/performance/embedding-init.test.ts @@ -54,7 +54,9 @@ describe.skipIf(!hasDatabaseUrl)("Embedding Initialization Performance", () => { await rt.cleanup().catch(() => {}); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch(() => {}); + await cleanupTestData(connectionString, testData.organization.id).catch( + () => {}, + ); } }); @@ -128,13 +130,19 @@ describe.skipIf(!hasDatabaseUrl)("Embedding Initialization Performance", () => { const warmVariance = Math.abs(secondDuration - thirdDuration); console.log(`\nAverage warm: ${avgWarm.toFixed(0)}ms`); - console.log(`Savings: ${savings.toFixed(0)}ms (${savingsPercent.toFixed(1)}%)`); + console.log( + `Savings: ${savings.toFixed(0)}ms (${savingsPercent.toFixed(1)}%)`, + ); console.log(`Warm variance: ${warmVariance.toFixed(0)}ms`); if (savings > 300) { - console.log(`\n✅ SUCCESS: Repeated starts are ${savings.toFixed(0)}ms faster`); + console.log( + `\n✅ SUCCESS: Repeated starts are ${savings.toFixed(0)}ms faster`, + ); } else if (warmVariance < 500) { - console.log(`\n✅ SUCCESS: Repeated starts are stable even without a large speedup`); + console.log( + `\n✅ SUCCESS: Repeated starts are stable even without a large speedup`, + ); } else { console.log(`\n⚠️ WARNING: Repeated starts varied more than expected`); } @@ -149,7 +157,9 @@ describe.skipIf(!hasDatabaseUrl)("Embedding Initialization Performance", () => { // even when there is no dramatic "warm" speedup from embedding setup. // CI runners have high timing variance (GC, JIT, noisy neighbors), so // allow up to 3x or +200 ms headroom to avoid flaky failures. - expect(avgWarm).toBeLessThan(Math.max(firstDuration * 3, firstDuration + 200)); + expect(avgWarm).toBeLessThan( + Math.max(firstDuration * 3, firstDuration + 200), + ); expect(warmVariance).toBeLessThan(500); }, { timeout: 180000 }, diff --git a/packages/tests/runtime/integration/performance/runtime-creation.test.ts b/packages/tests/runtime/integration/performance/runtime-creation.test.ts index e3e80d673..95c16529a 100644 --- a/packages/tests/runtime/integration/performance/runtime-creation.test.ts +++ b/packages/tests/runtime/integration/performance/runtime-creation.test.ts @@ -72,8 +72,8 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Creation Performance", () => { await rt.cleanup(); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } }); @@ -142,7 +142,9 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Creation Performance", () => { // Target: <5000ms for ASSISTANT runtime (includes MCP init) if (avg > 5000) { - console.warn(`ASSISTANT runtime avg (${avg.toFixed(0)}ms) exceeds 5s target`); + console.warn( + `ASSISTANT runtime avg (${avg.toFixed(0)}ms) exceeds 5s target`, + ); } expect(avg).toBeGreaterThan(0); @@ -199,8 +201,8 @@ describe.skipIf(!hasDatabaseUrl)("Database Query Performance", () => { await testRuntime.cleanup(); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } }); @@ -291,7 +293,9 @@ describe.skipIf(!hasDatabaseUrl)("Database Query Performance", () => { } const avg = times.reduce((a, b) => a + b, 0) / times.length; - console.log(`\nMemory Retrieval (10 items): avg ${avg.toFixed(1)}ms (${runs} runs)`); + console.log( + `\nMemory Retrieval (10 items): avg ${avg.toFixed(1)}ms (${runs} runs)`, + ); expect(avg).toBeGreaterThan(0); expect(avg).toBeLessThan(200); // Retrieval should be very fast @@ -340,8 +344,8 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Caching Performance", () => { await rt.cleanup(); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } }); @@ -368,12 +372,18 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Caching Performance", () => { runtimes.push(runtime2); console.log("\nCache Performance:"); - console.log(` Cold start (cache miss): ${coldResult.durationMs.toFixed(1)}ms`); - console.log(` Warm start (cache hit): ${warmResult.durationMs.toFixed(1)}ms`); + console.log( + ` Cold start (cache miss): ${coldResult.durationMs.toFixed(1)}ms`, + ); + console.log( + ` Warm start (cache hit): ${warmResult.durationMs.toFixed(1)}ms`, + ); if (warmResult.durationMs < coldResult.durationMs) { const speedup = - ((coldResult.durationMs - warmResult.durationMs) / coldResult.durationMs) * 100; + ((coldResult.durationMs - warmResult.durationMs) / + coldResult.durationMs) * + 100; console.log(` Speedup: ${speedup.toFixed(1)}%`); } diff --git a/packages/tests/runtime/integration/runtime-factory/assistant-mode.test.ts b/packages/tests/runtime/integration/runtime-factory/assistant-mode.test.ts index 7fee1c888..d9eed93a3 100644 --- a/packages/tests/runtime/integration/runtime-factory/assistant-mode.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/assistant-mode.test.ts @@ -50,205 +50,222 @@ const skipLiveModelSuite = !hasDatabaseUrl || !hasRuntimeModelCredentials; // ASSISTANT Mode with MCP Tests // ============================================================================ -describe.skipIf(skipLiveModelSuite)("RuntimeFactory - ASSISTANT Mode (MCP)", () => { - let runtime: TestRuntime; - let testUser: TestUserContext; - - beforeAll(async () => { - console.log("\n" + "=".repeat(60)); - console.log("SETTING UP ASSISTANT MODE (MCP) TEST ENVIRONMENT"); - console.log("=".repeat(60)); - - // Verify database connection - const connected = await verifyConnection(); - if (!connected) { - throw new Error( - "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", - ); - } - connectionString = getConnectionString(); - console.log("Database connected"); - - // Create test data with unique identifiers - testData = await createTestDataSet(connectionString, { - organizationName: "ASSISTANT MCP Test Org", - userName: "ASSISTANT MCP Test User", - userEmail: `assistant-mcp-test-${Date.now()}@eliza.test`, - creditBalance: 1000.0, - includeCharacter: true, - characterName: "Mira", - characterData: mcpTestCharacter as unknown as Record, - characterSettings: mcpTestCharacter.settings as Record, +describe.skipIf(skipLiveModelSuite)( + "RuntimeFactory - ASSISTANT Mode (MCP)", + () => { + let runtime: TestRuntime; + let testUser: TestUserContext; + + beforeAll(async () => { + console.log("\n" + "=".repeat(60)); + console.log("SETTING UP ASSISTANT MODE (MCP) TEST ENVIRONMENT"); + console.log("=".repeat(60)); + + // Verify database connection + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", + ); + } + connectionString = getConnectionString(); + console.log("Database connected"); + + // Create test data with unique identifiers + testData = await createTestDataSet(connectionString, { + organizationName: "ASSISTANT MCP Test Org", + userName: "ASSISTANT MCP Test User", + userEmail: `assistant-mcp-test-${Date.now()}@eliza.test`, + creditBalance: 1000.0, + includeCharacter: true, + characterName: "Mira", + characterData: mcpTestCharacter as unknown as Record, + characterSettings: mcpTestCharacter.settings as Record, + }); + console.log("Test data created"); + console.log(` API Key: ${testData.apiKey.keyPrefix}...`); + console.log(` Credits: $${testData.organization.creditBalance}`); + console.log("=".repeat(60) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\nCleaning up ASSISTANT Mode (MCP) test..."); + if (runtime) { + await invalidateRuntime(runtime.agentId as string).catch((err) => + console.warn(`Runtime cleanup warning: ${err}`), + ); + } + if (testData && connectionString) { + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), + ); + } + logTimings("ASSISTANT Mode (MCP) Tests", timings); }); - console.log("Test data created"); - console.log(` API Key: ${testData.apiKey.keyPrefix}...`); - console.log(` Credits: $${testData.organization.creditBalance}`); - console.log("=".repeat(60) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\nCleaning up ASSISTANT Mode (MCP) test..."); - if (runtime) { - await invalidateRuntime(runtime.agentId as string).catch((err) => - console.warn(`Runtime cleanup warning: ${err}`), - ); - } - if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), - ); - } - logTimings("ASSISTANT Mode (MCP) Tests", timings); - }); - it("should create runtime in ASSISTANT mode with MCP", async () => { - startTimer("assistant_runtime_create"); + it("should create runtime in ASSISTANT mode with MCP", async () => { + startTimer("assistant_runtime_create"); - const userContext = buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - characterId: testData.character?.id, - webSearchEnabled: false, // Isolate MCP testing - }); + const userContext = buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + characterId: testData.character?.id, + webSearchEnabled: false, // Isolate MCP testing + }); - runtime = await runtimeFactory.createRuntimeForUser(userContext); + runtime = await runtimeFactory.createRuntimeForUser(userContext); - timings.assistantRuntimeCreate = endTimer("assistant_runtime_create"); + timings.assistantRuntimeCreate = endTimer("assistant_runtime_create"); - expect(runtime).toBeDefined(); - expect(runtime.character?.name).toBe("Mira"); - console.log(`\nASSISTANT runtime created in ${timings.assistantRuntimeCreate}ms`); - }, 60000); + expect(runtime).toBeDefined(); + expect(runtime.character?.name).toBe("Mira"); + console.log( + `\nASSISTANT runtime created in ${timings.assistantRuntimeCreate}ms`, + ); + }, 60000); - it("should have MCP service initialized", async () => { - startTimer("mcp_init"); - const isReady = await waitForMcpReady(runtime, 15000); - timings.mcpInit = endTimer("mcp_init"); + it("should have MCP service initialized", async () => { + startTimer("mcp_init"); + const isReady = await waitForMcpReady(runtime, 15000); + timings.mcpInit = endTimer("mcp_init"); - expect(isReady).toBe(true); + expect(isReady).toBe(true); - const mcpService = getMcpService(runtime); - expect(mcpService).toBeDefined(); + const mcpService = getMcpService(runtime); + expect(mcpService).toBeDefined(); - const tools = mcpService?.getTools?.(); - console.log(`\nMCP ready in ${timings.mcpInit}ms`); - console.log(` Tools: ${tools?.length || 0}`); - }, 30000); + const tools = mcpService?.getTools?.(); + console.log(`\nMCP ready in ${timings.mcpInit}ms`); + console.log(` Tools: ${tools?.length || 0}`); + }, 30000); - it("should process message with MCP tools available", async () => { - testUser = await createTestUser(runtime, "AssistantTestUser"); + it("should process message with MCP tools available", async () => { + testUser = await createTestUser(runtime, "AssistantTestUser"); - startTimer("assistant_message"); - const result = await sendTestMessage( - runtime, - testUser, - "What's the current price of Bitcoin?", - testData, - { timeoutMs: 120000 }, - ); - timings.assistantMessage = endTimer("assistant_message"); + startTimer("assistant_message"); + const result = await sendTestMessage( + runtime, + testUser, + "What's the current price of Bitcoin?", + testData, + { timeoutMs: 120000 }, + ); + timings.assistantMessage = endTimer("assistant_message"); - expect(result.didRespond).toBe(true); - console.log(`\nASSISTANT message processed in ${timings.assistantMessage}ms`); - console.log(` Response: ${result.response?.text?.substring(0, 80)}...`); - }, 180000); + expect(result.didRespond).toBe(true); + console.log( + `\nASSISTANT message processed in ${timings.assistantMessage}ms`, + ); + console.log(` Response: ${result.response?.text?.substring(0, 80)}...`); + }, 180000); - it("should cleanup ASSISTANT runtime", async () => { - await invalidateRuntime(runtime.agentId as string); - }); -}); + it("should cleanup ASSISTANT runtime", async () => { + await invalidateRuntime(runtime.agentId as string); + }); + }, +); // ============================================================================ // ASSISTANT Mode with Web Search Tests // ============================================================================ -describe.skipIf(skipLiveModelSuite)("RuntimeFactory - ASSISTANT Mode (Web Search)", () => { - let runtime: TestRuntime; - let testUser: TestUserContext; - let localConnectionString: string; - let localTestData: TestDataSet; - const localTimings: Record = {}; - - beforeAll(async () => { - console.log("\n" + "=".repeat(60)); - console.log("SETTING UP ASSISTANT MODE (WEB SEARCH) TEST ENVIRONMENT"); - console.log("=".repeat(60)); - - // Verify database connection - const connected = await verifyConnection(); - if (!connected) { - throw new Error( - "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", - ); - } - localConnectionString = getConnectionString(); - console.log("Database connected"); - - // Create test data with unique identifiers - localTestData = await createTestDataSet(localConnectionString, { - organizationName: "ASSISTANT WebSearch Test Org", - userName: "ASSISTANT WebSearch Test User", - userEmail: `assistant-websearch-test-${Date.now()}@eliza.test`, - creditBalance: 1000.0, - includeCharacter: true, - characterName: "Mira", - characterData: mcpTestCharacter as unknown as Record, - characterSettings: mcpTestCharacter.settings as Record, +describe.skipIf(skipLiveModelSuite)( + "RuntimeFactory - ASSISTANT Mode (Web Search)", + () => { + let runtime: TestRuntime; + let testUser: TestUserContext; + let localConnectionString: string; + let localTestData: TestDataSet; + const localTimings: Record = {}; + + beforeAll(async () => { + console.log("\n" + "=".repeat(60)); + console.log("SETTING UP ASSISTANT MODE (WEB SEARCH) TEST ENVIRONMENT"); + console.log("=".repeat(60)); + + // Verify database connection + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", + ); + } + localConnectionString = getConnectionString(); + console.log("Database connected"); + + // Create test data with unique identifiers + localTestData = await createTestDataSet(localConnectionString, { + organizationName: "ASSISTANT WebSearch Test Org", + userName: "ASSISTANT WebSearch Test User", + userEmail: `assistant-websearch-test-${Date.now()}@eliza.test`, + creditBalance: 1000.0, + includeCharacter: true, + characterName: "Mira", + characterData: mcpTestCharacter as unknown as Record, + characterSettings: mcpTestCharacter.settings as Record, + }); + console.log("Test data created"); + console.log(` API Key: ${localTestData.apiKey.keyPrefix}...`); + console.log(` Credits: $${localTestData.organization.creditBalance}`); + console.log("=".repeat(60) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\nCleaning up ASSISTANT Mode (Web Search) test..."); + if (runtime) { + await invalidateRuntime(runtime.agentId as string).catch((err) => + console.warn(`Runtime cleanup warning: ${err}`), + ); + } + if (localTestData && localConnectionString) { + await cleanupTestData( + localConnectionString, + localTestData.organization.id, + ).catch((err) => console.warn(`Data cleanup warning: ${err}`)); + } + logTimings("ASSISTANT Mode (Web Search) Tests", localTimings); }); - console.log("Test data created"); - console.log(` API Key: ${localTestData.apiKey.keyPrefix}...`); - console.log(` Credits: $${localTestData.organization.creditBalance}`); - console.log("=".repeat(60) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\nCleaning up ASSISTANT Mode (Web Search) test..."); - if (runtime) { - await invalidateRuntime(runtime.agentId as string).catch((err) => - console.warn(`Runtime cleanup warning: ${err}`), - ); - } - if (localTestData && localConnectionString) { - await cleanupTestData(localConnectionString, localTestData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), - ); - } - logTimings("ASSISTANT Mode (Web Search) Tests", localTimings); - }); - - it("should create runtime with web search enabled", async () => { - startTimer("websearch_runtime_create"); - const userContext = buildUserContext(localTestData, { - agentMode: AgentMode.ASSISTANT, - webSearchEnabled: true, - }); + it("should create runtime with web search enabled", async () => { + startTimer("websearch_runtime_create"); - runtime = await runtimeFactory.createRuntimeForUser(userContext); + const userContext = buildUserContext(localTestData, { + agentMode: AgentMode.ASSISTANT, + webSearchEnabled: true, + }); - localTimings.webSearchRuntimeCreate = endTimer("websearch_runtime_create"); + runtime = await runtimeFactory.createRuntimeForUser(userContext); - expect(runtime).toBeDefined(); - console.log(`\nWeb Search runtime created in ${localTimings.webSearchRuntimeCreate}ms`); - }, 60000); - - it("should process message with web search", async () => { - testUser = await createTestUser(runtime, "WebSearchTestUser"); + localTimings.webSearchRuntimeCreate = endTimer( + "websearch_runtime_create", + ); - startTimer("websearch_message"); - const result = await sendTestMessage( - runtime, - testUser, - "What is the latest news about AI?", - localTestData, - { timeoutMs: 120000 }, - ); - localTimings.webSearchMessage = endTimer("websearch_message"); + expect(runtime).toBeDefined(); + console.log( + `\nWeb Search runtime created in ${localTimings.webSearchRuntimeCreate}ms`, + ); + }, 60000); + + it("should process message with web search", async () => { + testUser = await createTestUser(runtime, "WebSearchTestUser"); + + startTimer("websearch_message"); + const result = await sendTestMessage( + runtime, + testUser, + "What is the latest news about AI?", + localTestData, + { timeoutMs: 120000 }, + ); + localTimings.webSearchMessage = endTimer("websearch_message"); - expect(result.didRespond).toBe(true); - console.log(`\nWeb Search message processed in ${localTimings.webSearchMessage}ms`); - }, 180000); + expect(result.didRespond).toBe(true); + console.log( + `\nWeb Search message processed in ${localTimings.webSearchMessage}ms`, + ); + }, 180000); - it("should cleanup web search runtime", async () => { - await invalidateRuntime(runtime.agentId as string); - }); -}); + it("should cleanup web search runtime", async () => { + await invalidateRuntime(runtime.agentId as string); + }); + }, +); diff --git a/packages/tests/runtime/integration/runtime-factory/build-mode.test.ts b/packages/tests/runtime/integration/runtime-factory/build-mode.test.ts index a1ce4f11c..59fd6c4a7 100644 --- a/packages/tests/runtime/integration/runtime-factory/build-mode.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/build-mode.test.ts @@ -87,8 +87,8 @@ describe.skipIf(skipLiveModelSuite)("RuntimeFactory - BUILD Mode", () => { ); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } logTimings("BUILD Mode Tests", timings); diff --git a/packages/tests/runtime/integration/runtime-factory/caching.test.ts b/packages/tests/runtime/integration/runtime-factory/caching.test.ts index 03310d605..cca5928f7 100644 --- a/packages/tests/runtime/integration/runtime-factory/caching.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/caching.test.ts @@ -76,8 +76,8 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { afterAll(async () => { console.log("\nCleaning up caching test..."); if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } logTimings("Caching Tests", timings); @@ -123,7 +123,9 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { const stats = getRuntimeCacheStats(); expect(stats).toBeDefined(); expect(stats.runtime).toBeDefined(); - console.log(`\nCache stats: size=${stats.runtime.size}/${stats.runtime.maxSize}`); + console.log( + `\nCache stats: size=${stats.runtime.size}/${stats.runtime.maxSize}`, + ); }); }); @@ -131,80 +133,84 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { // Performance Benchmarks // ============================================================================ -describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Performance Benchmarks", () => { - let localConnectionString: string; - let localTestData: TestDataSet; - const localTimings: Record = {}; - - beforeAll(async () => { - console.log("\n" + "=".repeat(60)); - console.log("SETTING UP PERFORMANCE BENCHMARK ENVIRONMENT"); - console.log("=".repeat(60)); - - // Verify database connection - const connected = await verifyConnection(); - if (!connected) { - throw new Error( - "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", - ); - } - localConnectionString = getConnectionString(); - console.log("Database connected"); - - // Create test data with unique identifiers - localTestData = await createTestDataSet(localConnectionString, { - organizationName: "Performance Benchmark Test Org", - userName: "Performance Benchmark Test User", - userEmail: `perf-benchmark-test-${Date.now()}@eliza.test`, - creditBalance: 1000.0, - includeCharacter: true, - characterName: "Mira", - characterData: mcpTestCharacter as unknown as Record, - characterSettings: mcpTestCharacter.settings as Record, - }); - console.log("Test data created"); - console.log(` API Key: ${localTestData.apiKey.keyPrefix}...`); - console.log(` Credits: $${localTestData.organization.creditBalance}`); - console.log("=".repeat(60) + "\n"); - }, 60000); - - afterAll(async () => { - console.log("\nCleaning up performance benchmark test..."); - if (localTestData && localConnectionString) { - await cleanupTestData(localConnectionString, localTestData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), - ); - } - logTimings("Performance Benchmark Tests", localTimings); - }); - - it("should benchmark runtime creation times", async () => { - const modes = [AgentMode.CHAT, AgentMode.ASSISTANT, AgentMode.BUILD]; - const benchmarks: Record = {}; - - for (const mode of modes) { - // Ensure clean state - const userContext = buildUserContext(localTestData, { - agentMode: mode, - webSearchEnabled: false, +describe.skipIf(!hasDatabaseUrl)( + "RuntimeFactory - Performance Benchmarks", + () => { + let localConnectionString: string; + let localTestData: TestDataSet; + const localTimings: Record = {}; + + beforeAll(async () => { + console.log("\n" + "=".repeat(60)); + console.log("SETTING UP PERFORMANCE BENCHMARK ENVIRONMENT"); + console.log("=".repeat(60)); + + // Verify database connection + const connected = await verifyConnection(); + if (!connected) { + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set and server is running.", + ); + } + localConnectionString = getConnectionString(); + console.log("Database connected"); + + // Create test data with unique identifiers + localTestData = await createTestDataSet(localConnectionString, { + organizationName: "Performance Benchmark Test Org", + userName: "Performance Benchmark Test User", + userEmail: `perf-benchmark-test-${Date.now()}@eliza.test`, + creditBalance: 1000.0, + includeCharacter: true, + characterName: "Mira", + characterData: mcpTestCharacter as unknown as Record, + characterSettings: mcpTestCharacter.settings as Record, }); + console.log("Test data created"); + console.log(` API Key: ${localTestData.apiKey.keyPrefix}...`); + console.log(` Credits: $${localTestData.organization.creditBalance}`); + console.log("=".repeat(60) + "\n"); + }, 60000); + + afterAll(async () => { + console.log("\nCleaning up performance benchmark test..."); + if (localTestData && localConnectionString) { + await cleanupTestData( + localConnectionString, + localTestData.organization.id, + ).catch((err) => console.warn(`Data cleanup warning: ${err}`)); + } + logTimings("Performance Benchmark Tests", localTimings); + }); - startTimer(`bench_${mode}`); - const runtime = await runtimeFactory.createRuntimeForUser(userContext); - benchmarks[mode] = endTimer(`bench_${mode}`); - - await invalidateRuntime(runtime.agentId as string); - } - - console.log("\nRuntime Creation Benchmarks:"); - for (const [mode, time] of Object.entries(benchmarks)) { - console.log(` ${mode}: ${time}ms`); - localTimings[`benchmark_${mode}`] = time; - } - - // All modes should create in under 10 seconds - expect(benchmarks[AgentMode.CHAT]).toBeLessThan(10000); - expect(benchmarks[AgentMode.ASSISTANT]).toBeLessThan(10000); - expect(benchmarks[AgentMode.BUILD]).toBeLessThan(10000); - }, 180000); -}); + it("should benchmark runtime creation times", async () => { + const modes = [AgentMode.CHAT, AgentMode.ASSISTANT, AgentMode.BUILD]; + const benchmarks: Record = {}; + + for (const mode of modes) { + // Ensure clean state + const userContext = buildUserContext(localTestData, { + agentMode: mode, + webSearchEnabled: false, + }); + + startTimer(`bench_${mode}`); + const runtime = await runtimeFactory.createRuntimeForUser(userContext); + benchmarks[mode] = endTimer(`bench_${mode}`); + + await invalidateRuntime(runtime.agentId as string); + } + + console.log("\nRuntime Creation Benchmarks:"); + for (const [mode, time] of Object.entries(benchmarks)) { + console.log(` ${mode}: ${time}ms`); + localTimings[`benchmark_${mode}`] = time; + } + + // All modes should create in under 10 seconds + expect(benchmarks[AgentMode.CHAT]).toBeLessThan(10000); + expect(benchmarks[AgentMode.ASSISTANT]).toBeLessThan(10000); + expect(benchmarks[AgentMode.BUILD]).toBeLessThan(10000); + }, 180000); + }, +); diff --git a/packages/tests/runtime/integration/runtime-factory/chat-mode.test.ts b/packages/tests/runtime/integration/runtime-factory/chat-mode.test.ts index a7ffab6a9..b310124bb 100644 --- a/packages/tests/runtime/integration/runtime-factory/chat-mode.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/chat-mode.test.ts @@ -88,8 +88,8 @@ describe.skipIf(skipLiveModelSuite)("RuntimeFactory - CHAT Mode", () => { ); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } logTimings("CHAT Mode Tests", timings); @@ -116,9 +116,15 @@ describe.skipIf(skipLiveModelSuite)("RuntimeFactory - CHAT Mode", () => { testUser = await createTestUser(runtime, "ChatTestUser"); startTimer("chat_message"); - const result = await sendTestMessage(runtime, testUser, "Hello! How are you?", testData, { - timeoutMs: 60000, - }); + const result = await sendTestMessage( + runtime, + testUser, + "Hello! How are you?", + testData, + { + timeoutMs: 60000, + }, + ); timings.chatMessage = endTimer("chat_message"); console.log( diff --git a/packages/tests/runtime/integration/runtime-factory/config-change-race.test.ts b/packages/tests/runtime/integration/runtime-factory/config-change-race.test.ts index 662ef681f..bb20cdce3 100644 --- a/packages/tests/runtime/integration/runtime-factory/config-change-race.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/config-change-race.test.ts @@ -58,7 +58,9 @@ async function setupTestEnvironment(): Promise { const connected = await verifyConnection(); if (!connected) { - throw new Error("Cannot connect to database. Make sure DATABASE_URL is set."); + throw new Error( + "Cannot connect to database. Make sure DATABASE_URL is set.", + ); } connectionString = getConnectionString(); console.log("Database connected"); @@ -80,8 +82,8 @@ async function setupTestEnvironment(): Promise { async function cleanupTestEnvironment(): Promise { console.log("\nCleaning up config change race condition test..."); if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } } @@ -112,7 +114,8 @@ describe.skipIf(!hasDatabaseUrl)("Mode Switching During Operations", () => { webSearchEnabled: false, }); - const assistantRuntime = await runtimeFactory.createRuntimeForUser(assistantContext); + const assistantRuntime = + await runtimeFactory.createRuntimeForUser(assistantContext); const agentId = assistantRuntime.agentId as string; console.log(`Created ASSISTANT runtime: ${agentId}`); @@ -126,13 +129,19 @@ describe.skipIf(!hasDatabaseUrl)("Mode Switching During Operations", () => { const operations: Promise[] = []; // Simulate multiple database operations like an evaluator would make - const simulateEvaluatorOperations = async (label: string): Promise => { + const simulateEvaluatorOperations = async ( + label: string, + ): Promise => { try { console.log(`${label}: Starting simulated evaluator operations...`); // Simulate runtime.countMemories() await assistantRuntime - .countMemories("00000000-0000-0000-0000-000000000001" as any, false, "messages") + .countMemories( + "00000000-0000-0000-0000-000000000001" as any, + false, + "messages", + ) .catch(() => {}); // May fail if room doesn't exist, that's OK // Simulate runtime.getMemories() @@ -165,7 +174,9 @@ describe.skipIf(!hasDatabaseUrl)("Mode Switching During Operations", () => { // Trigger mode change (invalidation) while operations are running const invalidationPromise = (async () => { await new Promise((r) => setTimeout(r, 50)); // Let operations start - console.log("\n>>> Triggering invalidation (simulating mode switch) <<<"); + console.log( + "\n>>> Triggering invalidation (simulating mode switch) <<<", + ); await invalidateRuntime(agentId); console.log(">>> Invalidation complete <<<\n"); })(); @@ -200,7 +211,9 @@ describe.skipIf(!hasDatabaseUrl)("Mode Switching During Operations", () => { // With the fix, no pool errors should occur const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, @@ -257,7 +270,9 @@ describe.skipIf(!hasDatabaseUrl)("Mode Switching During Operations", () => { // No pool errors should occur const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, @@ -292,7 +307,9 @@ describe.skipIf(!hasDatabaseUrl)("Service Access During Invalidation", () => { // Get knowledge service if available const knowledgeService = runtime.getService("knowledge"); - console.log(`Knowledge service: ${knowledgeService ? "available" : "not available"}`); + console.log( + `Knowledge service: ${knowledgeService ? "available" : "not available"}`, + ); const errors: string[] = []; @@ -327,7 +344,9 @@ describe.skipIf(!hasDatabaseUrl)("Service Access During Invalidation", () => { // Pool errors should not occur with the fix const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, @@ -362,7 +381,11 @@ describe.skipIf(!hasDatabaseUrl)("Service Access During Invalidation", () => { // Simulate memory operations await runtime - .countMemories("00000000-0000-0000-0000-000000000001" as any, false, "messages") + .countMemories( + "00000000-0000-0000-0000-000000000001" as any, + false, + "messages", + ) .catch(() => {}); // Room may not exist // Real DB query @@ -376,7 +399,11 @@ describe.skipIf(!hasDatabaseUrl)("Service Access During Invalidation", () => { }; // Multiple memory operation streams - const ops = [memoryOps("Stream1"), memoryOps("Stream2"), memoryOps("Stream3")]; + const ops = [ + memoryOps("Stream1"), + memoryOps("Stream2"), + memoryOps("Stream3"), + ]; // Invalidation during operations const invalidation = async (): Promise => { @@ -387,7 +414,9 @@ describe.skipIf(!hasDatabaseUrl)("Service Access During Invalidation", () => { await Promise.all([...ops, invalidation()]); - console.log(`\nResults: ${successes.length} successes, ${errors.length} errors`); + console.log( + `\nResults: ${successes.length} successes, ${errors.length} errors`, + ); if (errors.length > 0) { errors.forEach((e) => console.log(` ${e}`)); } else { @@ -396,7 +425,9 @@ describe.skipIf(!hasDatabaseUrl)("Service Access During Invalidation", () => { // No pool errors with the fix const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, @@ -463,7 +494,9 @@ describe.skipIf(!hasDatabaseUrl)("Concurrent Configuration Changes", () => { // No pool errors const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, @@ -477,7 +510,9 @@ describe.skipIf(!hasDatabaseUrl)("Concurrent Configuration Changes", () => { "should handle plugin configuration changes during queries", async () => { if (!hasTavilyApiKey) { - console.log("Skipping plugin config webSearch case: TAVILY_API_KEY not set"); + console.log( + "Skipping plugin config webSearch case: TAVILY_API_KEY not set", + ); return; } @@ -548,7 +583,9 @@ describe.skipIf(!hasDatabaseUrl)("Concurrent Configuration Changes", () => { // No pool errors const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, @@ -595,7 +632,9 @@ describe.skipIf(!hasDatabaseUrl)("Long-Running Operation Simulation", () => { // Step 1: runtime.countMemories() - line 215 in evaluator console.log(" [Evaluator] Step 1: countMemories"); - await runtime.countMemories(testUser.roomId, false, "messages").catch(() => {}); + await runtime + .countMemories(testUser.roomId, false, "messages") + .catch(() => {}); completedSteps.push("countMemories_1"); // Small delay (simulating processing) @@ -635,7 +674,9 @@ describe.skipIf(!hasDatabaseUrl)("Long-Running Operation Simulation", () => { // Step 7: runtime.countMemories() again - line 332 console.log(" [Evaluator] Step 7: Final countMemories"); - await runtime.countMemories(testUser.roomId, false, "messages").catch(() => {}); + await runtime + .countMemories(testUser.roomId, false, "messages") + .catch(() => {}); completedSteps.push("countMemories_2"); // Step 8: Update checkpoint - line 337 @@ -671,7 +712,9 @@ describe.skipIf(!hasDatabaseUrl)("Long-Running Operation Simulation", () => { errors.forEach((e) => console.log(` ${e}`)); const hasPoolError = errors.some( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); if (hasPoolError) { @@ -684,7 +727,9 @@ describe.skipIf(!hasDatabaseUrl)("Long-Running Operation Simulation", () => { // With the fix, no pool errors should occur const poolErrors = errors.filter( - (err) => err.toLowerCase().includes("pool") || err.toLowerCase().includes("cannot use"), + (err) => + err.toLowerCase().includes("pool") || + err.toLowerCase().includes("cannot use"), ); expect(poolErrors.length).toBe(0); }, diff --git a/packages/tests/runtime/integration/runtime-factory/mcp-oauth-injection.test.ts b/packages/tests/runtime/integration/runtime-factory/mcp-oauth-injection.test.ts index be9f922a7..58118bc76 100644 --- a/packages/tests/runtime/integration/runtime-factory/mcp-oauth-injection.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/mcp-oauth-injection.test.ts @@ -52,17 +52,18 @@ async function cleanupEnvironment(): Promise { const client = new Client({ connectionString }); await client.connect(); try { - await client.query(`DELETE FROM oauth_sessions WHERE organization_id = $1`, [ - testData.organization.id, - ]); + await client.query( + `DELETE FROM oauth_sessions WHERE organization_id = $1`, + [testData.organization.id], + ); } catch (e) { console.warn(`OAuth cleanup warning: ${e}`); } finally { await client.end(); } - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Cleanup warning: ${err}`), ); } } @@ -111,7 +112,9 @@ async function createOAuthSession( * Helper to get MCP settings from runtime. * RuntimeFactory injects per-request MCP config into character.settings at runtime. */ -function getMcpSettings(runtime: any): { servers?: Record } | undefined { +function getMcpSettings( + runtime: any, +): { servers?: Record } | undefined { return runtime.character?.settings?.mcp; } @@ -153,7 +156,11 @@ describe.skipIf(!hasDatabaseUrl)("MCP OAuth Injection", () => { it("should inject MCP plugin when user has Google OAuth connection", async () => { // Create OAuth session in database - await createOAuthSession(testData.organization.id, testData.user.id, "google"); + await createOAuthSession( + testData.organization.id, + testData.user.id, + "google", + ); // Create context WITH oauth connections const userContext: UserContext = { @@ -174,11 +181,16 @@ describe.skipIf(!hasDatabaseUrl)("MCP OAuth Injection", () => { expect(mcpSettings?.servers).toBeDefined(); expect(mcpSettings?.servers?.google).toBeDefined(); - const googleServer = mcpSettings?.servers?.google as { url?: string; type?: string }; + const googleServer = mcpSettings?.servers?.google as { + url?: string; + type?: string; + }; expect(googleServer.url).toContain("/api/mcps/google/streamable-http"); expect(googleServer.type).toBe("streamable-http"); - console.log("✅ Google MCP server injected for user with OAuth connection"); + console.log( + "✅ Google MCP server injected for user with OAuth connection", + ); console.log(` Server URL: ${googleServer.url}`); } finally { await invalidateRuntime(runtime.agentId as string); @@ -212,7 +224,9 @@ describe.skipIf(!hasDatabaseUrl)("MCP OAuth Injection", () => { expect(googleServer.url).toContain("/api/mcps/google/streamable-http"); expect(googleServer.headers?.["X-API-Key"]).toBeUndefined(); - console.log("✅ API key is not persisted in MCP settings for same-origin MCP server"); + console.log( + "✅ API key is not persisted in MCP settings for same-origin MCP server", + ); } finally { await invalidateRuntime(runtime.agentId as string); } @@ -238,7 +252,9 @@ describe.skipIf(!hasDatabaseUrl)("MCP OAuth Injection", () => { expect(mcpSettings.servers.slack).toBeUndefined(); } - console.log("✅ Unsupported platform OAuth connection does not inject MCP server"); + console.log( + "✅ Unsupported platform OAuth connection does not inject MCP server", + ); } finally { await invalidateRuntime(runtime.agentId as string); } diff --git a/packages/tests/runtime/integration/runtime-factory/oauth-cache-invalidation.test.ts b/packages/tests/runtime/integration/runtime-factory/oauth-cache-invalidation.test.ts index c98c75fc7..34de2ef6f 100644 --- a/packages/tests/runtime/integration/runtime-factory/oauth-cache-invalidation.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/oauth-cache-invalidation.test.ts @@ -63,13 +63,13 @@ async function setupEnvironment(): Promise { async function cleanupEnvironment(): Promise { console.log("\n🧹 Cleaning up..."); if (testData) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Cleanup warning: ${err}`), ); } if (testData2) { - await cleanupTestData(connectionString, testData2.organization.id).catch((err) => - console.warn(`Cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData2.organization.id).catch( + (err) => console.warn(`Cleanup warning: ${err}`), ); } } @@ -118,7 +118,9 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeCache.removeByOrganization", () => { it("should remove multiple runtimes for same organization (with/without webSearch)", async () => { if (!hasTavilyApiKey) { - console.log("Skipping webSearch invalidation case: TAVILY_API_KEY not set"); + console.log( + "Skipping webSearch invalidation case: TAVILY_API_KEY not set", + ); return; } @@ -206,10 +208,14 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeCache.removeByOrganization", () => { expect(totalRemoved).toBeGreaterThanOrEqual(1); // Cache should now be empty for this org - const verifyRemoved = await invalidateByOrganization(testData.organization.id); + const verifyRemoved = await invalidateByOrganization( + testData.organization.id, + ); expect(verifyRemoved).toBe(0); - console.log(`✅ Concurrent calls handled safely (total removed: ${totalRemoved})`); + console.log( + `✅ Concurrent calls handled safely (total removed: ${totalRemoved})`, + ); }, 60000); it("should reject invalid organization IDs (non-UUID format)", async () => { @@ -259,7 +265,9 @@ describe.skipIf(!hasDatabaseUrl)("EntitySettingsCache invalidation", () => { // Set something in cache const testSettings = new Map(); testSettings.set("TEST_KEY", "test_value"); - await entitySettingsCache.set(userId, agentId, testSettings, { TEST_KEY: "entity_settings" }); + await entitySettingsCache.set(userId, agentId, testSettings, { + TEST_KEY: "entity_settings", + }); // Verify it's cached const cached = await entitySettingsCache.get(userId, agentId); @@ -285,8 +293,12 @@ describe.skipIf(!hasDatabaseUrl)("EntitySettingsCache invalidation", () => { const settings1 = new Map([["KEY1", "value1"]]); const settings2 = new Map([["KEY2", "value2"]]); - await entitySettingsCache.set(userId, agentId1, settings1, { KEY1: "entity_settings" }); - await entitySettingsCache.set(userId, agentId2, settings2, { KEY2: "entity_settings" }); + await entitySettingsCache.set(userId, agentId1, settings1, { + KEY1: "entity_settings", + }); + await entitySettingsCache.set(userId, agentId2, settings2, { + KEY2: "entity_settings", + }); // Verify both are cached expect(await entitySettingsCache.get(userId, agentId1)).not.toBeNull(); @@ -311,8 +323,12 @@ describe.skipIf(!hasDatabaseUrl)("EntitySettingsCache invalidation", () => { const settings1 = new Map([["USER1_KEY", "user1_value"]]); const settings2 = new Map([["USER2_KEY", "user2_value"]]); - await entitySettingsCache.set(userId1, agentId, settings1, { USER1_KEY: "entity_settings" }); - await entitySettingsCache.set(userId2, agentId, settings2, { USER2_KEY: "entity_settings" }); + await entitySettingsCache.set(userId1, agentId, settings1, { + USER1_KEY: "entity_settings", + }); + await entitySettingsCache.set(userId2, agentId, settings2, { + USER2_KEY: "entity_settings", + }); // Invalidate only user 1 await entitySettingsCache.invalidateUser(userId1); @@ -330,91 +346,95 @@ describe.skipIf(!hasDatabaseUrl)("EntitySettingsCache invalidation", () => { }, 30000); }); -describe.skipIf(!hasDatabaseUrl)("OAuth flow cache invalidation integration", () => { - beforeAll(setupEnvironment, 60000); - afterAll(cleanupEnvironment); +describe.skipIf(!hasDatabaseUrl)( + "OAuth flow cache invalidation integration", + () => { + beforeAll(setupEnvironment, 60000); + afterAll(cleanupEnvironment); - afterEach(async () => { - // Clean up caches - if (testData?.organization?.id) { - await invalidateByOrganization(testData.organization.id); - } - if (testData?.user?.id) { - await entitySettingsCache.invalidateUser(testData.user.id); - } - }); - - it("should allow MCP plugin reload after runtime cache invalidation", async () => { - // 1. Create runtime WITHOUT OAuth (no MCP plugin) - const userContext1: UserContext = { - ...buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - webSearchEnabled: false, - }), - oauthConnections: undefined, // No OAuth - }; - const runtime1 = await runtimeFactory.createRuntimeForUser(userContext1); - const agentId = runtime1.agentId as string; - - // Verify cached - expect(isRuntimeCached(agentId)).toBe(true); - expect(hasRuntimeForOrganization(testData.organization.id)).toBe(true); + afterEach(async () => { + // Clean up caches + if (testData?.organization?.id) { + await invalidateByOrganization(testData.organization.id); + } + if (testData?.user?.id) { + await entitySettingsCache.invalidateUser(testData.user.id); + } + }); - // 2. Simulate OAuth connect by invalidating cache - const removed = await invalidateByOrganization(testData.organization.id); - expect(removed).toBe(1); - expect(hasRuntimeForOrganization(testData.organization.id)).toBe(false); + it("should allow MCP plugin reload after runtime cache invalidation", async () => { + // 1. Create runtime WITHOUT OAuth (no MCP plugin) + const userContext1: UserContext = { + ...buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + webSearchEnabled: false, + }), + oauthConnections: undefined, // No OAuth + }; + const runtime1 = await runtimeFactory.createRuntimeForUser(userContext1); + const agentId = runtime1.agentId as string; - // 3. Create runtime again WITH OAuth (should get MCP plugin) - const userContext2: UserContext = { - ...buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - webSearchEnabled: false, - }), - oauthConnections: [{ platform: "google" }], - }; - const runtime2 = await runtimeFactory.createRuntimeForUser(userContext2); - - // 4. Verify MCP settings are now present - type McpServerConfig = { - url?: string; - type?: string; - headers?: Record; - }; - const mcpSettings = ( - runtime2.character as { - settings?: { mcp?: { servers?: Record } }; - } - ).settings?.mcp; - expect(mcpSettings).toBeDefined(); - expect(mcpSettings?.servers?.google).toBeDefined(); + // Verify cached + expect(isRuntimeCached(agentId)).toBe(true); + expect(hasRuntimeForOrganization(testData.organization.id)).toBe(true); - console.log("✅ MCP plugin loaded after cache invalidation"); - }, 90000); + // 2. Simulate OAuth connect by invalidating cache + const removed = await invalidateByOrganization(testData.organization.id); + expect(removed).toBe(1); + expect(hasRuntimeForOrganization(testData.organization.id)).toBe(false); - it("should handle rapid connect/disconnect cycles", async () => { - // Simulate rapid OAuth changes - for (let i = 0; i < 3; i++) { - // Connect - const connectContext: UserContext = { + // 3. Create runtime again WITH OAuth (should get MCP plugin) + const userContext2: UserContext = { ...buildUserContext(testData, { agentMode: AgentMode.ASSISTANT, webSearchEnabled: false, }), oauthConnections: [{ platform: "google" }], }; - const runtime = await runtimeFactory.createRuntimeForUser(connectContext); - expect(isRuntimeCached(runtime.agentId as string)).toBe(true); - expect(hasRuntimeForOrganization(testData.organization.id)).toBe(true); + const runtime2 = await runtimeFactory.createRuntimeForUser(userContext2); - // Disconnect (invalidate) - await invalidateByOrganization(testData.organization.id); - expect(hasRuntimeForOrganization(testData.organization.id)).toBe(false); - } + // 4. Verify MCP settings are now present + type McpServerConfig = { + url?: string; + type?: string; + headers?: Record; + }; + const mcpSettings = ( + runtime2.character as { + settings?: { mcp?: { servers?: Record } }; + } + ).settings?.mcp; + expect(mcpSettings).toBeDefined(); + expect(mcpSettings?.servers?.google).toBeDefined(); + + console.log("✅ MCP plugin loaded after cache invalidation"); + }, 90000); + + it("should handle rapid connect/disconnect cycles", async () => { + // Simulate rapid OAuth changes + for (let i = 0; i < 3; i++) { + // Connect + const connectContext: UserContext = { + ...buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + webSearchEnabled: false, + }), + oauthConnections: [{ platform: "google" }], + }; + const runtime = + await runtimeFactory.createRuntimeForUser(connectContext); + expect(isRuntimeCached(runtime.agentId as string)).toBe(true); + expect(hasRuntimeForOrganization(testData.organization.id)).toBe(true); + + // Disconnect (invalidate) + await invalidateByOrganization(testData.organization.id); + expect(hasRuntimeForOrganization(testData.organization.id)).toBe(false); + } - console.log("✅ Rapid connect/disconnect cycles handled"); - }, 180000); -}); + console.log("✅ Rapid connect/disconnect cycles handled"); + }, 180000); + }, +); describe.skipIf(!hasDatabaseUrl)("Edge cases and error handling", () => { beforeAll(setupEnvironment, 60000); @@ -462,8 +482,12 @@ describe.skipIf(!hasDatabaseUrl)("Edge cases and error handling", () => { const userId = testData.user.id; // Set global settings (agentId = null) - const globalSettings = new Map([["GLOBAL_KEY", "global_value"]]); - await entitySettingsCache.set(userId, null, globalSettings, { GLOBAL_KEY: "entity_settings" }); + const globalSettings = new Map([ + ["GLOBAL_KEY", "global_value"], + ]); + await entitySettingsCache.set(userId, null, globalSettings, { + GLOBAL_KEY: "entity_settings", + }); // Verify cached const cached = await entitySettingsCache.get(userId, null); diff --git a/packages/tests/runtime/integration/runtime-factory/pool-closure-race.test.ts b/packages/tests/runtime/integration/runtime-factory/pool-closure-race.test.ts index 65d3b3997..56146d744 100644 --- a/packages/tests/runtime/integration/runtime-factory/pool-closure-race.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/pool-closure-race.test.ts @@ -88,8 +88,8 @@ async function setupTestEnvironment(): Promise { async function cleanupTestEnvironment(): Promise { console.log("\nCleaning up pool race condition test..."); if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Data cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Data cleanup warning: ${err}`), ); } } @@ -153,7 +153,9 @@ describe.skipIf(!hasDatabaseUrl)("Pool Closure Race Condition", () => { // We expect an error because the pool was ended // The error message should contain "pool" and "end" or "closed" if (!errorOccurred) { - console.log("WARNING: No error occurred - adapter may still be usable"); + console.log( + "WARNING: No error occurred - adapter may still be usable", + ); console.log("This could indicate:"); console.log(" 1. Pool is not actually closed"); console.log(" 2. Adapter has connection recovery mechanisms"); @@ -212,7 +214,9 @@ describe.skipIf(!hasDatabaseUrl)("Pool Closure Race Condition", () => { // With OLD behavior, the adapter should fail (or at least log errors) console.log("\n*** OLD BEHAVIOR TEST COMPLETE ***"); - console.log("If errors occurred above, it confirms the old buggy behavior."); + console.log( + "If errors occurred above, it confirms the old buggy behavior.", + ); // Cleanup await invalidateRuntime(agentIdA).catch(() => {}); @@ -324,10 +328,14 @@ describe.skipIf(!hasDatabaseUrl)("Pool Closure Race Condition", () => { await Promise.all([...queryPromises, evictionPromise]); - console.log(`\nResults: ${results.length} successful, ${errors.length} errors`); + console.log( + `\nResults: ${results.length} successful, ${errors.length} errors`, + ); if (errors.length === 0) { - console.log("*** SUCCESS: All queries succeeded with NEW eviction ***"); + console.log( + "*** SUCCESS: All queries succeeded with NEW eviction ***", + ); console.log("The fix is working correctly!"); } else { console.log("*** UNEXPECTED ERRORS ***"); @@ -346,92 +354,105 @@ describe.skipIf(!hasDatabaseUrl)("Pool Closure Race Condition", () => { ); }); - describe.skipIf(!hasTavilyApiKey)("Race Condition: Multiple Runtimes Sharing Pool", () => { - /** - * This test demonstrates the shared pool issue: - * 1. Create Runtime A for Agent 1 - * 2. Create Runtime B for Agent 2 (shares the same pool) - * 3. Close Runtime A (ends the shared pool) - * 4. Runtime B should fail - */ - it( - "should demonstrate shared pool closure affecting other runtimes", - async () => { - // We need two different agents to properly test shared pool - // For this test, we'll create runtimes with different cache keys - - const userContextA = buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - webSearchEnabled: false, // This creates cache key: agentId - }); - - const userContextB = buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - webSearchEnabled: true, // This creates cache key: agentId:ws - }); - - // Create Runtime A - const runtimeA = await runtimeFactory.createRuntimeForUser(userContextA); - const agentIdA = runtimeA.agentId as string; - console.log(`\nCreated Runtime A: ${agentIdA}`); - - // Create Runtime B (with web search - different cache key) - const runtimeB = await runtimeFactory.createRuntimeForUser(userContextB); - const agentIdB = `${runtimeB.agentId}:ws`; - console.log(`Created Runtime B: ${agentIdB}`); + describe.skipIf(!hasTavilyApiKey)( + "Race Condition: Multiple Runtimes Sharing Pool", + () => { + /** + * This test demonstrates the shared pool issue: + * 1. Create Runtime A for Agent 1 + * 2. Create Runtime B for Agent 2 (shares the same pool) + * 3. Close Runtime A (ends the shared pool) + * 4. Runtime B should fail + */ + it( + "should demonstrate shared pool closure affecting other runtimes", + async () => { + // We need two different agents to properly test shared pool + // For this test, we'll create runtimes with different cache keys + + const userContextA = buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + webSearchEnabled: false, // This creates cache key: agentId + }); - // Verify both are cached - expect(isRuntimeCached(agentIdA)).toBe(true); - expect(isRuntimeCached(agentIdB)).toBe(true); + const userContextB = buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + webSearchEnabled: true, // This creates cache key: agentId:ws + }); - // Get adapters - const adapterEntries = _testing.getAdapterEntries(); - console.log(`Adapter entries: ${Array.from(adapterEntries.keys()).join(", ")}`); - - // Both runtimes should be healthy - const healthyA = await runtimeA.isReady(); - const healthyB = await runtimeB.isReady(); - console.log(`Runtime A healthy: ${healthyA}`); - console.log(`Runtime B healthy: ${healthyB}`); - expect(healthyA).toBe(true); - expect(healthyB).toBe(true); - - // Now close Runtime A using safeClose (what eviction does) - console.log("\nClosing Runtime A via safeClose..."); - await _testing.forceEvictRuntime(agentIdA); - console.log("Runtime A closed"); + // Create Runtime A + const runtimeA = + await runtimeFactory.createRuntimeForUser(userContextA); + const agentIdA = runtimeA.agentId as string; + console.log(`\nCreated Runtime A: ${agentIdA}`); + + // Create Runtime B (with web search - different cache key) + const runtimeB = + await runtimeFactory.createRuntimeForUser(userContextB); + const agentIdB = `${runtimeB.agentId}:ws`; + console.log(`Created Runtime B: ${agentIdB}`); + + // Verify both are cached + expect(isRuntimeCached(agentIdA)).toBe(true); + expect(isRuntimeCached(agentIdB)).toBe(true); + + // Get adapters + const adapterEntries = _testing.getAdapterEntries(); + console.log( + `Adapter entries: ${Array.from(adapterEntries.keys()).join(", ")}`, + ); - // Try to use Runtime B - let errorB: Error | null = null; - try { - console.log("Checking Runtime B health after A was closed..."); - await runtimeB.isReady(); - console.log("Runtime B is still healthy"); - } catch (error) { - errorB = error as Error; - console.log(`Runtime B error: ${errorB.message}`); - } + // Both runtimes should be healthy + const healthyA = await runtimeA.isReady(); + const healthyB = await runtimeB.isReady(); + console.log(`Runtime A healthy: ${healthyA}`); + console.log(`Runtime B healthy: ${healthyB}`); + expect(healthyA).toBe(true); + expect(healthyB).toBe(true); + + // Now close Runtime A using safeClose (what eviction does) + console.log("\nClosing Runtime A via safeClose..."); + await _testing.forceEvictRuntime(agentIdA); + console.log("Runtime A closed"); + + // Try to use Runtime B + let errorB: Error | null = null; + try { + console.log("Checking Runtime B health after A was closed..."); + await runtimeB.isReady(); + console.log("Runtime B is still healthy"); + } catch (error) { + errorB = error as Error; + console.log(`Runtime B error: ${errorB.message}`); + } - // Analysis - if (errorB) { - console.log("\n*** SHARED POOL ISSUE REPRODUCED ***"); - console.log("Closing Runtime A affected Runtime B because they share a pool"); - expect(errorB.message.toLowerCase()).toMatch(/pool|end|closed|cannot/); - } else { - console.log("\n*** Shared pool issue NOT reproduced ***"); - console.log("Possible reasons:"); - console.log(" 1. Each runtime has a separate pool (not shared)"); - console.log(" 2. Pool recovery happened automatically"); - console.log(" 3. Different adapter instances with same underlying pool"); - } + // Analysis + if (errorB) { + console.log("\n*** SHARED POOL ISSUE REPRODUCED ***"); + console.log( + "Closing Runtime A affected Runtime B because they share a pool", + ); + expect(errorB.message.toLowerCase()).toMatch( + /pool|end|closed|cannot/, + ); + } else { + console.log("\n*** Shared pool issue NOT reproduced ***"); + console.log("Possible reasons:"); + console.log(" 1. Each runtime has a separate pool (not shared)"); + console.log(" 2. Pool recovery happened automatically"); + console.log( + " 3. Different adapter instances with same underlying pool", + ); + } - // Cleanup - await invalidateRuntime(agentIdA).catch(() => {}); - await invalidateRuntime(agentIdB).catch(() => {}); - }, - TEST_TIMEOUT, - ); - }); + // Cleanup + await invalidateRuntime(agentIdA).catch(() => {}); + await invalidateRuntime(agentIdB).catch(() => {}); + }, + TEST_TIMEOUT, + ); + }, + ); describe("Race Condition: Direct Adapter Close", () => { /** @@ -568,10 +589,14 @@ describe.skipIf(!hasDatabaseUrl)("Pool Closure Race Condition", () => { ); console.log(` Has pool-related error: ${hasPoolError}`); } else { - console.log("\n*** No errors - all queries completed before pool close ***"); + console.log( + "\n*** No errors - all queries completed before pool close ***", + ); console.log("This could mean:"); console.log(" 1. Queries completed too fast"); - console.log(" 2. isReady() doesn't throw (it catches and returns false)"); + console.log( + " 2. isReady() doesn't throw (it catches and returns false)", + ); } // Cleanup @@ -644,7 +669,9 @@ describe.skipIf(!hasDatabaseUrl)("Pool Closure Race Condition", () => { if (errors.length > 0) { console.log("\n*** RACE CONDITION REPRODUCED ***"); - console.log("Queries failed because pool was closed while they were in-flight"); + console.log( + "Queries failed because pool was closed while they were in-flight", + ); // Verify we got the expected pool-related error const hasPoolError = errors.some( @@ -697,7 +724,8 @@ describe.skipIf(!hasDatabaseUrl)("Production Error Reproduction", () => { webSearchEnabled: false, }); - const existingRuntime = await runtimeFactory.createRuntimeForUser(userContext); + const existingRuntime = + await runtimeFactory.createRuntimeForUser(userContext); const existingAgentId = existingRuntime.agentId as string; console.log(`\nStep 1: Created existing runtime: ${existingAgentId}`); @@ -707,7 +735,9 @@ describe.skipIf(!hasDatabaseUrl)("Production Error Reproduction", () => { // Step 2: Simulate timeout scenario by forcing eviction // This is what happens when cache evicts stale/unhealthy entries - console.log("\nStep 2: Simulating eviction (what happens after timeout/error)"); + console.log( + "\nStep 2: Simulating eviction (what happens after timeout/error)", + ); // Hold a promise that will try to query during eviction const concurrentQueryPromise = (async () => { @@ -757,7 +787,9 @@ describe.skipIf(!hasDatabaseUrl)("Production Error Reproduction", () => { expect(queryResult.success).toBe(false); } else { console.log("Query succeeded - race condition timing not reproduced"); - console.log("The test may need adjustment for timing or the fix may already be in place"); + console.log( + "The test may need adjustment for timing or the fix may already be in place", + ); } // Cleanup diff --git a/packages/tests/runtime/integration/runtime-factory/user-context-isolation.test.ts b/packages/tests/runtime/integration/runtime-factory/user-context-isolation.test.ts index f31553b08..49353a652 100644 --- a/packages/tests/runtime/integration/runtime-factory/user-context-isolation.test.ts +++ b/packages/tests/runtime/integration/runtime-factory/user-context-isolation.test.ts @@ -68,17 +68,21 @@ async function cleanupEnvironment(): Promise { console.log("\n🧹 Cleaning up..."); // Invalidate any cached runtimes - await invalidateRuntime("b850bc30-45f8-0041-a00a-83df46d8555d").catch(() => {}); + await invalidateRuntime("b850bc30-45f8-0041-a00a-83df46d8555d").catch( + () => {}, + ); if (testDataUser1) { - await cleanupTestData(connectionString, testDataUser1.organization.id).catch((err) => - console.warn(`Cleanup warning: ${err}`), - ); + await cleanupTestData( + connectionString, + testDataUser1.organization.id, + ).catch((err) => console.warn(`Cleanup warning: ${err}`)); } if (testDataUser2) { - await cleanupTestData(connectionString, testDataUser2.organization.id).catch((err) => - console.warn(`Cleanup warning: ${err}`), - ); + await cleanupTestData( + connectionString, + testDataUser2.organization.id, + ).catch((err) => console.warn(`Cleanup warning: ${err}`)); } } @@ -110,7 +114,10 @@ describe.skipIf(!hasDatabaseUrl)("User Context Isolation", () => { const result = await client.query( "SELECT settings FROM agents WHERE id = 'b850bc30-45f8-0041-a00a-83df46d8555d'", ); - const settings = (result.rows[0]?.settings || {}) as Record; + const settings = (result.rows[0]?.settings || {}) as Record< + string, + unknown + >; // Remove user-specific settings delete settings.ELIZAOS_API_KEY; @@ -173,9 +180,13 @@ describe.skipIf(!hasDatabaseUrl)("User Context Isolation", () => { const runtime1 = await runtimeFactory.createRuntimeForUser(userContext1); // Verify User 1's settings - expect(runtime1.getSetting("ELIZAOS_API_KEY")).toBe(testDataUser1.apiKey.key); + expect(runtime1.getSetting("ELIZAOS_API_KEY")).toBe( + testDataUser1.apiKey.key, + ); expect(runtime1.getSetting("USER_ID")).toBe(testDataUser1.user.id); - console.log(`✅ User 1 runtime created (API: ${testDataUser1.apiKey.key.substring(0, 15)}...)`); + console.log( + `✅ User 1 runtime created (API: ${testDataUser1.apiKey.key.substring(0, 15)}...)`, + ); await invalidateRuntime(runtime1.agentId as string); @@ -246,7 +257,9 @@ describe.skipIf(!hasDatabaseUrl)("User Context Isolation", () => { it("should not leak MCP settings between users with different OAuth states", async () => { // Helper to get MCP settings the way McpService does - const getMcpSettings = (runtime: any): Record | undefined => { + const getMcpSettings = ( + runtime: any, + ): Record | undefined => { return runtime.character?.settings?.mcp || runtime.settings?.mcp; }; @@ -270,7 +283,9 @@ describe.skipIf(!hasDatabaseUrl)("User Context Isolation", () => { expect(googleServer1?.url).toContain("/api/mcps/google/streamable-http"); expect(googleServer1?.headers?.["X-API-Key"]).toBeUndefined(); - console.log("✅ User 1 (with OAuth) has Google MCP enabled without persisted API key"); + console.log( + "✅ User 1 (with OAuth) has Google MCP enabled without persisted API key", + ); await invalidateRuntime(runtime1.agentId as string); diff --git a/packages/tests/runtime/mcp-assistant-trending.test.ts b/packages/tests/runtime/mcp-assistant-trending.test.ts index 85033871d..9abfe4b8f 100644 --- a/packages/tests/runtime/mcp-assistant-trending.test.ts +++ b/packages/tests/runtime/mcp-assistant-trending.test.ts @@ -54,7 +54,9 @@ async function setupTestEnvironment(): Promise { // Check debug tracing const debugEnabled = isDebugTracingEnabled(); - console.log(`\n🔍 Debug Tracing: ${debugEnabled ? "✅ ENABLED" : "❌ DISABLED"}`); + console.log( + `\n🔍 Debug Tracing: ${debugEnabled ? "✅ ENABLED" : "❌ DISABLED"}`, + ); if (!debugEnabled) { console.log(" Set DEBUG_TRACING=true in .env to enable debug output"); } @@ -98,201 +100,215 @@ async function cleanupTestEnvironment(): Promise { await testRuntimeResult.cleanup(); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch(() => {}); + await cleanupTestData(connectionString, testData.organization.id).catch( + () => {}, + ); } logTimings("MCP Assistant Trending Tests", timings); console.log("✅ Cleanup complete\n"); } -describe.skipIf(skipLiveModelSuite)("MCP Assistant - Trending Tokens Query", () => { - beforeAll(async () => { - await setupTestEnvironment(); - }); - afterAll(cleanupTestEnvironment); - - it("should create runtime with MCP plugin", async () => { - startTimer("runtime_creation"); - - testRuntimeResult = await createTestRuntime({ - testData, - characterId: testData.character?.id, - agentMode: "ASSISTANT" as any, - webSearchEnabled: false, +describe.skipIf(skipLiveModelSuite)( + "MCP Assistant - Trending Tokens Query", + () => { + beforeAll(async () => { + await setupTestEnvironment(); }); + afterAll(cleanupTestEnvironment); - timings.runtimeCreation = endTimer("runtime_creation"); - - expect(testRuntimeResult).toBeDefined(); - expect(testRuntimeResult.runtime).toBeDefined(); - expect(testRuntimeResult.runtime.agentId).toBeDefined(); + it("should create runtime with MCP plugin", async () => { + startTimer("runtime_creation"); - console.log(`\n✅ Runtime created in ${timings.runtimeCreation}ms`); - console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); - console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); - }, 120000); + testRuntimeResult = await createTestRuntime({ + testData, + characterId: testData.character?.id, + agentMode: "ASSISTANT" as any, + webSearchEnabled: false, + }); - it("should have MCP service available", async () => { - startTimer("mcp_service_check"); + timings.runtimeCreation = endTimer("runtime_creation"); - const mcpService = getMcpService(testRuntimeResult.runtime); + expect(testRuntimeResult).toBeDefined(); + expect(testRuntimeResult.runtime).toBeDefined(); + expect(testRuntimeResult.runtime.agentId).toBeDefined(); - timings.mcpServiceCheck = endTimer("mcp_service_check"); + console.log(`\n✅ Runtime created in ${timings.runtimeCreation}ms`); + console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); + console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); + }, 120000); - expect(mcpService).toBeDefined(); - console.log(`\n✅ MCP service found in ${timings.mcpServiceCheck}ms`); + it("should have MCP service available", async () => { + startTimer("mcp_service_check"); - if (mcpService?.getServers) { - const servers = mcpService.getServers(); - console.log(` Servers: ${servers?.length || 0}`); - } - }, 10000); - - it("should wait for MCP initialization", async () => { - startTimer("mcp_init_wait"); - - const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); + const mcpService = getMcpService(testRuntimeResult.runtime); - timings.mcpInitWait = endTimer("mcp_init_wait"); + timings.mcpServiceCheck = endTimer("mcp_service_check"); - expect(isReady).toBe(true); - console.log(`\n✅ MCP initialized in ${timings.mcpInitWait}ms`); + expect(mcpService).toBeDefined(); + console.log(`\n✅ MCP service found in ${timings.mcpServiceCheck}ms`); - const mcpService = getMcpService(testRuntimeResult.runtime); - if (mcpService?.getTools) { - const tools = mcpService.getTools(); - console.log(` Tools available: ${tools?.length || 0}`); - if (tools && tools.length > 0) { - console.log( - ` Sample tools: ${tools - .slice(0, 5) - .map((t: any) => t.name || t) - .join(", ")}`, - ); + if (mcpService?.getServers) { + const servers = mcpService.getServers(); + console.log(` Servers: ${servers?.length || 0}`); } - } - }, 30000); - - it("should create test user with elizaOS entities", async () => { - startTimer("user_creation"); + }, 10000); - testUserContext = await createTestUser(testRuntimeResult.runtime, "TrendingTestUser"); + it("should wait for MCP initialization", async () => { + startTimer("mcp_init_wait"); - timings.userCreation = endTimer("user_creation"); + const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); - expect(testUserContext).toBeDefined(); - expect(testUserContext.entityId).toBeDefined(); - expect(testUserContext.roomId).toBeDefined(); - expect(testUserContext.worldId).toBeDefined(); + timings.mcpInitWait = endTimer("mcp_init_wait"); - console.log(`\n✅ Test user created in ${timings.userCreation}ms`); - console.log(` Entity ID: ${testUserContext.entityId}`); - console.log(` Room ID: ${testUserContext.roomId}`); - }, 30000); + expect(isReady).toBe(true); + console.log(`\n✅ MCP initialized in ${timings.mcpInitWait}ms`); - it("should process 'can you get trending tokens' with debug tracing", async () => { - const debugEnabled = isDebugTracingEnabled(); - console.log( - `\n🔍 Processing message with debug tracing ${debugEnabled ? "ENABLED" : "DISABLED"}`, - ); - - // Clear any existing traces - clearDebugTraces(); - - startTimer("trending_query"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Can you get trending tokens?", - testData, - { - timeoutMs: 120000, - debug: { - enabled: debugEnabled, - renderView: "full", - storeTrace: true, + const mcpService = getMcpService(testRuntimeResult.runtime); + if (mcpService?.getTools) { + const tools = mcpService.getTools(); + console.log(` Tools available: ${tools?.length || 0}`); + if (tools && tools.length > 0) { + console.log( + ` Sample tools: ${tools + .slice(0, 5) + .map((t: any) => t.name || t) + .join(", ")}`, + ); + } + } + }, 30000); + + it("should create test user with elizaOS entities", async () => { + startTimer("user_creation"); + + testUserContext = await createTestUser( + testRuntimeResult.runtime, + "TrendingTestUser", + ); + + timings.userCreation = endTimer("user_creation"); + + expect(testUserContext).toBeDefined(); + expect(testUserContext.entityId).toBeDefined(); + expect(testUserContext.roomId).toBeDefined(); + expect(testUserContext.worldId).toBeDefined(); + + console.log(`\n✅ Test user created in ${timings.userCreation}ms`); + console.log(` Entity ID: ${testUserContext.entityId}`); + console.log(` Room ID: ${testUserContext.roomId}`); + }, 30000); + + it("should process 'can you get trending tokens' with debug tracing", async () => { + const debugEnabled = isDebugTracingEnabled(); + console.log( + `\n🔍 Processing message with debug tracing ${debugEnabled ? "ENABLED" : "DISABLED"}`, + ); + + // Clear any existing traces + clearDebugTraces(); + + startTimer("trending_query"); + + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "Can you get trending tokens?", + testData, + { + timeoutMs: 120000, + debug: { + enabled: debugEnabled, + renderView: "full", + storeTrace: true, + }, }, - }, - ); + ); - timings.trendingQuery = endTimer("trending_query"); + timings.trendingQuery = endTimer("trending_query"); - console.log(`\n📨 Message processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); + console.log(`\n📨 Message processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(`\n📝 Response:`); - console.log(` ${result.response.text?.substring(0, 500)}...`); - } + if (result.response) { + console.log(`\n📝 Response:`); + console.log(` ${result.response.text?.substring(0, 500)}...`); + } - if (result.error) { - console.log(`\n❌ Error: ${result.error}`); - } + if (result.error) { + console.log(`\n❌ Error: ${result.error}`); + } - // Output debug trace if available - if (debugEnabled) { - const trace = getLatestDebugTrace(); - if (trace) { - console.log("\n" + "=".repeat(60)); - console.log("🔍 DEBUG TRACE"); - console.log("=".repeat(60)); - - const markdown = renderDebugTrace(trace, "full"); - console.log(markdown); - - console.log("\n" + "=".repeat(60)); - console.log("📊 TRACE SUMMARY"); - console.log("=".repeat(60)); - console.log(` Run ID: ${trace.runId}`); - console.log(` Status: ${trace.status}`); - console.log(` Agent Mode: ${trace.agentMode}`); - console.log(` Steps: ${trace.steps?.length || 0}`); - console.log(` Duration: ${trace.endedAt ? trace.endedAt - trace.startedAt : "N/A"}ms`); - - const fail = trace.failures[0]; - if (fail) { - console.log(`\n⚠️ Failure detected:`); - console.log(` Type: ${fail.type}`); - console.log(` Message: ${fail.message}`); - console.log(` Step: ${fail.stepIndex}`); + // Output debug trace if available + if (debugEnabled) { + const trace = getLatestDebugTrace(); + if (trace) { + console.log("\n" + "=".repeat(60)); + console.log("🔍 DEBUG TRACE"); + console.log("=".repeat(60)); + + const markdown = renderDebugTrace(trace, "full"); + console.log(markdown); + + console.log("\n" + "=".repeat(60)); + console.log("📊 TRACE SUMMARY"); + console.log("=".repeat(60)); + console.log(` Run ID: ${trace.runId}`); + console.log(` Status: ${trace.status}`); + console.log(` Agent Mode: ${trace.agentMode}`); + console.log(` Steps: ${trace.steps?.length || 0}`); + console.log( + ` Duration: ${trace.endedAt ? trace.endedAt - trace.startedAt : "N/A"}ms`, + ); + + const fail = trace.failures[0]; + if (fail) { + console.log(`\n⚠️ Failure detected:`); + console.log(` Type: ${fail.type}`); + console.log(` Message: ${fail.message}`); + console.log(` Step: ${fail.stepIndex}`); + } + } else { + console.log( + "\n⚠️ No debug trace captured (trace may not have been generated)", + ); } - } else { - console.log("\n⚠️ No debug trace captured (trace may not have been generated)"); } - } - // The test passes if we got a response OR we got a specific MCP-related interaction - expect(result.didRespond || result.error === undefined).toBe(true); - }, 180000); - - it("should also handle a simpler greeting to verify basic functionality", async () => { - startTimer("greeting_test"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Hello! What can you help me with?", - testData, - { - timeoutMs: 60000, - }, - ); + // The test passes if we got a response OR we got a specific MCP-related interaction + expect(result.didRespond || result.error === undefined).toBe(true); + }, 180000); - timings.greetingTest = endTimer("greeting_test"); + it("should also handle a simpler greeting to verify basic functionality", async () => { + startTimer("greeting_test"); - console.log(`\n📨 Greeting processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(` Response: ${result.response.text?.substring(0, 200)}...`); - } - if (result.error) { - console.log(` Error: ${result.error}`); - } + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "Hello! What can you help me with?", + testData, + { + timeoutMs: 60000, + }, + ); - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - }, 120000); -}); + timings.greetingTest = endTimer("greeting_test"); + + console.log(`\n📨 Greeting processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); + if (result.response) { + console.log( + ` Response: ${result.response.text?.substring(0, 200)}...`, + ); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } + + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + }, 120000); + }, +); describe.skipIf(!hasDatabaseUrl)("Debug Tracing Status", () => { it("should report debug tracing configuration", () => { @@ -302,7 +318,9 @@ describe.skipIf(!hasDatabaseUrl)("Debug Tracing Status", () => { console.log("DEBUG TRACING STATUS"); console.log("=".repeat(60)); console.log(`Enabled: ${debugEnabled ? "✅ YES" : "❌ NO"}`); - console.log(`Environment: DEBUG_TRACING=${process.env.DEBUG_TRACING || "not set"}`); + console.log( + `Environment: DEBUG_TRACING=${process.env.DEBUG_TRACING || "not set"}`, + ); console.log("=".repeat(60)); if (!debugEnabled) { diff --git a/packages/tests/runtime/mcp-loading.test.ts b/packages/tests/runtime/mcp-loading.test.ts index bfa29fc70..af66f9196 100644 --- a/packages/tests/runtime/mcp-loading.test.ts +++ b/packages/tests/runtime/mcp-loading.test.ts @@ -80,113 +80,123 @@ async function cleanupTestEnvironment(): Promise { await testRuntimeResult.cleanup(); } if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch(() => {}); + await cleanupTestData(connectionString, testData.organization.id).catch( + () => {}, + ); } logTimings("MCP Loading Tests", timings); console.log("✅ Cleanup complete\n"); } -describe.skipIf(skipLiveModelSuite)("MCP Plugin Loading - Production Flow", () => { - beforeAll(setupTestEnvironment); - afterAll(cleanupTestEnvironment); - - it("should create runtime with MCP plugin using RuntimeFactory", async () => { - startTimer("runtime_creation"); - - testRuntimeResult = await createTestRuntime({ - testData, - characterId: testData.character?.id, - agentMode: "ASSISTANT" as any, - webSearchEnabled: false, - }); - - timings.runtimeCreation = endTimer("runtime_creation"); - - expect(testRuntimeResult).toBeDefined(); - expect(testRuntimeResult.runtime).toBeDefined(); - expect(testRuntimeResult.runtime.agentId).toBeDefined(); - - console.log(`\n✅ Runtime created in ${timings.runtimeCreation}ms`); - console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); - console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); - }, 120000); - - it("should have MCP service available", async () => { - startTimer("mcp_service_check"); - - const mcpService = getMcpService(testRuntimeResult.runtime); - - timings.mcpServiceCheck = endTimer("mcp_service_check"); - - expect(mcpService).toBeDefined(); - console.log(`\n✅ MCP service found in ${timings.mcpServiceCheck}ms`); - - if (mcpService?.getServers) { - const servers = mcpService.getServers(); - console.log(` Servers: ${servers?.length || 0}`); - } - }, 10000); - - it("should wait for MCP initialization", async () => { - startTimer("mcp_init_wait"); - - const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); - - timings.mcpInitWait = endTimer("mcp_init_wait"); - - expect(isReady).toBe(true); - console.log(`\n✅ MCP initialized in ${timings.mcpInitWait}ms`); - - const mcpService = getMcpService(testRuntimeResult.runtime); - if (mcpService?.getTools) { - const tools = mcpService.getTools(); - console.log(` Tools available: ${tools?.length || 0}`); - } - }, 30000); - - it("should create test user with elizaOS entities", async () => { - startTimer("user_creation"); - - testUserContext = await createTestUser(testRuntimeResult.runtime, "MCPTestUser"); - - timings.userCreation = endTimer("user_creation"); - - expect(testUserContext).toBeDefined(); - expect(testUserContext.entityId).toBeDefined(); - expect(testUserContext.roomId).toBeDefined(); - expect(testUserContext.worldId).toBeDefined(); - - console.log(`\n✅ Test user created in ${timings.userCreation}ms`); - console.log(` Entity ID: ${testUserContext.entityId}`); - console.log(` Room ID: ${testUserContext.roomId}`); - }, 30000); - - it("should process a message through the runtime", async () => { - startTimer("message_processing"); - - const result = await sendTestMessage( - testRuntimeResult.runtime, - testUserContext, - "Hello! What can you help me with?", - testData, - { - timeoutMs: 60000, - }, - ); +describe.skipIf(skipLiveModelSuite)( + "MCP Plugin Loading - Production Flow", + () => { + beforeAll(setupTestEnvironment); + afterAll(cleanupTestEnvironment); - timings.messageProcessing = endTimer("message_processing"); - - console.log(`\n📨 Message processed in ${result.duration}ms`); - console.log(` Did respond: ${result.didRespond}`); - if (result.response) { - console.log(` Response: ${result.response.text?.substring(0, 100)}...`); - } - if (result.error) { - console.log(` Error: ${result.error}`); - } - - expect(result.didRespond).toBe(true); - expect(result.response).toBeDefined(); - expect(result.error).toBeUndefined(); - }, 120000); -}); + it("should create runtime with MCP plugin using RuntimeFactory", async () => { + startTimer("runtime_creation"); + + testRuntimeResult = await createTestRuntime({ + testData, + characterId: testData.character?.id, + agentMode: "ASSISTANT" as any, + webSearchEnabled: false, + }); + + timings.runtimeCreation = endTimer("runtime_creation"); + + expect(testRuntimeResult).toBeDefined(); + expect(testRuntimeResult.runtime).toBeDefined(); + expect(testRuntimeResult.runtime.agentId).toBeDefined(); + + console.log(`\n✅ Runtime created in ${timings.runtimeCreation}ms`); + console.log(` Agent ID: ${testRuntimeResult.runtime.agentId}`); + console.log(` Character: ${testRuntimeResult.runtime.character?.name}`); + }, 120000); + + it("should have MCP service available", async () => { + startTimer("mcp_service_check"); + + const mcpService = getMcpService(testRuntimeResult.runtime); + + timings.mcpServiceCheck = endTimer("mcp_service_check"); + + expect(mcpService).toBeDefined(); + console.log(`\n✅ MCP service found in ${timings.mcpServiceCheck}ms`); + + if (mcpService?.getServers) { + const servers = mcpService.getServers(); + console.log(` Servers: ${servers?.length || 0}`); + } + }, 10000); + + it("should wait for MCP initialization", async () => { + startTimer("mcp_init_wait"); + + const isReady = await waitForMcpReady(testRuntimeResult.runtime, 15000); + + timings.mcpInitWait = endTimer("mcp_init_wait"); + + expect(isReady).toBe(true); + console.log(`\n✅ MCP initialized in ${timings.mcpInitWait}ms`); + + const mcpService = getMcpService(testRuntimeResult.runtime); + if (mcpService?.getTools) { + const tools = mcpService.getTools(); + console.log(` Tools available: ${tools?.length || 0}`); + } + }, 30000); + + it("should create test user with elizaOS entities", async () => { + startTimer("user_creation"); + + testUserContext = await createTestUser( + testRuntimeResult.runtime, + "MCPTestUser", + ); + + timings.userCreation = endTimer("user_creation"); + + expect(testUserContext).toBeDefined(); + expect(testUserContext.entityId).toBeDefined(); + expect(testUserContext.roomId).toBeDefined(); + expect(testUserContext.worldId).toBeDefined(); + + console.log(`\n✅ Test user created in ${timings.userCreation}ms`); + console.log(` Entity ID: ${testUserContext.entityId}`); + console.log(` Room ID: ${testUserContext.roomId}`); + }, 30000); + + it("should process a message through the runtime", async () => { + startTimer("message_processing"); + + const result = await sendTestMessage( + testRuntimeResult.runtime, + testUserContext, + "Hello! What can you help me with?", + testData, + { + timeoutMs: 60000, + }, + ); + + timings.messageProcessing = endTimer("message_processing"); + + console.log(`\n📨 Message processed in ${result.duration}ms`); + console.log(` Did respond: ${result.didRespond}`); + if (result.response) { + console.log( + ` Response: ${result.response.text?.substring(0, 100)}...`, + ); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } + + expect(result.didRespond).toBe(true); + expect(result.response).toBeDefined(); + expect(result.error).toBeUndefined(); + }, 120000); + }, +); diff --git a/packages/tests/runtime/performance.test.ts b/packages/tests/runtime/performance.test.ts index d0870bfcc..34aafc12c 100644 --- a/packages/tests/runtime/performance.test.ts +++ b/packages/tests/runtime/performance.test.ts @@ -56,7 +56,9 @@ async function setupEnvironment(): Promise { async function cleanupEnvironment(): Promise { console.log("\n🧹 Cleaning up..."); if (testData && connectionString) { - await cleanupTestData(connectionString, testData.organization.id).catch(() => {}); + await cleanupTestData(connectionString, testData.organization.id).catch( + () => {}, + ); } } @@ -104,7 +106,9 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Creation Performance", () => { // Target: <3000ms for CHAT runtime if (avg > 3000) { - console.warn(`⚠️ CHAT runtime avg (${avg.toFixed(0)}ms) exceeds 3s target`); + console.warn( + `⚠️ CHAT runtime avg (${avg.toFixed(0)}ms) exceeds 3s target`, + ); } expect(avg).toBeGreaterThan(0); @@ -138,7 +142,9 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Creation Performance", () => { // Target: <5000ms for ASSISTANT runtime (includes MCP init) if (avg > 5000) { - console.warn(`⚠️ ASSISTANT runtime avg (${avg.toFixed(0)}ms) exceeds 5s target`); + console.warn( + `⚠️ ASSISTANT runtime avg (${avg.toFixed(0)}ms) exceeds 5s target`, + ); } expect(avg).toBeGreaterThan(0); @@ -252,7 +258,9 @@ describe.skipIf(!hasDatabaseUrl)("Database Query Performance", () => { } const avg = times.reduce((a, b) => a + b, 0) / times.length; - console.log(`\n📊 Memory Retrieval (10 items): avg ${avg.toFixed(1)}ms (${runs} runs)`); + console.log( + `\n📊 Memory Retrieval (10 items): avg ${avg.toFixed(1)}ms (${runs} runs)`, + ); expect(avg).toBeGreaterThan(0); expect(avg).toBeLessThan(200); // Retrieval should be very fast @@ -293,12 +301,18 @@ describe.skipIf(!hasDatabaseUrl)("Runtime Caching Performance", () => { runtimes.push(runtime2); console.log("\n📊 Cache Performance:"); - console.log(` Cold start (cache miss): ${coldResult.durationMs.toFixed(1)}ms`); - console.log(` Warm start (cache hit): ${warmResult.durationMs.toFixed(1)}ms`); + console.log( + ` Cold start (cache miss): ${coldResult.durationMs.toFixed(1)}ms`, + ); + console.log( + ` Warm start (cache hit): ${warmResult.durationMs.toFixed(1)}ms`, + ); if (warmResult.durationMs < coldResult.durationMs) { const speedup = - ((coldResult.durationMs - warmResult.durationMs) / coldResult.durationMs) * 100; + ((coldResult.durationMs - warmResult.durationMs) / + coldResult.durationMs) * + 100; console.log(` Speedup: ${speedup.toFixed(1)}%`); } diff --git a/packages/tests/runtime/runtime-factory.test.ts b/packages/tests/runtime/runtime-factory.test.ts index 17ecc7eac..db9490aa7 100644 --- a/packages/tests/runtime/runtime-factory.test.ts +++ b/packages/tests/runtime/runtime-factory.test.ts @@ -88,8 +88,8 @@ async function setupEnvironment(): Promise { async function cleanupEnvironment(): Promise { console.log("\n🧹 Cleaning up..."); if (testData) { - await cleanupTestData(connectionString, testData.organization.id).catch((err) => - console.warn(`Cleanup warning: ${err}`), + await cleanupTestData(connectionString, testData.organization.id).catch( + (err) => console.warn(`Cleanup warning: ${err}`), ); } logTimings("All RuntimeFactory Tests", allTimings); @@ -120,16 +120,24 @@ describe.skipIf(skipLiveModelSuites)("RuntimeFactory - CHAT Mode", () => { expect(runtime).toBeDefined(); expect(runtime.agentId).toBeDefined(); - console.log(`\n✅ CHAT runtime created in ${allTimings.chatRuntimeCreate}ms`); + console.log( + `\n✅ CHAT runtime created in ${allTimings.chatRuntimeCreate}ms`, + ); }, 60000); it("should process message in CHAT mode", async () => { testUser = await createTestUser(runtime, "ChatTestUser"); startTimer("chat_message"); - const result = await sendTestMessage(runtime, testUser, "Hello! How are you?", testData, { - timeoutMs: 60000, - }); + const result = await sendTestMessage( + runtime, + testUser, + "Hello! How are you?", + testData, + { + timeoutMs: 60000, + }, + ); allTimings.chatMessage = endTimer("chat_message"); console.log( @@ -163,117 +171,129 @@ describe.skipIf(skipLiveModelSuites)("RuntimeFactory - CHAT Mode", () => { // ASSISTANT Mode Tests (with MCP) // ============================================================================ -describe.skipIf(skipLiveModelSuites)("RuntimeFactory - ASSISTANT Mode (MCP)", () => { - let runtime: TestRuntime; - let testUser: TestUserContext; +describe.skipIf(skipLiveModelSuites)( + "RuntimeFactory - ASSISTANT Mode (MCP)", + () => { + let runtime: TestRuntime; + let testUser: TestUserContext; - beforeAll(setupEnvironment); - afterAll(cleanupEnvironment); + beforeAll(setupEnvironment); + afterAll(cleanupEnvironment); - it("should create runtime in ASSISTANT mode with MCP", async () => { - startTimer("assistant_runtime_create"); + it("should create runtime in ASSISTANT mode with MCP", async () => { + startTimer("assistant_runtime_create"); - const userContext = buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - characterId: testData.character?.id, - webSearchEnabled: false, // Isolate MCP testing - }); + const userContext = buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + characterId: testData.character?.id, + webSearchEnabled: false, // Isolate MCP testing + }); - runtime = await runtimeFactory.createRuntimeForUser(userContext); + runtime = await runtimeFactory.createRuntimeForUser(userContext); - allTimings.assistantRuntimeCreate = endTimer("assistant_runtime_create"); + allTimings.assistantRuntimeCreate = endTimer("assistant_runtime_create"); - expect(runtime).toBeDefined(); - expect(runtime.character?.name).toBe("Mira"); - console.log(`\n✅ ASSISTANT runtime created in ${allTimings.assistantRuntimeCreate}ms`); - }, 60000); + expect(runtime).toBeDefined(); + expect(runtime.character?.name).toBe("Mira"); + console.log( + `\n✅ ASSISTANT runtime created in ${allTimings.assistantRuntimeCreate}ms`, + ); + }, 60000); - it("should have MCP service initialized", async () => { - startTimer("mcp_init"); - const isReady = await waitForMcpReady(runtime, 15000); - allTimings.mcpInit = endTimer("mcp_init"); + it("should have MCP service initialized", async () => { + startTimer("mcp_init"); + const isReady = await waitForMcpReady(runtime, 15000); + allTimings.mcpInit = endTimer("mcp_init"); - expect(isReady).toBe(true); + expect(isReady).toBe(true); - const mcpService = getMcpService(runtime); - expect(mcpService).toBeDefined(); + const mcpService = getMcpService(runtime); + expect(mcpService).toBeDefined(); - const tools = mcpService?.getTools?.(); - console.log(`\n✅ MCP ready in ${allTimings.mcpInit}ms`); - console.log(` Tools: ${tools?.length || 0}`); - }, 30000); + const tools = mcpService?.getTools?.(); + console.log(`\n✅ MCP ready in ${allTimings.mcpInit}ms`); + console.log(` Tools: ${tools?.length || 0}`); + }, 30000); - it("should process message with MCP tools available", async () => { - testUser = await createTestUser(runtime, "AssistantTestUser"); + it("should process message with MCP tools available", async () => { + testUser = await createTestUser(runtime, "AssistantTestUser"); - startTimer("assistant_message"); - const result = await sendTestMessage( - runtime, - testUser, - "What's the current price of Bitcoin?", - testData, - { timeoutMs: 120000 }, - ); - allTimings.assistantMessage = endTimer("assistant_message"); + startTimer("assistant_message"); + const result = await sendTestMessage( + runtime, + testUser, + "What's the current price of Bitcoin?", + testData, + { timeoutMs: 120000 }, + ); + allTimings.assistantMessage = endTimer("assistant_message"); - expect(result.didRespond).toBe(true); - console.log(`\n✅ ASSISTANT message in ${allTimings.assistantMessage}ms`); - console.log(` Response: ${result.response?.text?.substring(0, 80)}...`); - }, 180000); + expect(result.didRespond).toBe(true); + console.log(`\n✅ ASSISTANT message in ${allTimings.assistantMessage}ms`); + console.log(` Response: ${result.response?.text?.substring(0, 80)}...`); + }, 180000); - it("should cleanup ASSISTANT runtime", async () => { - await invalidateRuntime(runtime.agentId as string); - }); -}); + it("should cleanup ASSISTANT runtime", async () => { + await invalidateRuntime(runtime.agentId as string); + }); + }, +); // ============================================================================ // ASSISTANT Mode with Web Search // ============================================================================ -describe.skipIf(skipLiveModelSuites)("RuntimeFactory - ASSISTANT Mode (Web Search)", () => { - let runtime: TestRuntime; - let testUser: TestUserContext; +describe.skipIf(skipLiveModelSuites)( + "RuntimeFactory - ASSISTANT Mode (Web Search)", + () => { + let runtime: TestRuntime; + let testUser: TestUserContext; - beforeAll(setupEnvironment); - afterAll(cleanupEnvironment); + beforeAll(setupEnvironment); + afterAll(cleanupEnvironment); - it("should create runtime with web search enabled", async () => { - startTimer("websearch_runtime_create"); + it("should create runtime with web search enabled", async () => { + startTimer("websearch_runtime_create"); - const userContext = buildUserContext(testData, { - agentMode: AgentMode.ASSISTANT, - webSearchEnabled: true, - }); + const userContext = buildUserContext(testData, { + agentMode: AgentMode.ASSISTANT, + webSearchEnabled: true, + }); - runtime = await runtimeFactory.createRuntimeForUser(userContext); + runtime = await runtimeFactory.createRuntimeForUser(userContext); - allTimings.webSearchRuntimeCreate = endTimer("websearch_runtime_create"); + allTimings.webSearchRuntimeCreate = endTimer("websearch_runtime_create"); - expect(runtime).toBeDefined(); - console.log(`\n✅ Web Search runtime in ${allTimings.webSearchRuntimeCreate}ms`); - }, 60000); + expect(runtime).toBeDefined(); + console.log( + `\n✅ Web Search runtime in ${allTimings.webSearchRuntimeCreate}ms`, + ); + }, 60000); - it("should process message with web search", async () => { - testUser = await createTestUser(runtime, "WebSearchTestUser"); + it("should process message with web search", async () => { + testUser = await createTestUser(runtime, "WebSearchTestUser"); - startTimer("websearch_message"); - const result = await sendTestMessage( - runtime, - testUser, - "What is the latest news about AI?", - testData, - { timeoutMs: 120000 }, - ); - allTimings.webSearchMessage = endTimer("websearch_message"); + startTimer("websearch_message"); + const result = await sendTestMessage( + runtime, + testUser, + "What is the latest news about AI?", + testData, + { timeoutMs: 120000 }, + ); + allTimings.webSearchMessage = endTimer("websearch_message"); - expect(result.didRespond).toBe(true); - console.log(`\n✅ Web Search message in ${allTimings.webSearchMessage}ms`); - }, 180000); + expect(result.didRespond).toBe(true); + console.log( + `\n✅ Web Search message in ${allTimings.webSearchMessage}ms`, + ); + }, 180000); - it("should cleanup web search runtime", async () => { - await invalidateRuntime(runtime.agentId as string); - }); -}); + it("should cleanup web search runtime", async () => { + await invalidateRuntime(runtime.agentId as string); + }); + }, +); // ============================================================================ // BUILD Mode Tests @@ -300,7 +320,9 @@ describe.skipIf(skipLiveModelSuites)("RuntimeFactory - BUILD Mode", () => { allTimings.buildRuntimeCreate = endTimer("build_runtime_create"); expect(runtime).toBeDefined(); - console.log(`\n✅ BUILD runtime created in ${allTimings.buildRuntimeCreate}ms`); + console.log( + `\n✅ BUILD runtime created in ${allTimings.buildRuntimeCreate}ms`, + ); }, 60000); it("should process BUILD mode message", async () => { @@ -374,7 +396,9 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { const stats = getRuntimeCacheStats(); expect(stats).toBeDefined(); expect(stats.runtime).toBeDefined(); - console.log(`\n📊 Cache stats: size=${stats.runtime.size}/${stats.runtime.maxSize}`); + console.log( + `\n📊 Cache stats: size=${stats.runtime.size}/${stats.runtime.maxSize}`, + ); }); it("should separate cached runtimes when direct model preferences differ", async () => { @@ -389,7 +413,8 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { agentMode: AgentMode.ASSISTANT, webSearchEnabled: false, modelPreferences: { - responseHandlerModel: "projects/demo/locations/us-central1/endpoints/demo-handler", + responseHandlerModel: + "projects/demo/locations/us-central1/endpoints/demo-handler", }, }); @@ -397,12 +422,12 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { const runtime2 = await runtimeFactory.createRuntimeForUser(tunedContext); expect(runtime1).not.toBe(runtime2); - expect(runtime1.character.settings?.ELIZAOS_CLOUD_RESPONSE_HANDLER_MODEL).toBe( - "google/gemini-2.5-flash-lite", - ); - expect(runtime2.character.settings?.ELIZAOS_CLOUD_RESPONSE_HANDLER_MODEL).toBe( - "projects/demo/locations/us-central1/endpoints/demo-handler", - ); + expect( + runtime1.character.settings?.ELIZAOS_CLOUD_RESPONSE_HANDLER_MODEL, + ).toBe("google/gemini-2.5-flash-lite"); + expect( + runtime2.character.settings?.ELIZAOS_CLOUD_RESPONSE_HANDLER_MODEL, + ).toBe("projects/demo/locations/us-central1/endpoints/demo-handler"); await invalidateRuntime(runtime1.agentId as string); }, 60000); @@ -412,37 +437,40 @@ describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Caching Behavior", () => { // Performance Benchmarks // ============================================================================ -describe.skipIf(!hasDatabaseUrl)("RuntimeFactory - Performance Benchmarks", () => { - beforeAll(setupEnvironment); - afterAll(cleanupEnvironment); - - it("should benchmark runtime creation times", async () => { - const modes = [AgentMode.CHAT, AgentMode.ASSISTANT, AgentMode.BUILD]; - const benchmarks: Record = {}; - - for (const mode of modes) { - // Ensure clean state - const userContext = buildUserContext(testData, { - agentMode: mode, - webSearchEnabled: false, - }); - - startTimer(`bench_${mode}`); - const runtime = await runtimeFactory.createRuntimeForUser(userContext); - benchmarks[mode] = endTimer(`bench_${mode}`); - - await invalidateRuntime(runtime.agentId as string); - } - - console.log("\n📊 Runtime Creation Benchmarks:"); - for (const [mode, time] of Object.entries(benchmarks)) { - console.log(` ${mode}: ${time}ms`); - allTimings[`benchmark_${mode}`] = time; - } - - // All modes should create in under 10 seconds - expect(benchmarks[AgentMode.CHAT]).toBeLessThan(10000); - expect(benchmarks[AgentMode.ASSISTANT]).toBeLessThan(10000); - expect(benchmarks[AgentMode.BUILD]).toBeLessThan(10000); - }, 180000); -}); +describe.skipIf(!hasDatabaseUrl)( + "RuntimeFactory - Performance Benchmarks", + () => { + beforeAll(setupEnvironment); + afterAll(cleanupEnvironment); + + it("should benchmark runtime creation times", async () => { + const modes = [AgentMode.CHAT, AgentMode.ASSISTANT, AgentMode.BUILD]; + const benchmarks: Record = {}; + + for (const mode of modes) { + // Ensure clean state + const userContext = buildUserContext(testData, { + agentMode: mode, + webSearchEnabled: false, + }); + + startTimer(`bench_${mode}`); + const runtime = await runtimeFactory.createRuntimeForUser(userContext); + benchmarks[mode] = endTimer(`bench_${mode}`); + + await invalidateRuntime(runtime.agentId as string); + } + + console.log("\n📊 Runtime Creation Benchmarks:"); + for (const [mode, time] of Object.entries(benchmarks)) { + console.log(` ${mode}: ${time}ms`); + allTimings[`benchmark_${mode}`] = time; + } + + // All modes should create in under 10 seconds + expect(benchmarks[AgentMode.CHAT]).toBeLessThan(10000); + expect(benchmarks[AgentMode.ASSISTANT]).toBeLessThan(10000); + expect(benchmarks[AgentMode.BUILD]).toBeLessThan(10000); + }, 180000); + }, +); diff --git a/packages/tests/setup-runtime.ts b/packages/tests/setup-runtime.ts index 3f9411e4c..02a6a6d70 100644 --- a/packages/tests/setup-runtime.ts +++ b/packages/tests/setup-runtime.ts @@ -5,7 +5,10 @@ * Creates test fixtures, runs tests, cleans up after. */ -import { getConnectionString, verifyConnection } from "./infrastructure/local-database"; +import { + getConnectionString, + verifyConnection, +} from "./infrastructure/local-database"; import { cleanupTestData, createTestDataSet, @@ -21,7 +24,9 @@ let isSetup = false; */ export function getTestData(): TestDataSet { if (!globalTestData) { - throw new Error("Test data not initialized. Call setupTestEnvironment() first."); + throw new Error( + "Test data not initialized. Call setupTestEnvironment() first.", + ); } return globalTestData; } @@ -55,7 +60,9 @@ export interface SetupOptions { * Setup the test environment * Call this in beforeAll() of your test suite */ -export async function setupTestEnvironment(options: SetupOptions = {}): Promise { +export async function setupTestEnvironment( + options: SetupOptions = {}, +): Promise { if (isSetup) { console.log("[Setup] Environment already initialized, skipping..."); return; diff --git a/packages/tests/setup.ts b/packages/tests/setup.ts index fd172d8b7..4688970d7 100644 --- a/packages/tests/setup.ts +++ b/packages/tests/setup.ts @@ -56,7 +56,9 @@ async function verifyLocalServerRunning(): Promise { // 401/403 = server running but auth required (expected for some endpoints) // 200 = healthy // 5xx = server error (should still proceed, server is technically running) - console.log(` ✅ Local server running at ${LOCAL_SERVER_URL} (status: ${response.status})`); + console.log( + ` ✅ Local server running at ${LOCAL_SERVER_URL} (status: ${response.status})`, + ); } // Run verification synchronously at module load time diff --git a/packages/tests/support/bun-partial-module-shims.ts b/packages/tests/support/bun-partial-module-shims.ts index d8c2d8a6b..2a1c2bc97 100644 --- a/packages/tests/support/bun-partial-module-shims.ts +++ b/packages/tests/support/bun-partial-module-shims.ts @@ -4,21 +4,29 @@ * (e.g. `@/db/repositories`) and later imports in the same process. */ -const parsedCostBuffer = Number.parseFloat(process.env.CREDIT_COST_BUFFER ?? ""); +const parsedCostBuffer = Number.parseFloat( + process.env.CREDIT_COST_BUFFER ?? "", +); const COST_BUFFER = Number.isFinite(parsedCostBuffer) ? parsedCostBuffer : 1.5; const MIN_RESERVATION = 0.000001; -const { UsersRepository: RealUsersRepository } = await import("@/db/repositories/users"); +const { UsersRepository: RealUsersRepository } = await import( + "@/db/repositories/users" +); /** Minimal interface for stubbing usersRepository in tests. */ type UsersRepositoryStub = Partial>; /** Shape returned by stubUsersRepositoryModule matching the real module exports. */ -interface UsersRepositoryModuleStub { +interface UsersRepositoryModuleStub< + TUsersRepository extends UsersRepositoryStub, +> { UsersRepository: typeof RealUsersRepository; usersRepository: TUsersRepository; } -export function stubUsersRepositoryModule(overrides: { +export function stubUsersRepositoryModule< + TUsersRepository extends UsersRepositoryStub, +>(overrides: { usersRepository: TUsersRepository; }): UsersRepositoryModuleStub { return { diff --git a/packages/tests/unit/admin-docker-routes.test.ts b/packages/tests/unit/admin-docker-routes.test.ts index ef9f0455b..1d4fedb81 100644 --- a/packages/tests/unit/admin-docker-routes.test.ts +++ b/packages/tests/unit/admin-docker-routes.test.ts @@ -12,8 +12,20 @@ describe("Status Consistency", () => { "disconnected", "error", ]; - const ALL_NODE: DockerNodeStatus[] = ["healthy", "degraded", "offline", "unknown"]; - const BADGE = new Set(["running", "stopped", "error", "provisioning", "pending", "disconnected"]); + const ALL_NODE: DockerNodeStatus[] = [ + "healthy", + "degraded", + "offline", + "unknown", + ]; + const BADGE = new Set([ + "running", + "stopped", + "error", + "provisioning", + "pending", + "disconnected", + ]); test("badge covers all sandbox statuses", () => { for (const s of ALL_SANDBOX) expect(BADGE.has(s)).toBe(true); }); @@ -56,7 +68,9 @@ describe("Logs Route — isValidDockerLogsSince", () => { expect(isValidDockerLogsSince("1h;cat /etc/passwd")).toBe(false); }); test("rejects command injection via &&", () => { - expect(isValidDockerLogsSince("2026-03-09T12:00:00Z && whoami")).toBe(false); + expect(isValidDockerLogsSince("2026-03-09T12:00:00Z && whoami")).toBe( + false, + ); }); // Invalid: locale-dependent strings (the core fix for item #2) diff --git a/packages/tests/unit/anthropic-thinking.test.ts b/packages/tests/unit/anthropic-thinking.test.ts index 09980ff3a..da722d1fe 100644 --- a/packages/tests/unit/anthropic-thinking.test.ts +++ b/packages/tests/unit/anthropic-thinking.test.ts @@ -45,12 +45,19 @@ describe("resolveAnthropicThinkingBudgetTokens", () => { }); test("returns null for Anthropic model that does not support extended thinking", () => { - const result = resolveAnthropicThinkingBudgetTokens("anthropic/claude-3-haiku", {}); + const result = resolveAnthropicThinkingBudgetTokens( + "anthropic/claude-3-haiku", + {}, + ); expect(result).toBeNull(); }); test("uses per-agent budget when provided for supported Anthropic model", () => { - const result = resolveAnthropicThinkingBudgetTokens("anthropic/claude-sonnet-4", {}, 5000); + const result = resolveAnthropicThinkingBudgetTokens( + "anthropic/claude-sonnet-4", + {}, + 5000, + ); expect(result).toBe(5000); }); @@ -64,14 +71,20 @@ describe("resolveAnthropicThinkingBudgetTokens", () => { }); test("falls back to env budget when per-agent budget is undefined", () => { - const result = resolveAnthropicThinkingBudgetTokens("anthropic/claude-sonnet-4", { - [COT_ENV_KEY]: "8000", - }); + const result = resolveAnthropicThinkingBudgetTokens( + "anthropic/claude-sonnet-4", + { + [COT_ENV_KEY]: "8000", + }, + ); expect(result).toBe(8000); }); test("returns null when both per-agent and env budgets are unset", () => { - const result = resolveAnthropicThinkingBudgetTokens("anthropic/claude-sonnet-4", {}); + const result = resolveAnthropicThinkingBudgetTokens( + "anthropic/claude-sonnet-4", + {}, + ); expect(result).toBeNull(); }); @@ -94,17 +107,23 @@ describe("resolveAnthropicThinkingBudgetTokens", () => { }); test("clamps env fallback budget to max cap", () => { - const result = resolveAnthropicThinkingBudgetTokens("anthropic/claude-sonnet-4", { - [COT_ENV_KEY]: "15000", - [COT_MAX_ENV_KEY]: "10000", - }); + const result = resolveAnthropicThinkingBudgetTokens( + "anthropic/claude-sonnet-4", + { + [COT_ENV_KEY]: "15000", + [COT_MAX_ENV_KEY]: "10000", + }, + ); expect(result).toBe(10000); }); test("returns null when env budget is 0 (explicitly disabled)", () => { - const result = resolveAnthropicThinkingBudgetTokens("anthropic/claude-sonnet-4", { - [COT_ENV_KEY]: "0", - }); + const result = resolveAnthropicThinkingBudgetTokens( + "anthropic/claude-sonnet-4", + { + [COT_ENV_KEY]: "0", + }, + ); expect(result).toBeNull(); }); @@ -156,39 +175,49 @@ describe("anthropic COT env", () => { }); test("positive integer → number", () => { - expect(parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "1024" })).toBe(1024); - expect(parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: " 2048 " })).toBe(2048); + expect(parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "1024" })).toBe( + 1024, + ); + expect(parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: " 2048 " })).toBe( + 2048, + ); }); test("invalid non-empty throws", () => { - expect(() => parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "abc" })).toThrow( - /non-negative integer/, - ); - expect(() => parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "12.5" })).toThrow( - /non-negative integer/, - ); - expect(() => parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "12x" })).toThrow( - /non-negative integer/, - ); + expect(() => + parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "abc" }), + ).toThrow(/non-negative integer/); + expect(() => + parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "12.5" }), + ).toThrow(/non-negative integer/); + expect(() => + parseAnthropicCotBudgetFromEnv({ [COT_ENV_KEY]: "12x" }), + ).toThrow(/non-negative integer/); }); }); describe("anthropicThinkingProviderOptions", () => { test("non-anthropic model → {}", () => { expect(anthropicThinkingProviderOptions("gpt-4o", {})).toEqual({}); - expect(anthropicThinkingProviderOptions("openai/gpt-4o", { [COT_ENV_KEY]: "1024" })).toEqual( - {}, - ); + expect( + anthropicThinkingProviderOptions("openai/gpt-4o", { + [COT_ENV_KEY]: "1024", + }), + ).toEqual({}); }); test("anthropic model + budget → thinking enabled", () => { const env = { [COT_ENV_KEY]: "1024" }; - expect(anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env)).toEqual({ + expect( + anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env), + ).toEqual({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1024 } }, }, }); - expect(anthropicThinkingProviderOptions("claude-sonnet-4-5-20250929", env)).toEqual({ + expect( + anthropicThinkingProviderOptions("claude-sonnet-4-5-20250929", env), + ).toEqual({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1024 } }, }, @@ -196,17 +225,27 @@ describe("anthropic COT env", () => { }); test("anthropic model + no budget → {}", () => { - expect(anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", {})).toEqual({}); + expect( + anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", {}), + ).toEqual({}); }); test("per-agent 0 disables despite env default", () => { const env = { [COT_ENV_KEY]: "1024" }; - expect(anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env, 0)).toEqual({}); + expect( + anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env, 0), + ).toEqual({}); }); test("per-agent budget overrides env default", () => { const env = { [COT_ENV_KEY]: "1024" }; - expect(anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env, 2048)).toEqual({ + expect( + anthropicThinkingProviderOptions( + "anthropic/claude-sonnet-4.6", + env, + 2048, + ), + ).toEqual({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 2048 } }, }, @@ -215,7 +254,13 @@ describe("anthropic COT env", () => { test("ANTHROPIC_COT_BUDGET_MAX clamps per-agent budget", () => { const env = { [COT_ENV_KEY]: "1024", [COT_MAX_ENV_KEY]: "500" }; - expect(anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env, 9000)).toEqual({ + expect( + anthropicThinkingProviderOptions( + "anthropic/claude-sonnet-4.6", + env, + 9000, + ), + ).toEqual({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 500 } }, }, @@ -224,7 +269,9 @@ describe("anthropic COT env", () => { test("ANTHROPIC_COT_BUDGET_MAX clamps env default when no per-agent override", () => { const env = { [COT_ENV_KEY]: "9000", [COT_MAX_ENV_KEY]: "1000" }; - expect(anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env)).toEqual({ + expect( + anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env), + ).toEqual({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1000 } }, }, @@ -236,7 +283,9 @@ describe("anthropic COT env", () => { test("aliases mergeProviderOptions(undefined, anthropicThinking…)", () => { expect(mergeAnthropicCotProviderOptions("openai/gpt-4o", {})).toEqual({}); const env = { [COT_ENV_KEY]: "1024" }; - expect(mergeAnthropicCotProviderOptions("anthropic/claude-sonnet-4.6", env)).toEqual( + expect( + mergeAnthropicCotProviderOptions("anthropic/claude-sonnet-4.6", env), + ).toEqual( mergeProviderOptions( undefined, anthropicThinkingProviderOptions("anthropic/claude-sonnet-4.6", env), @@ -249,7 +298,10 @@ describe("anthropic COT env", () => { test("gateway order + anthropic when Claude + budget", () => { const env = { [COT_ENV_KEY]: "1024" }; expect( - mergeGatewayGroqPreferenceWithAnthropicCot("anthropic/claude-sonnet-4.6", env), + mergeGatewayGroqPreferenceWithAnthropicCot( + "anthropic/claude-sonnet-4.6", + env, + ), ).toEqual( mergeProviderOptions( { providerOptions: { gateway: { order: ["groq"] } } }, @@ -262,9 +314,18 @@ describe("anthropic COT env", () => { describe("mergeGoogleImageModalitiesWithAnthropicCot", () => { test("matches explicit google merge + anthropic fragment", () => { - expect(mergeGoogleImageModalitiesWithAnthropicCot("google/gemini-2.5-flash-image", {})).toEqual( + expect( + mergeGoogleImageModalitiesWithAnthropicCot( + "google/gemini-2.5-flash-image", + {}, + ), + ).toEqual( mergeProviderOptions( - { providerOptions: { google: { responseModalities: ["TEXT", "IMAGE"] } } }, + { + providerOptions: { + google: { responseModalities: ["TEXT", "IMAGE"] }, + }, + }, anthropicThinkingProviderOptions("google/gemini-2.5-flash-image", {}), ), ); @@ -277,7 +338,9 @@ describe("parseAnthropicCotBudgetMaxFromEnv", () => { }); test("positive → cap", () => { - expect(parseAnthropicCotBudgetMaxFromEnv({ [COT_MAX_ENV_KEY]: "8192" })).toBe(8192); + expect( + parseAnthropicCotBudgetMaxFromEnv({ [COT_MAX_ENV_KEY]: "8192" }), + ).toBe(8192); }); }); @@ -293,18 +356,28 @@ describe("parseThinkingBudgetFromCharacterSettings", () => { }); test("integer ≥ 0", () => { - expect(parseThinkingBudgetFromCharacterSettings({ anthropicThinkingBudgetTokens: 0 })).toBe(0); - expect(parseThinkingBudgetFromCharacterSettings({ anthropicThinkingBudgetTokens: 42 })).toBe( - 42, - ); + expect( + parseThinkingBudgetFromCharacterSettings({ + anthropicThinkingBudgetTokens: 0, + }), + ).toBe(0); + expect( + parseThinkingBudgetFromCharacterSettings({ + anthropicThinkingBudgetTokens: 42, + }), + ).toBe(42); }); test("float input is rejected", () => { expect( - parseThinkingBudgetFromCharacterSettings({ anthropicThinkingBudgetTokens: 4000.9 }), + parseThinkingBudgetFromCharacterSettings({ + anthropicThinkingBudgetTokens: 4000.9, + }), ).toBeUndefined(); expect( - parseThinkingBudgetFromCharacterSettings({ anthropicThinkingBudgetTokens: 1.1 }), + parseThinkingBudgetFromCharacterSettings({ + anthropicThinkingBudgetTokens: 1.1, + }), ).toBeUndefined(); }); }); @@ -320,7 +393,9 @@ describe("supportsExtendedThinking", () => { expect(supportsExtendedThinking("claude-3-7-sonnet")).toBe(true); expect(supportsExtendedThinking("anthropic/claude-3-7-sonnet")).toBe(true); expect(supportsExtendedThinking("claude-3-7-sonnet-20250219")).toBe(true); - expect(supportsExtendedThinking("anthropic/claude-3-7-sonnet-latest")).toBe(true); + expect(supportsExtendedThinking("anthropic/claude-3-7-sonnet-latest")).toBe( + true, + ); }); test("returns false for Claude 3 Opus", () => { @@ -337,7 +412,9 @@ describe("supportsExtendedThinking", () => { expect(supportsExtendedThinking("claude-opus-4")).toBe(true); expect(supportsExtendedThinking("anthropic/claude-opus-4")).toBe(true); expect(supportsExtendedThinking("claude-opus-4-20250514")).toBe(true); - expect(supportsExtendedThinking("anthropic/claude-opus-4-latest")).toBe(true); + expect(supportsExtendedThinking("anthropic/claude-opus-4-latest")).toBe( + true, + ); }); test("returns false for Claude Haiku (does not support thinking)", () => { @@ -364,8 +441,14 @@ describe("mergeProviderOptions", () => { test("preserves google and adds anthropic", () => { const merged = mergeProviderOptions( - { providerOptions: { google: { responseModalities: ["TEXT", "IMAGE"] } } }, - { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 512 } } } }, + { + providerOptions: { google: { responseModalities: ["TEXT", "IMAGE"] } }, + }, + { + providerOptions: { + anthropic: { thinking: { type: "enabled", budgetTokens: 512 } }, + }, + }, ); expect(merged).toEqual({ providerOptions: { @@ -378,7 +461,11 @@ describe("mergeProviderOptions", () => { test("merges gateway.order with anthropic", () => { const merged = mergeProviderOptions( { providerOptions: { gateway: { order: ["groq"] } } }, - { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1024 } } } }, + { + providerOptions: { + anthropic: { thinking: { type: "enabled", budgetTokens: 1024 } }, + }, + }, ); expect(merged).toEqual({ providerOptions: { @@ -391,7 +478,11 @@ describe("mergeProviderOptions", () => { test("both sides anthropic → later wins shallow fields", () => { const merged = mergeProviderOptions( { providerOptions: { anthropic: { sendReasoning: false } } }, - { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 100 } } } }, + { + providerOptions: { + anthropic: { thinking: { type: "enabled", budgetTokens: 100 } }, + }, + }, ); expect(merged.providerOptions.anthropic).toEqual({ sendReasoning: false, @@ -403,11 +494,17 @@ describe("mergeProviderOptions", () => { // Note: Only gateway, anthropic, and google keys are deep-merged. // Other provider keys (e.g. openai, mistral) are clobbered by outer spread. const merged = mergeProviderOptions( - { providerOptions: { openai: { organizationId: "org-123", projectId: "proj-456" } } }, + { + providerOptions: { + openai: { organizationId: "org-123", projectId: "proj-456" }, + }, + }, { providerOptions: { openai: { organizationId: "org-789" } } }, ); // The entire openai object from extra replaces base — projectId is lost - expect(merged.providerOptions.openai).toEqual({ organizationId: "org-789" }); + expect(merged.providerOptions.openai).toEqual({ + organizationId: "org-789", + }); }); test("documents that non-deep-merged keys drop base fields on conflict", () => { @@ -420,6 +517,8 @@ describe("mergeProviderOptions", () => { ); // safeMode is lost because mistral isn't deep-merged expect(merged.providerOptions.mistral).toEqual({ apiKey: "key-2" }); - expect((merged.providerOptions.mistral as { safeMode?: boolean }).safeMode).toBeUndefined(); + expect( + (merged.providerOptions.mistral as { safeMode?: boolean }).safeMode, + ).toBeUndefined(); }); }); diff --git a/packages/tests/unit/api/openapi-catalog.test.ts b/packages/tests/unit/api/openapi-catalog.test.ts index 59a4b19ff..8e539cc0e 100644 --- a/packages/tests/unit/api/openapi-catalog.test.ts +++ b/packages/tests/unit/api/openapi-catalog.test.ts @@ -18,7 +18,9 @@ describe("Public API catalog", () => { } for (const endpoint of API_ENDPOINTS) { - expect(implemented.has(`${endpoint.method} ${endpoint.path}`)).toBe(true); + expect(implemented.has(`${endpoint.method} ${endpoint.path}`)).toBe( + true, + ); } }, { timeout: 15_000 }, @@ -32,11 +34,17 @@ describe("Public API catalog", () => { const body = await response.json(); expect(body.info?.title).toBe("Eliza Cloud API"); - expect(body.info?.contact?.url).toMatch(/^https:\/\/www\.(dev\.)?elizacloud\.ai$/); - expect(body.servers?.[0]?.url).toMatch(/^https:\/\/www\.(dev\.)?elizacloud\.ai$/); + expect(body.info?.contact?.url).toMatch( + /^https:\/\/www\.(dev\.)?elizacloud\.ai$/, + ); + expect(body.servers?.[0]?.url).toMatch( + /^https:\/\/www\.(dev\.)?elizacloud\.ai$/, + ); for (const endpoint of API_ENDPOINTS) { - expect(body.paths[endpoint.path]?.[endpoint.method.toLowerCase()]).toBeDefined(); + expect( + body.paths[endpoint.path]?.[endpoint.method.toLowerCase()], + ).toBeDefined(); } expect(body.paths["/api/elevenlabs/tts"]?.post).toBeDefined(); @@ -49,6 +57,8 @@ describe("Public API catalog", () => { expect(response.status).toBe(204); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); - expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET"); + expect(response.headers.get("Access-Control-Allow-Methods")).toContain( + "GET", + ); }); }); diff --git a/packages/tests/unit/api/route-test-helpers.ts b/packages/tests/unit/api/route-test-helpers.ts index 5ebbdd194..368d7b55c 100644 --- a/packages/tests/unit/api/route-test-helpers.ts +++ b/packages/tests/unit/api/route-test-helpers.ts @@ -16,7 +16,9 @@ export function jsonRequest( }); } -export function routeParams>(params: T): { params: Promise } { +export function routeParams>( + params: T, +): { params: Promise } { return { params: Promise.resolve(params) }; } @@ -32,7 +34,14 @@ export function formDataRequest(url: string, formData: FormData) { } as unknown as NextRequest; } -export function createFile(name: string, type: string, contents: string | Uint8Array = "test") { - const data = typeof contents === "string" ? new TextEncoder().encode(contents) : contents; +export function createFile( + name: string, + type: string, + contents: string | Uint8Array = "test", +) { + const data = + typeof contents === "string" + ? new TextEncoder().encode(contents) + : contents; return new File([data as BlobPart], name, { type }); } diff --git a/packages/tests/unit/blooio-schema.test.ts b/packages/tests/unit/blooio-schema.test.ts index 290508915..fe2785fd6 100644 --- a/packages/tests/unit/blooio-schema.test.ts +++ b/packages/tests/unit/blooio-schema.test.ts @@ -98,13 +98,18 @@ describe("Blooio Webhook Schema", () => { const payload = { event: "message_received", sender: "+15551234567", - attachments: ["https://media.blooio.com/image1.jpg", "https://media.blooio.com/image2.jpg"], + attachments: [ + "https://media.blooio.com/image1.jpg", + "https://media.blooio.com/image2.jpg", + ], }; const result = parseBlooioWebhookEvent(payload); expect(result.attachments).toHaveLength(2); - expect(result.attachments?.[0]).toBe("https://media.blooio.com/image1.jpg"); + expect(result.attachments?.[0]).toBe( + "https://media.blooio.com/image1.jpg", + ); }); it("should accept object attachments with url and name", () => { @@ -120,11 +125,17 @@ describe("Blooio Webhook Schema", () => { const result = parseBlooioWebhookEvent(payload); expect(result.attachments).toHaveLength(2); - const firstAttachment = result.attachments?.[0] as { url: string; name?: string | null }; + const firstAttachment = result.attachments?.[0] as { + url: string; + name?: string | null; + }; expect(firstAttachment.url).toBe("https://media.blooio.com/image.jpg"); expect(firstAttachment.name).toBe("photo.jpg"); - const secondAttachment = result.attachments?.[1] as { url: string; name?: string | null }; + const secondAttachment = result.attachments?.[1] as { + url: string; + name?: string | null; + }; expect(secondAttachment.name).toBeNull(); }); diff --git a/packages/tests/unit/cloud-bootstrap-context-routing.test.ts b/packages/tests/unit/cloud-bootstrap-context-routing.test.ts index 2c74605de..fdbec4df9 100644 --- a/packages/tests/unit/cloud-bootstrap-context-routing.test.ts +++ b/packages/tests/unit/cloud-bootstrap-context-routing.test.ts @@ -44,10 +44,13 @@ describe("cloud bootstrap context routing", () => { }); test("derives available contexts from action and provider catalog fallbacks", () => { - const nextState = attachAvailableContexts({ values: {}, data: {}, text: "" } as never, { - actions: [{ name: "SEND_TOKEN" }, { name: "WEB_SEARCH" }] as never, - providers: [{ name: "walletBalance" }, { name: "knowledge" }] as never, - }); + const nextState = attachAvailableContexts( + { values: {}, data: {}, text: "" } as never, + { + actions: [{ name: "SEND_TOKEN" }, { name: "WEB_SEARCH" }] as never, + providers: [{ name: "walletBalance" }, { name: "knowledge" }] as never, + }, + ); expect(nextState.values.availableContexts).toContain("general"); expect(nextState.values.availableContexts).toContain("wallet"); @@ -57,25 +60,42 @@ describe("cloud bootstrap context routing", () => { test("filters actions to the active routed contexts", () => { const filtered = filterActionsByRouting( - [{ name: "SEND_TOKEN" }, { name: "WEB_SEARCH" }, { name: "MANAGE_PLUGINS" }] as never, + [ + { name: "SEND_TOKEN" }, + { name: "WEB_SEARCH" }, + { name: "MANAGE_PLUGINS" }, + ] as never, { primaryContext: "wallet", secondaryContexts: ["knowledge"], }, ); - expect(filtered.map((action) => action.name)).toEqual(["SEND_TOKEN", "WEB_SEARCH"]); + expect(filtered.map((action) => action.name)).toEqual([ + "SEND_TOKEN", + "WEB_SEARCH", + ]); }); test("parses mixed context list inputs and ignores invalid entries", () => { expect( - parseContextList(["wallet;automation", "Knowledge", "wallet", "not-a-context", 123]), + parseContextList([ + "wallet;automation", + "Knowledge", + "wallet", + "not-a-context", + 123, + ]), ).toEqual(["wallet", "automation", "knowledge"]); }); test("returns all actions when routing stays in the general context", () => { const filtered = filterActionsByRouting( - [{ name: "SEND_TOKEN" }, { name: "WEB_SEARCH" }, { name: "MANAGE_PLUGINS" }] as never, + [ + { name: "SEND_TOKEN" }, + { name: "WEB_SEARCH" }, + { name: "MANAGE_PLUGINS" }, + ] as never, {}, ); @@ -88,7 +108,10 @@ describe("cloud bootstrap context routing", () => { test("prefers declared action contexts over catalog fallbacks", () => { const filtered = filterActionsByRouting( - [{ name: "SEND_TOKEN", contexts: ["knowledge"] }, { name: "WEB_SEARCH" }] as never, + [ + { name: "SEND_TOKEN", contexts: ["knowledge"] }, + { name: "WEB_SEARCH" }, + ] as never, { primaryContext: "wallet", }, @@ -111,6 +134,8 @@ describe("cloud bootstrap context routing", () => { ); expect(nextState.values.retained).toBe("yes"); - expect(nextState.values.availableContexts).toBe("browser, general, knowledge, wallet"); + expect(nextState.values.availableContexts).toBe( + "browser, general, knowledge, wallet", + ); }); }); diff --git a/packages/tests/unit/cloud-bootstrap-multi-step-guards.test.ts b/packages/tests/unit/cloud-bootstrap-multi-step-guards.test.ts index 8bd642b36..7e301524b 100644 --- a/packages/tests/unit/cloud-bootstrap-multi-step-guards.test.ts +++ b/packages/tests/unit/cloud-bootstrap-multi-step-guards.test.ts @@ -7,7 +7,13 @@ import { describe("cloud bootstrap multi-step guards", () => { test("extracts available action names from provider data", () => { expect( - [...getAvailableActionNames([{ name: "WEB_SEARCH" }, { name: "FINISH" }, null])].sort(), + [ + ...getAvailableActionNames([ + { name: "WEB_SEARCH" }, + { name: "FINISH" }, + null, + ]), + ].sort(), ).toEqual(["FINISH", "WEB_SEARCH"]); }); diff --git a/packages/tests/unit/cloudformation-template.test.ts b/packages/tests/unit/cloudformation-template.test.ts index 1dc9fe0d7..1eacfd9ae 100644 --- a/packages/tests/unit/cloudformation-template.test.ts +++ b/packages/tests/unit/cloudformation-template.test.ts @@ -20,7 +20,8 @@ describe("per-user CloudFormation template", () => { ], }); - const ingressRules = template.Resources.UserSecurityGroup.Properties.SecurityGroupIngress; + const ingressRules = + template.Resources.UserSecurityGroup.Properties.SecurityGroupIngress; const directIngressRule = ingressRules.find( (rule: Record) => "Fn::If" in rule, ); @@ -41,7 +42,8 @@ describe("per-user CloudFormation template", () => { }); test("still allows ALB traffic to reach the container port", () => { - const ingressRules = template.Resources.UserSecurityGroup.Properties.SecurityGroupIngress; + const ingressRules = + template.Resources.UserSecurityGroup.Properties.SecurityGroupIngress; expect(ingressRules).toContainEqual({ IpProtocol: "tcp", diff --git a/packages/tests/unit/compat-envelope.test.ts b/packages/tests/unit/compat-envelope.test.ts index fa96e00e3..7e5f83165 100644 --- a/packages/tests/unit/compat-envelope.test.ts +++ b/packages/tests/unit/compat-envelope.test.ts @@ -86,40 +86,58 @@ describe("toCompatAgent", () => { expect(agent.updated_at).toBe("2026-03-09T11:00:00.000Z"); expect(agent.last_heartbeat_at).toBe("2026-03-09T12:00:00.000Z"); expect(agent.containerUrl).toBe("http://10.0.0.5:18800"); - expect(agent.webUiUrl).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz"); + expect(agent.webUiUrl).toBe( + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz", + ); }); test("maps pending status to queued", () => { - expect(toCompatAgent(makeSandbox({ status: "pending" })).status).toBe("queued"); + expect(toCompatAgent(makeSandbox({ status: "pending" })).status).toBe( + "queued", + ); }); test("maps error status to failed", () => { - expect(toCompatAgent(makeSandbox({ status: "error" })).status).toBe("failed"); + expect(toCompatAgent(makeSandbox({ status: "error" })).status).toBe( + "failed", + ); }); test("maps disconnected status to stopped", () => { - expect(toCompatAgent(makeSandbox({ status: "disconnected" })).status).toBe("stopped"); + expect(toCompatAgent(makeSandbox({ status: "disconnected" })).status).toBe( + "stopped", + ); }); test("uses sandbox_id as container_id fallback", () => { - const agent = toCompatAgent(makeSandbox({ container_name: null, sandbox_id: "vercel-123" })); + const agent = toCompatAgent( + makeSandbox({ container_name: null, sandbox_id: "vercel-123" }), + ); expect(agent.container_id).toBe("vercel-123"); }); test("web_ui_url uses public domain route when headscale_ip is null", () => { const agent = toCompatAgent(makeSandbox({ headscale_ip: null })); - expect(agent.web_ui_url).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz"); - expect(agent.webUiUrl).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz"); + expect(agent.web_ui_url).toBe( + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz", + ); + expect(agent.webUiUrl).toBe( + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz", + ); }); test("uses configurable agent base domain for web UI links", () => { process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN = "agents.example.com"; const agent = toCompatAgent(makeSandbox()); - expect(agent.webUiUrl).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.agents.example.com"); + expect(agent.webUiUrl).toBe( + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.agents.example.com", + ); }); test("last_heartbeat_at is null when never heartbeated", () => { - expect(toCompatAgent(makeSandbox({ last_heartbeat_at: null })).last_heartbeat_at).toBeNull(); + expect( + toCompatAgent(makeSandbox({ last_heartbeat_at: null })).last_heartbeat_at, + ).toBeNull(); }); }); @@ -241,14 +259,18 @@ describe("toCompatStatus", () => { expect(status.status).toBe("running"); expect(status.lastHeartbeat).toBe("2026-03-09T12:00:00.000Z"); expect(status.bridgeUrl).toBe("http://10.0.0.5:18800"); - expect(status.webUiUrl).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz"); + expect(status.webUiUrl).toBe( + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.shad0w.xyz", + ); expect(status.currentNode).toBe("agent-node-1"); expect(status.suspendedReason).toBeNull(); expect(status.databaseStatus).toBe("ready"); }); test("includes error message as suspendedReason", () => { - const status = toCompatStatus(makeSandbox({ status: "error", error_message: "OOM killed" })); + const status = toCompatStatus( + makeSandbox({ status: "error", error_message: "OOM killed" }), + ); expect(status.status).toBe("failed"); expect(status.suspendedReason).toBe("OOM killed"); }); @@ -291,7 +313,9 @@ describe("toCompatUsage", () => { }); test("defaults funding source to unknown", () => { - expect(toCompatUsage(makeSandbox({ agent_config: {} })).fundingSource).toBe("unknown"); + expect(toCompatUsage(makeSandbox({ agent_config: {} })).fundingSource).toBe( + "unknown", + ); }); }); @@ -299,9 +323,12 @@ describe("mapStatus", () => { test("pending -> queued", () => expect(mapStatus("pending")).toBe("queued")); test("provisioning -> provisioning", () => expect(mapStatus("provisioning")).toBe("provisioning")); - test("running -> running", () => expect(mapStatus("running")).toBe("running")); - test("stopped -> stopped", () => expect(mapStatus("stopped")).toBe("stopped")); - test("disconnected -> stopped", () => expect(mapStatus("disconnected")).toBe("stopped")); + test("running -> running", () => + expect(mapStatus("running")).toBe("running")); + test("stopped -> stopped", () => + expect(mapStatus("stopped")).toBe("stopped")); + test("disconnected -> stopped", () => + expect(mapStatus("disconnected")).toBe("stopped")); test("error -> failed", () => expect(mapStatus("error")).toBe("failed")); }); diff --git a/packages/tests/unit/cors-utils.test.ts b/packages/tests/unit/cors-utils.test.ts index b60aff3a4..bfedc644a 100644 --- a/packages/tests/unit/cors-utils.test.ts +++ b/packages/tests/unit/cors-utils.test.ts @@ -23,7 +23,9 @@ describe("getCorsHeaders", () => { it("reflects www subdomain as allowed origin", () => { const headers = getCorsHeaders("https://www.milady.ai"); - expect(headers["Access-Control-Allow-Origin"]).toBe("https://www.milady.ai"); + expect(headers["Access-Control-Allow-Origin"]).toBe( + "https://www.milady.ai", + ); }); it("sets Access-Control-Allow-Credentials for allowed origins", () => { @@ -79,7 +81,9 @@ describe("getCorsHeaders", () => { const headers = getCorsHeaders(null); expect(headers["Access-Control-Allow-Headers"]).toBeDefined(); expect(headers["Access-Control-Allow-Headers"]).toContain("X-API-Key"); - expect(headers["Access-Control-Allow-Headers"]).toContain("Authorization"); + expect(headers["Access-Control-Allow-Headers"]).toContain( + "Authorization", + ); expect(headers["Access-Control-Allow-Headers"]).toContain("Content-Type"); }); diff --git a/packages/tests/unit/credits.test.ts b/packages/tests/unit/credits.test.ts index c085596ec..574df5afe 100644 --- a/packages/tests/unit/credits.test.ts +++ b/packages/tests/unit/credits.test.ts @@ -3,7 +3,10 @@ */ import { beforeAll, describe, expect, test } from "bun:test"; -import type { CreditReservation, ReserveCreditsParams } from "@/lib/services/credits"; +import type { + CreditReservation, + ReserveCreditsParams, +} from "@/lib/services/credits"; type CreditsSnapshot = { constants: { @@ -109,8 +112,12 @@ describe("Credits Constants", () => { }); test("EPSILON is 10% of MIN_RESERVATION", () => { - expect(snapshot.constants.EPSILON).toBe(snapshot.constants.MIN_RESERVATION * 0.1); - expect(snapshot.constants.EPSILON).toBeLessThan(snapshot.constants.MIN_RESERVATION); + expect(snapshot.constants.EPSILON).toBe( + snapshot.constants.MIN_RESERVATION * 0.1, + ); + expect(snapshot.constants.EPSILON).toBeLessThan( + snapshot.constants.MIN_RESERVATION, + ); }); test("DEFAULT_OUTPUT_TOKENS is 500", () => { @@ -120,7 +127,9 @@ describe("Credits Constants", () => { describe("InsufficientCreditsError", () => { test("has correct name", () => { - expect(snapshot.insufficientCreditsError.name).toBe("InsufficientCreditsError"); + expect(snapshot.insufficientCreditsError.name).toBe( + "InsufficientCreditsError", + ); }); test("stores required amount", () => { @@ -188,7 +197,9 @@ describe("creditsService", () => { }); test("createAnonymousReservation method exists", () => { - expect(snapshot.creditsService.createAnonymousReservationType).toBe("function"); + expect(snapshot.creditsService.createAnonymousReservationType).toBe( + "function", + ); }); test("ReserveCreditsParams type is usable", () => { diff --git a/packages/tests/unit/cron-auth.test.ts b/packages/tests/unit/cron-auth.test.ts index ebeb24c7a..dac563a49 100644 --- a/packages/tests/unit/cron-auth.test.ts +++ b/packages/tests/unit/cron-auth.test.ts @@ -135,7 +135,12 @@ describe("verifyCronSecret", () => { }); it("accepts valid secret with various log prefixes", () => { - const prefixes = ["[AgentBudgets Cron]", "[Health Check Cron]", "[Test]", ""]; + const prefixes = [ + "[AgentBudgets Cron]", + "[Health Check Cron]", + "[Test]", + "", + ]; for (const prefix of prefixes) { const request = mockRequest(`Bearer ${TEST_SECRET}`); diff --git a/packages/tests/unit/database-url.test.ts b/packages/tests/unit/database-url.test.ts index 6e5c80d7b..1c21be32d 100644 --- a/packages/tests/unit/database-url.test.ts +++ b/packages/tests/unit/database-url.test.ts @@ -54,7 +54,9 @@ describe("database URL fallback", () => { DISABLE_LOCAL_DOCKER_DB_FALLBACK: undefined, }; - expect(resolveDatabaseUrl(env)).toBe("postgresql://test:pass@localhost:5432/test"); + expect(resolveDatabaseUrl(env)).toBe( + "postgresql://test:pass@localhost:5432/test", + ); }); test("exposes the canonical localhost Docker URL constant", () => { @@ -69,7 +71,9 @@ describe("database URL fallback", () => { LOCAL_DOCKER_DB_HOST: "docker.local", LOCAL_DOCKER_DB_PORT: "55432", }), - ).toBe("postgresql://eliza_dev:local_dev_password@docker.local:55432/eliza_dev"); + ).toBe( + "postgresql://eliza_dev:local_dev_password@docker.local:55432/eliza_dev", + ); }); test("does not fall back in CI or production", () => { @@ -108,7 +112,9 @@ describe("database URL fallback", () => { const applied = applyDatabaseUrlFallback(process.env as StringEnv); - expect(applied).toBe("postgresql://eliza_dev:local_dev_password@docker.test:5439/eliza_dev"); + expect(applied).toBe( + "postgresql://eliza_dev:local_dev_password@docker.test:5439/eliza_dev", + ); expect((process.env as StringEnv).DATABASE_URL).toBe( "postgresql://eliza_dev:local_dev_password@docker.test:5439/eliza_dev", ); diff --git a/packages/tests/unit/docker-infrastructure.test.ts b/packages/tests/unit/docker-infrastructure.test.ts index 6c1294f92..6304d5945 100644 --- a/packages/tests/unit/docker-infrastructure.test.ts +++ b/packages/tests/unit/docker-infrastructure.test.ts @@ -27,8 +27,10 @@ import { // Tests // --------------------------------------------------------------------------- -const sandboxProviderModuleUrl = new URL("../../lib/services/sandbox-provider.ts", import.meta.url) - .href; +const sandboxProviderModuleUrl = new URL( + "../../lib/services/sandbox-provider.ts", + import.meta.url, +).href; function runSandboxProviderFactory(providerEnv?: string) { const env = { ...process.env }; @@ -69,7 +71,9 @@ function runSandboxProviderFactory(providerEnv?: string) { expect(result.exitCode).toBe(0); expect(stderr).toBe(""); - return JSON.parse(stdout) as { ok: true; name: string | null } | { ok: false; message: string }; + return JSON.parse(stdout) as + | { ok: true; name: string | null } + | { ok: false; message: string }; } describe("Docker Infrastructure - Pure Functions", () => { @@ -140,7 +144,9 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("throws for shell injection chars (semicolon, space)", () => { - expect(() => validateAgentId("agent;rm -rf /")).toThrow(/Invalid agent ID/); + expect(() => validateAgentId("agent;rm -rf /")).toThrow( + /Invalid agent ID/, + ); }); test("throws for trailing newline", () => { @@ -197,15 +203,21 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("throws for control characters (null byte)", () => { - expect(() => validateAgentName("agent\x00name")).toThrow(/control characters/); + expect(() => validateAgentName("agent\x00name")).toThrow( + /control characters/, + ); }); test("throws for control characters (newline)", () => { - expect(() => validateAgentName("agent\nname")).toThrow(/control characters/); + expect(() => validateAgentName("agent\nname")).toThrow( + /control characters/, + ); }); test("throws for control characters (tab)", () => { - expect(() => validateAgentName("agent\tname")).toThrow(/control characters/); + expect(() => validateAgentName("agent\tname")).toThrow( + /control characters/, + ); }); }); @@ -218,26 +230,36 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("rejects empty keys", () => { - expect(() => validateEnvKey("")).toThrow(/Invalid environment variable key/); + expect(() => validateEnvKey("")).toThrow( + /Invalid environment variable key/, + ); }); test("rejects keys starting with a digit", () => { - expect(() => validateEnvKey("1SECRET")).toThrow(/Invalid environment variable key/); + expect(() => validateEnvKey("1SECRET")).toThrow( + /Invalid environment variable key/, + ); }); test("rejects punctuation", () => { - expect(() => validateEnvKey("JWT-SECRET")).toThrow(/Invalid environment variable key/); + expect(() => validateEnvKey("JWT-SECRET")).toThrow( + /Invalid environment variable key/, + ); }); test("rejects trailing newlines", () => { - expect(() => validateEnvKey("JWT_SECRET\n")).toThrow(/Invalid environment variable key/); + expect(() => validateEnvKey("JWT_SECRET\n")).toThrow( + /Invalid environment variable key/, + ); }); }); // ------------------------------------------------------------------------- describe("validateEnvValue", () => { test("accepts printable values", () => { - expect(() => validateEnvValue("JWT_SECRET", "hello-world_123")).not.toThrow(); + expect(() => + validateEnvValue("JWT_SECRET", "hello-world_123"), + ).not.toThrow(); }); test("accepts UUIDs, URLs, and base64-like tokens", () => { @@ -262,7 +284,9 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("rejects tabs", () => { - expect(() => validateEnvValue("JWT_SECRET", "abc\tdef")).toThrow(/control characters/); + expect(() => validateEnvValue("JWT_SECRET", "abc\tdef")).toThrow( + /control characters/, + ); }); }); @@ -270,7 +294,10 @@ describe("Docker Infrastructure - Pure Functions", () => { describe("resolveStewardContainerUrl", () => { test("preserves explicit STEWARD_CONTAINER_URL overrides", () => { expect( - resolveStewardContainerUrl("http://localhost:3200", "http://steward.internal:9999"), + resolveStewardContainerUrl( + "http://localhost:3200", + "http://steward.internal:9999", + ), ).toBe("http://steward.internal:9999"); }); @@ -284,14 +311,18 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("passes through non-loopback URLs unchanged", () => { - expect(resolveStewardContainerUrl("http://10.0.0.8:3200")).toBe("http://10.0.0.8:3200"); + expect(resolveStewardContainerUrl("http://10.0.0.8:3200")).toBe( + "http://10.0.0.8:3200", + ); }); }); // ------------------------------------------------------------------------- describe("requiresDockerHostGateway", () => { test("returns true for host.docker.internal", () => { - expect(requiresDockerHostGateway("http://host.docker.internal:3200")).toBe(true); + expect( + requiresDockerHostGateway("http://host.docker.internal:3200"), + ).toBe(true); }); test("returns false for non-host-gateway URLs", () => { @@ -318,9 +349,9 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("rejects non-hex output", () => { - expect(() => extractDockerCreateContainerId("container created successfully")).toThrow( - /invalid container id/, - ); + expect(() => + extractDockerCreateContainerId("container created successfully"), + ).toThrow(/invalid container id/); }); }); @@ -354,7 +385,9 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("throws when all ports in range are excluded", () => { - expect(() => allocatePort(10, 13, new Set([10, 11, 12]))).toThrow(/No available ports/); + expect(() => allocatePort(10, 13, new Set([10, 11, 12]))).toThrow( + /No available ports/, + ); }); test("works with an empty exclusion set", () => { @@ -380,9 +413,9 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("throws when the derived Docker container name would be too long", () => { - expect(() => getContainerName("a".repeat(MAX_AGENT_ID_LENGTH + 1))).toThrow( - /Invalid agent ID/, - ); + expect(() => + getContainerName("a".repeat(MAX_AGENT_ID_LENGTH + 1)), + ).toThrow(/Invalid agent ID/); }); test("rejects an agentId that would exceed the Docker container name limit", () => { @@ -398,7 +431,9 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("rejects invalid leading characters", () => { - expect(() => validateContainerName("-bad")).toThrow(/Invalid container name/); + expect(() => validateContainerName("-bad")).toThrow( + /Invalid container name/, + ); }); }); @@ -418,8 +453,12 @@ describe("Docker Infrastructure - Pure Functions", () => { }); test("rejects traversal and repeated separators", () => { - expect(() => validateVolumePath("/data//agents/test")).toThrow(/normalized/); - expect(() => validateVolumePath("/data/../agents/test")).toThrow(/normalized/); + expect(() => validateVolumePath("/data//agents/test")).toThrow( + /normalized/, + ); + expect(() => validateVolumePath("/data/../agents/test")).toThrow( + /normalized/, + ); }); }); @@ -468,7 +507,9 @@ describe("Docker Infrastructure - Pure Functions", () => { test("throws when env var is not set", () => { delete process.env.MILADY_DOCKER_NODES; - expect(() => parseDockerNodes()).toThrow(/MILADY_DOCKER_NODES env var is not set/); + expect(() => parseDockerNodes()).toThrow( + /MILADY_DOCKER_NODES env var is not set/, + ); }); test("throws when all entries are invalid", () => { @@ -509,7 +550,8 @@ describe("Docker Infrastructure - Pure Functions", () => { const result = runSandboxProviderFactory("unknown"); expect(result).toEqual({ ok: false, - message: 'Unknown sandbox provider: "unknown". Supported values: vercel, docker', + message: + 'Unknown sandbox provider: "unknown". Supported values: vercel, docker', }); }); diff --git a/packages/tests/unit/eliza-app/auth-endpoint.test.ts b/packages/tests/unit/eliza-app/auth-endpoint.test.ts index 577aa850b..ad2778fe0 100644 --- a/packages/tests/unit/eliza-app/auth-endpoint.test.ts +++ b/packages/tests/unit/eliza-app/auth-endpoint.test.ts @@ -345,7 +345,9 @@ describe("Telegram Auth Request Schema", () => { const result = telegramAuthSchema.safeParse(data); expect(result.success).toBe(true); if (result.success) { - expect((result.data as Record).extra_field).toBeUndefined(); + expect( + (result.data as Record).extra_field, + ).toBeUndefined(); } }); }); diff --git a/packages/tests/unit/eliza-app/connection-success-route.test.ts b/packages/tests/unit/eliza-app/connection-success-route.test.ts index f9e1d0561..b00c0f8db 100644 --- a/packages/tests/unit/eliza-app/connection-success-route.test.ts +++ b/packages/tests/unit/eliza-app/connection-success-route.test.ts @@ -6,11 +6,15 @@ import { GET as connectionSuccessGet } from "@/app/api/eliza-app/auth/connection describe("connection success route", () => { test("redirects web connections back to dashboard chat", async () => { const response = await connectionSuccessGet( - new NextRequest("https://elizacloud.ai/api/eliza-app/auth/connection-success?platform=web"), + new NextRequest( + "https://elizacloud.ai/api/eliza-app/auth/connection-success?platform=web", + ), ); expect(response.status).toBe(307); - expect(response.headers.get("location")).toBe("https://elizacloud.ai/dashboard/chat"); + expect(response.headers.get("location")).toBe( + "https://elizacloud.ai/dashboard/chat", + ); }); test("renders a platform-specific success page for messaging channels", async () => { diff --git a/packages/tests/unit/eliza-app/cross-platform-linking.test.ts b/packages/tests/unit/eliza-app/cross-platform-linking.test.ts index 95dde3836..038a906af 100644 --- a/packages/tests/unit/eliza-app/cross-platform-linking.test.ts +++ b/packages/tests/unit/eliza-app/cross-platform-linking.test.ts @@ -144,19 +144,23 @@ function updateUser(id: string, data: Partial): User { // Check unique constraints for updated fields if (data.telegram_id && data.telegram_id !== user.telegram_id) { const existing = findByTelegramId(data.telegram_id); - if (existing && existing.id !== id) throw new Error("unique constraint: telegram_id"); + if (existing && existing.id !== id) + throw new Error("unique constraint: telegram_id"); } if (data.discord_id && data.discord_id !== user.discord_id) { const existing = findByDiscordId(data.discord_id); - if (existing && existing.id !== id) throw new Error("unique constraint: discord_id"); + if (existing && existing.id !== id) + throw new Error("unique constraint: discord_id"); } if (data.whatsapp_id && data.whatsapp_id !== user.whatsapp_id) { const existing = findByWhatsAppId(data.whatsapp_id); - if (existing && existing.id !== id) throw new Error("unique constraint: whatsapp_id"); + if (existing && existing.id !== id) + throw new Error("unique constraint: whatsapp_id"); } if (data.phone_number && data.phone_number !== user.phone_number) { const existing = findByPhone(data.phone_number); - if (existing && existing.id !== id) throw new Error("unique constraint: phone_number"); + if (existing && existing.id !== id) + throw new Error("unique constraint: phone_number"); } Object.assign(user, data); @@ -200,14 +204,19 @@ function findOrCreateByTelegramWithPhone( updateUser(existingTelegram.id, { phone_number: phoneNumber, phone_verified: true, - telegram_username: telegramData.username || existingTelegram.telegram_username, + telegram_username: + telegramData.username || existingTelegram.telegram_username, telegram_first_name: telegramData.first_name, }); } else if (existingTelegram.phone_number !== phoneNumber) { throw new Error("PHONE_MISMATCH"); } const user = findById(existingTelegram.id)!; - return { user, organization: findOrgById(user.organization_id)!, isNew: false }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: false, + }; } // Step 2: Check by phone_number @@ -222,7 +231,11 @@ function findOrCreateByTelegramWithPhone( telegram_first_name: telegramData.first_name, }); const user = findById(existingPhone.id)!; - return { user, organization: findOrgById(user.organization_id)!, isNew: false }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: false, + }; } // Step 3: Create new @@ -234,7 +247,11 @@ function findOrCreateByTelegramWithPhone( phone_verified: true, name: telegramData.first_name, }); - return { user, organization: findOrgById(user.organization_id)!, isNew: true }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: true, + }; } /** @@ -272,7 +289,11 @@ function findOrCreateByDiscordId( updateUser(existingDiscord.id, updates); } const user = findById(existingDiscord.id)!; - return { user, organization: findOrgById(user.organization_id)!, isNew: false }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: false, + }; } // Step 2: Check by phone_number (NEW - cross-platform linking) @@ -289,7 +310,11 @@ function findOrCreateByDiscordId( discord_avatar_url: discordData.avatarUrl ?? null, }); const user = findById(existingPhone.id)!; - return { user, organization: findOrgById(user.organization_id)!, isNew: false }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: false, + }; } } @@ -304,7 +329,11 @@ function findOrCreateByDiscordId( phone_verified: !!phoneNumber, name: displayName, }); - return { user, organization: findOrgById(user.organization_id)!, isNew: true }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: true, + }; } /** @@ -313,7 +342,11 @@ function findOrCreateByDiscordId( function findOrCreateByPhone(phoneNumber: string): FindOrCreateResult { const existing = findByPhone(phoneNumber); if (existing) { - return { user: existing, organization: findOrgById(existing.organization_id)!, isNew: false }; + return { + user: existing, + organization: findOrgById(existing.organization_id)!, + isNew: false, + }; } const lastFour = phoneNumber.slice(-4); const user = createUser({ @@ -321,18 +354,28 @@ function findOrCreateByPhone(phoneNumber: string): FindOrCreateResult { phone_verified: true, name: `User ***${lastFour}`, }); - return { user, organization: findOrgById(user.organization_id)!, isNew: true }; + return { + user, + organization: findOrgById(user.organization_id)!, + isNew: true, + }; } /** * Simulates linkTelegramToUser from user-service.ts (session-based linking) */ -function linkTelegramToUser(userId: string, telegramData: TelegramData): LinkResult { +function linkTelegramToUser( + userId: string, + telegramData: TelegramData, +): LinkResult { const telegramId = String(telegramData.id); const existingTelegram = findByTelegramId(telegramId); if (existingTelegram && existingTelegram.id !== userId) { - return { success: false, error: "This Telegram account is already linked to another account" }; + return { + success: false, + error: "This Telegram account is already linked to another account", + }; } if (existingTelegram && existingTelegram.id === userId) { return { success: true }; // Idempotent @@ -346,7 +389,10 @@ function linkTelegramToUser(userId: string, telegramData: TelegramData): LinkRes }); return { success: true }; } catch { - return { success: false, error: "This Telegram account is already linked to another account" }; + return { + success: false, + error: "This Telegram account is already linked to another account", + }; } } @@ -365,7 +411,10 @@ function linkDiscordToUser( const existingDiscord = findByDiscordId(discordData.discordId); if (existingDiscord && existingDiscord.id !== userId) { - return { success: false, error: "This Discord account is already linked to another account" }; + return { + success: false, + error: "This Discord account is already linked to another account", + }; } if (existingDiscord && existingDiscord.id === userId) { return { success: true }; // Idempotent @@ -380,7 +429,10 @@ function linkDiscordToUser( }); return { success: true }; } catch { - return { success: false, error: "This Discord account is already linked to another account" }; + return { + success: false, + error: "This Discord account is already linked to another account", + }; } } @@ -391,14 +443,20 @@ function linkPhoneToUser(userId: string, phoneNumber: string): LinkResult { const existingPhone = findByPhone(phoneNumber); if (existingPhone) { if (existingPhone.id === userId) return { success: true }; - return { success: false, error: "This phone number is already linked to another account" }; + return { + success: false, + error: "This phone number is already linked to another account", + }; } try { updateUser(userId, { phone_number: phoneNumber, phone_verified: true }); return { success: true }; } catch { - return { success: false, error: "This phone number is already linked to another account" }; + return { + success: false, + error: "This phone number is already linked to another account", + }; } } @@ -406,7 +464,10 @@ function linkPhoneToUser(userId: string, phoneNumber: string): LinkResult { * Simulates findOrCreateByWhatsAppId from user-service.ts * 3-step lookup: whatsapp_id → phone (auto-derived) → create */ -function findOrCreateByWhatsAppId(whatsappId: string, profileName?: string): FindOrCreateResult { +function findOrCreateByWhatsAppId( + whatsappId: string, + profileName?: string, +): FindOrCreateResult { const derivedPhone = `+${whatsappId.replace(/\D/g, "")}`; // Step 1: Check by whatsapp_id @@ -456,7 +517,10 @@ function linkWhatsAppToUser( const existingWhatsApp = findByWhatsAppId(whatsappData.whatsappId); if (existingWhatsApp && existingWhatsApp.id !== userId) { - return { success: false, error: "This WhatsApp account is already linked to another account" }; + return { + success: false, + error: "This WhatsApp account is already linked to another account", + }; } if (existingWhatsApp && existingWhatsApp.id === userId) { return { success: true }; // Idempotent @@ -469,7 +533,10 @@ function linkWhatsAppToUser( }); return { success: true }; } catch { - return { success: false, error: "This WhatsApp account is already linked to another account" }; + return { + success: false, + error: "This WhatsApp account is already linked to another account", + }; } } @@ -477,8 +544,15 @@ function linkWhatsAppToUser( // Test data constants // ============================================================================= -const TELEGRAM_USER: TelegramData = { id: 12345678, first_name: "Alice", username: "alice_tg" }; -const DISCORD_USER: DiscordData = { username: "alice_discord", globalName: "Alice D" }; +const TELEGRAM_USER: TelegramData = { + id: 12345678, + first_name: "Alice", + username: "alice_tg", +}; +const DISCORD_USER: DiscordData = { + username: "alice_discord", + globalName: "Alice D", +}; const DISCORD_ID = "987654321012345678"; const PHONE = "+14155551234"; @@ -945,9 +1019,9 @@ describe("Cross-Platform Account Linking", () => { findOrCreateByTelegramWithPhone(TELEGRAM_USER, PHONE_A); // Try to re-auth with same Telegram but different phone → should throw PHONE_MISMATCH - expect(() => findOrCreateByTelegramWithPhone(TELEGRAM_USER, PHONE_B)).toThrow( - "PHONE_MISMATCH", - ); + expect(() => + findOrCreateByTelegramWithPhone(TELEGRAM_USER, PHONE_B), + ).toThrow("PHONE_MISMATCH"); expect(users.length).toBe(1); }); @@ -1083,9 +1157,9 @@ describe("Cross-Platform Account Linking", () => { findOrCreateByDiscordId(DISCORD_ID_A, { username: "userA" }, PHONE); - expect(() => findOrCreateByDiscordId(DISCORD_ID_B, { username: "userB" }, PHONE)).toThrow( - "DISCORD_ALREADY_LINKED", - ); + expect(() => + findOrCreateByDiscordId(DISCORD_ID_B, { username: "userB" }, PHONE), + ).toThrow("DISCORD_ALREADY_LINKED"); }); }); @@ -1154,7 +1228,10 @@ describe("Cross-Platform Account Linking", () => { }); test("links WhatsApp to existing phone user (Telegram-first)", () => { - const r1 = findOrCreateByTelegramWithPhone(TELEGRAM_USER, WA_DERIVED_PHONE); + const r1 = findOrCreateByTelegramWithPhone( + TELEGRAM_USER, + WA_DERIVED_PHONE, + ); expect(r1.user.whatsapp_id).toBeNull(); const r2 = findOrCreateByWhatsAppId(WA_ID, WA_NAME); @@ -1181,12 +1258,19 @@ describe("Cross-Platform Account Linking", () => { expect(r1.isNew).toBe(true); // Step 2: Telegram with same phone - const r2 = findOrCreateByTelegramWithPhone(TELEGRAM_USER, WA_DERIVED_PHONE); + const r2 = findOrCreateByTelegramWithPhone( + TELEGRAM_USER, + WA_DERIVED_PHONE, + ); expect(r2.isNew).toBe(false); expect(r2.user.id).toBe(r1.user.id); // Step 3: Discord with same phone - const r3 = findOrCreateByDiscordId(DISCORD_ID, DISCORD_USER, WA_DERIVED_PHONE); + const r3 = findOrCreateByDiscordId( + DISCORD_ID, + DISCORD_USER, + WA_DERIVED_PHONE, + ); expect(r3.isNew).toBe(false); expect(r3.user.id).toBe(r1.user.id); diff --git a/packages/tests/unit/eliza-app/deterministic-uuid.test.ts b/packages/tests/unit/eliza-app/deterministic-uuid.test.ts index 077603494..bbfb8dfb7 100644 --- a/packages/tests/unit/eliza-app/deterministic-uuid.test.ts +++ b/packages/tests/unit/eliza-app/deterministic-uuid.test.ts @@ -18,7 +18,8 @@ import { describe("generateDeterministicUUID", () => { test("produces valid UUID format", () => { const uuid = generateDeterministicUUID("test-input"); - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; expect(uuid).toMatch(uuidRegex); }); @@ -37,23 +38,33 @@ describe("generateDeterministicUUID", () => { test("handles empty string input", () => { const uuid = generateDeterministicUUID(""); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("handles unicode input", () => { const uuid = generateDeterministicUUID("用户测试🎉"); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("handles very long input", () => { const longInput = "x".repeat(10000); const uuid = generateDeterministicUUID(longInput); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("handles special characters", () => { - const uuid = generateDeterministicUUID("user@email.com+test/path?query=1&foo=bar"); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + const uuid = generateDeterministicUUID( + "user@email.com+test/path?query=1&foo=bar", + ); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("output is always lowercase", () => { @@ -67,12 +78,16 @@ describe("generateElizaAppRoomId", () => { test("generates valid UUID for Telegram room", () => { const roomId = generateElizaAppRoomId("telegram", agentId, "123456789"); - expect(roomId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(roomId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("generates valid UUID for iMessage room", () => { const roomId = generateElizaAppRoomId("imessage", agentId, "+14155551234"); - expect(roomId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(roomId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("same agent + user = same room ID", () => { @@ -94,8 +109,16 @@ describe("generateElizaAppRoomId", () => { }); test("different channels = different room IDs for same identifier", () => { - const telegramRoom = generateElizaAppRoomId("telegram", agentId, "123456789"); - const imessageRoom = generateElizaAppRoomId("imessage", agentId, "123456789"); + const telegramRoom = generateElizaAppRoomId( + "telegram", + agentId, + "123456789", + ); + const imessageRoom = generateElizaAppRoomId( + "imessage", + agentId, + "123456789", + ); expect(telegramRoom).not.toBe(imessageRoom); }); }); @@ -103,12 +126,16 @@ describe("generateElizaAppRoomId", () => { describe("generateElizaAppEntityId", () => { test("generates valid UUID for Telegram user", () => { const entityId = generateElizaAppEntityId("telegram", "123456789"); - expect(entityId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(entityId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("generates valid UUID for iMessage user", () => { const entityId = generateElizaAppEntityId("imessage", "+14155551234"); - expect(entityId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(entityId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("same identifier = same entity ID", () => { diff --git a/packages/tests/unit/eliza-app/discord-auth.test.ts b/packages/tests/unit/eliza-app/discord-auth.test.ts index ec9120dff..b6c97521d 100644 --- a/packages/tests/unit/eliza-app/discord-auth.test.ts +++ b/packages/tests/unit/eliza-app/discord-auth.test.ts @@ -18,7 +18,10 @@ import { readFileSync } from "fs"; import { join } from "path"; import { z } from "zod"; -import { isValidE164, normalizePhoneNumber } from "@/lib/utils/phone-normalization"; +import { + isValidE164, + normalizePhoneNumber, +} from "@/lib/utils/phone-normalization"; // ============================================================================= // DISCORD AUTH SERVICE - Pure logic tests @@ -26,7 +29,10 @@ import { isValidE164, normalizePhoneNumber } from "@/lib/utils/phone-normalizati describe("Discord Auth Service", () => { describe("getAvatarUrl", () => { - const getAvatarUrl = (userId: string, avatarHash: string | null): string | null => { + const getAvatarUrl = ( + userId: string, + avatarHash: string | null, + ): string | null => { if (!avatarHash) return null; const ext = avatarHash.startsWith("a_") ? "gif" : "png"; return `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.${ext}`; @@ -34,12 +40,16 @@ describe("Discord Auth Service", () => { test("generates correct static avatar URL", () => { const url = getAvatarUrl("123456789", "abcdef123456"); - expect(url).toBe("https://cdn.discordapp.com/avatars/123456789/abcdef123456.png"); + expect(url).toBe( + "https://cdn.discordapp.com/avatars/123456789/abcdef123456.png", + ); }); test("generates gif URL for animated avatars (a_ prefix)", () => { const url = getAvatarUrl("123456789", "a_abcdef123456"); - expect(url).toBe("https://cdn.discordapp.com/avatars/123456789/a_abcdef123456.gif"); + expect(url).toBe( + "https://cdn.discordapp.com/avatars/123456789/a_abcdef123456.gif", + ); }); test("returns null when avatarHash is null", () => { @@ -59,20 +69,29 @@ describe("Discord Auth Service", () => { }); describe("getDisplayName", () => { - const getDisplayName = (data: { global_name: string | null; username: string }): string => { + const getDisplayName = (data: { + global_name: string | null; + username: string; + }): string => { return data.global_name || data.username; }; test("uses global_name when available", () => { - expect(getDisplayName({ global_name: "Test User", username: "testuser" })).toBe("Test User"); + expect( + getDisplayName({ global_name: "Test User", username: "testuser" }), + ).toBe("Test User"); }); test("falls back to username when global_name is null", () => { - expect(getDisplayName({ global_name: null, username: "testuser" })).toBe("testuser"); + expect(getDisplayName({ global_name: null, username: "testuser" })).toBe( + "testuser", + ); }); test("falls back to username when global_name is empty", () => { - expect(getDisplayName({ global_name: "", username: "testuser" })).toBe("testuser"); + expect(getDisplayName({ global_name: "", username: "testuser" })).toBe( + "testuser", + ); }); }); @@ -89,15 +108,21 @@ describe("Discord Auth Service", () => { }; test("rejects bot accounts", () => { - expect(shouldReject({ id: "123", username: "bot", bot: true })).toBe(true); + expect(shouldReject({ id: "123", username: "bot", bot: true })).toBe( + true, + ); }); test("rejects system accounts", () => { - expect(shouldReject({ id: "123", username: "system", system: true })).toBe(true); + expect( + shouldReject({ id: "123", username: "system", system: true }), + ).toBe(true); }); test("rejects accounts that are both bot and system", () => { - expect(shouldReject({ id: "123", username: "both", bot: true, system: true })).toBe(true); + expect( + shouldReject({ id: "123", username: "both", bot: true, system: true }), + ).toBe(true); }); test("accepts normal user accounts", () => { @@ -105,11 +130,15 @@ describe("Discord Auth Service", () => { }); test("accepts when bot is explicitly false", () => { - expect(shouldReject({ id: "123", username: "user", bot: false })).toBe(false); + expect(shouldReject({ id: "123", username: "user", bot: false })).toBe( + false, + ); }); test("accepts when system is explicitly false", () => { - expect(shouldReject({ id: "123", username: "user", system: false })).toBe(false); + expect(shouldReject({ id: "123", username: "user", system: false })).toBe( + false, + ); }); }); @@ -373,7 +402,9 @@ describe("Discord Auth Request Schema - ACTUAL Zod + normalizePhoneNumber()", () }); expect(result.success).toBe(false); if (!result.success) { - const phoneIssue = result.error.issues.find((i) => i.path.includes("phone_number")); + const phoneIssue = result.error.issues.find((i) => + i.path.includes("phone_number"), + ); expect(phoneIssue).toBeDefined(); expect(phoneIssue!.message).toContain("Invalid phone number format"); } @@ -422,7 +453,10 @@ describe("Phone Linking Logic", () => { error?: string; } - const simulateLinkResult = (userId: string, existingOwner: string | null): LinkResult => { + const simulateLinkResult = ( + userId: string, + existingOwner: string | null, + ): LinkResult => { if (!existingOwner) return { success: true }; if (existingOwner === userId) return { success: true }; return { @@ -548,7 +582,9 @@ describe("Discord Auth Race Condition Handling", () => { test("ignores unrelated errors", () => { expect(isUniqueConstraintError(new Error("timeout"))).toBe(false); - expect(isUniqueConstraintError(new Error("connection refused"))).toBe(false); + expect(isUniqueConstraintError(new Error("connection refused"))).toBe( + false, + ); }); test("ignores non-Error objects", () => { @@ -578,7 +614,9 @@ describe("Discord Config Structure", () => { join(process.cwd(), "packages/lib/services/eliza-app/config.ts"), "utf-8", ); - expect(configSource).toContain('botToken: optionalRuntimeEnv("ELIZA_APP_DISCORD_BOT_TOKEN")'); + expect(configSource).toContain( + 'botToken: optionalRuntimeEnv("ELIZA_APP_DISCORD_BOT_TOKEN")', + ); expect(configSource).toContain( 'applicationId: optionalRuntimeEnv("ELIZA_APP_DISCORD_APPLICATION_ID")', ); @@ -592,7 +630,9 @@ describe("Discord Config Structure", () => { join(process.cwd(), "packages/lib/services/eliza-app/config.ts"), "utf-8", ); - expect(configSource).toContain('process.env.ELIZA_APP_DISCORD_ENABLED === "true"'); + expect(configSource).toContain( + 'process.env.ELIZA_APP_DISCORD_ENABLED === "true"', + ); expect(configSource).toContain( "Discord is enabled but required Discord env vars are not set in production", ); diff --git a/packages/tests/unit/eliza-app/discord-user-service.test.ts b/packages/tests/unit/eliza-app/discord-user-service.test.ts index c1038e2f5..446792a43 100644 --- a/packages/tests/unit/eliza-app/discord-user-service.test.ts +++ b/packages/tests/unit/eliza-app/discord-user-service.test.ts @@ -14,8 +14,13 @@ import { describe, expect, test } from "bun:test"; describe("Discord User Service", () => { describe("generateSlugFromDiscord", () => { // Replicating the slug generation logic for testing - const generateSlugFromDiscord = (username?: string, discordId?: string): string => { - const base = username ? username.toLowerCase().replace(/[^a-z0-9]/g, "-") : discordId; + const generateSlugFromDiscord = ( + username?: string, + discordId?: string, + ): string => { + const base = username + ? username.toLowerCase().replace(/[^a-z0-9]/g, "-") + : discordId; const random = Math.random().toString(36).substring(2, 8); const timestamp = Date.now().toString(36).slice(-4); return `discord-${base}-${timestamp}${random}`; @@ -95,14 +100,19 @@ describe("Discord User Service", () => { }); describe("Discord avatar URL generation", () => { - const generateAvatarUrl = (discordUserId: string, avatar: string | null): string | null => { + const generateAvatarUrl = ( + discordUserId: string, + avatar: string | null, + ): string | null => { if (!avatar) return null; return `https://cdn.discordapp.com/avatars/${discordUserId}/${avatar}.png`; }; test("generates correct avatar URL", () => { const url = generateAvatarUrl("123456789", "abcdef123456"); - expect(url).toBe("https://cdn.discordapp.com/avatars/123456789/abcdef123456.png"); + expect(url).toBe( + "https://cdn.discordapp.com/avatars/123456789/abcdef123456.png", + ); }); test("returns null when no avatar", () => { @@ -120,7 +130,10 @@ describe("Discord User Service", () => { }); describe("Display name resolution", () => { - const resolveDisplayName = (username: string, globalName?: string | null): string => { + const resolveDisplayName = ( + username: string, + globalName?: string | null, + ): string => { return globalName || username; }; @@ -154,11 +167,21 @@ describe("Discord User Service", () => { avatarUrl?: string | null; } - const needsProfileUpdate = (existing: ExistingUser, newData: NewUserData): boolean => { - if (newData.username && newData.username !== existing.discord_username) return true; - if (newData.globalName !== undefined && newData.globalName !== existing.discord_global_name) + const needsProfileUpdate = ( + existing: ExistingUser, + newData: NewUserData, + ): boolean => { + if (newData.username && newData.username !== existing.discord_username) return true; - if (newData.avatarUrl !== undefined && newData.avatarUrl !== existing.discord_avatar_url) + if ( + newData.globalName !== undefined && + newData.globalName !== existing.discord_global_name + ) + return true; + if ( + newData.avatarUrl !== undefined && + newData.avatarUrl !== existing.discord_avatar_url + ) return true; return false; }; @@ -170,7 +193,10 @@ describe("Discord User Service", () => { }); test("detects globalName change", () => { - const existing: ExistingUser = { discord_username: "user", discord_global_name: "Old Name" }; + const existing: ExistingUser = { + discord_username: "user", + discord_global_name: "Old Name", + }; const newData: NewUserData = { username: "user", globalName: "New Name" }; expect(needsProfileUpdate(existing, newData)).toBe(true); }); @@ -180,7 +206,10 @@ describe("Discord User Service", () => { discord_username: "user", discord_avatar_url: "https://old.png", }; - const newData: NewUserData = { username: "user", avatarUrl: "https://new.png" }; + const newData: NewUserData = { + username: "user", + avatarUrl: "https://new.png", + }; expect(needsProfileUpdate(existing, newData)).toBe(true); }); @@ -199,13 +228,19 @@ describe("Discord User Service", () => { }); test("handles null to value transition", () => { - const existing: ExistingUser = { discord_username: "user", discord_global_name: null }; + const existing: ExistingUser = { + discord_username: "user", + discord_global_name: null, + }; const newData: NewUserData = { username: "user", globalName: "New Name" }; expect(needsProfileUpdate(existing, newData)).toBe(true); }); test("handles value to null transition", () => { - const existing: ExistingUser = { discord_username: "user", discord_global_name: "Name" }; + const existing: ExistingUser = { + discord_username: "user", + discord_global_name: "Name", + }; const newData: NewUserData = { username: "user", globalName: null }; expect(needsProfileUpdate(existing, newData)).toBe(true); }); @@ -245,7 +280,9 @@ describe("Discord User Service", () => { test("returns false for non-Error objects", () => { expect(isUniqueConstraintError("string error")).toBe(false); - expect(isUniqueConstraintError({ message: "unique constraint" })).toBe(false); + expect(isUniqueConstraintError({ message: "unique constraint" })).toBe( + false, + ); expect(isUniqueConstraintError(null)).toBe(false); }); }); diff --git a/packages/tests/unit/eliza-app/discord-webhook.test.ts b/packages/tests/unit/eliza-app/discord-webhook.test.ts index e10de969b..9f5aa0091 100644 --- a/packages/tests/unit/eliza-app/discord-webhook.test.ts +++ b/packages/tests/unit/eliza-app/discord-webhook.test.ts @@ -120,13 +120,19 @@ describe("Discord Webhook", () => { test("detects message with attachments", () => { expect( - hasContent({ content: "", attachments: [{ url: "https://example.com/file.png" }] }), + hasContent({ + content: "", + attachments: [{ url: "https://example.com/file.png" }], + }), ).toBe(true); }); test("detects message with voice attachments", () => { expect( - hasContent({ content: "", voice_attachments: [{ url: "https://example.com/voice.ogg" }] }), + hasContent({ + content: "", + voice_attachments: [{ url: "https://example.com/voice.ogg" }], + }), ).toBe(true); }); @@ -140,7 +146,9 @@ describe("Discord Webhook", () => { }); test("detects completely empty message", () => { - expect(hasContent({ content: "", attachments: [], voice_attachments: [] })).toBe(false); + expect( + hasContent({ content: "", attachments: [], voice_attachments: [] }), + ).toBe(false); }); }); @@ -177,7 +185,9 @@ describe("Discord Webhook", () => { const media = processAttachments(attachments); expect(media).toHaveLength(1); - expect(media[0].url).toBe("https://cdn.discordapp.com/attachments/123/456/image.png"); + expect(media[0].url).toBe( + "https://cdn.discordapp.com/attachments/123/456/image.png", + ); expect(media[0].contentType).toBe("image/png"); expect(media[0].title).toBe("image.png"); }); @@ -210,9 +220,21 @@ describe("Discord Webhook", () => { test("processes multiple attachments", () => { const attachments: DiscordAttachment[] = [ - { url: "https://example.com/1.png", content_type: "image/png", filename: "1.png" }, - { url: "https://example.com/2.jpg", content_type: "image/jpeg", filename: "2.jpg" }, - { url: "https://example.com/3.pdf", content_type: "application/pdf", filename: "3.pdf" }, + { + url: "https://example.com/1.png", + content_type: "image/png", + filename: "1.png", + }, + { + url: "https://example.com/2.jpg", + content_type: "image/jpeg", + filename: "2.jpg", + }, + { + url: "https://example.com/3.pdf", + content_type: "application/pdf", + filename: "3.pdf", + }, ]; const media = processAttachments(attachments); @@ -242,7 +264,9 @@ describe("Discord Webhook", () => { title?: string; } - const processVoiceAttachments = (voiceAttachments: VoiceAttachment[]): Media[] => { + const processVoiceAttachments = ( + voiceAttachments: VoiceAttachment[], + ): Media[] => { return voiceAttachments.map((va) => ({ url: va.url, contentType: va.content_type, @@ -431,7 +455,9 @@ describe("Discord Webhook", () => { expect(data.discordId).toBe("123456789"); expect(data.username).toBe("testuser"); expect(data.globalName).toBe("Test User"); - expect(data.avatarUrl).toBe("https://cdn.discordapp.com/avatars/123456789/abcdef123456.png"); + expect(data.avatarUrl).toBe( + "https://cdn.discordapp.com/avatars/123456789/abcdef123456.png", + ); }); test("handles missing global_name", () => { @@ -542,7 +568,12 @@ describe("Discord Event Payload Validation", () => { test("rejects payload missing event_type", () => { const payload = { event_id: "123", - data: { id: "1", channel_id: "2", author: { id: "3", username: "u" }, content: "x" }, + data: { + id: "1", + channel_id: "2", + author: { id: "3", username: "u" }, + content: "x", + }, }; expect(isValidPayload(payload)).toBe(false); }); @@ -550,7 +581,12 @@ describe("Discord Event Payload Validation", () => { test("rejects payload missing event_id", () => { const payload = { event_type: "MESSAGE_CREATE", - data: { id: "1", channel_id: "2", author: { id: "3", username: "u" }, content: "x" }, + data: { + id: "1", + channel_id: "2", + author: { id: "3", username: "u" }, + content: "x", + }, }; expect(isValidPayload(payload)).toBe(false); }); diff --git a/packages/tests/unit/eliza-app/oauth-enforcement.test.ts b/packages/tests/unit/eliza-app/oauth-enforcement.test.ts index bccd1e86f..7175d5f75 100644 --- a/packages/tests/unit/eliza-app/oauth-enforcement.test.ts +++ b/packages/tests/unit/eliza-app/oauth-enforcement.test.ts @@ -13,8 +13,15 @@ import { describe, expect, test } from "bun:test"; import { createHash } from "crypto"; import { readFileSync } from "fs"; import { join } from "path"; -import { generateElizaAppEntityId, generateElizaAppRoomId } from "@/lib/utils/deterministic-uuid"; -import { isValidEmail, maskEmailForLogging, normalizeEmail } from "@/lib/utils/email-validation"; +import { + generateElizaAppEntityId, + generateElizaAppRoomId, +} from "@/lib/utils/deterministic-uuid"; +import { + isValidEmail, + maskEmailForLogging, + normalizeEmail, +} from "@/lib/utils/email-validation"; import { isValidE164, normalizePhoneNumber, @@ -33,9 +40,12 @@ function extractMessagesFromWebhook(): { const telegramMatch = telegramWebhook.match( /Welcome! To chat with Eliza, please connect your Telegram first[\s\S]*?get-started/, ); - const hasTelegramEmoji = telegramWebhook.includes("👋") && (telegramMatch?.length ?? 0) > 0; + const hasTelegramEmoji = + telegramWebhook.includes("👋") && (telegramMatch?.length ?? 0) > 0; - const statusMatch = telegramWebhook.match(/Not connected yet[\s\S]*?get-started/); + const statusMatch = telegramWebhook.match( + /Not connected yet[\s\S]*?get-started/, + ); return { telegramRejection: @@ -93,7 +103,13 @@ describe("Phone Normalization - ACTUAL normalizePhoneNumber()", () => { }); test("same phone in different formats normalizes to same value", () => { - const formats = ["+14155551234", "14155551234", "4155551234", "(415) 555-1234", "415-555-1234"]; + const formats = [ + "+14155551234", + "14155551234", + "4155551234", + "(415) 555-1234", + "415-555-1234", + ]; const normalized = formats.map((f) => normalizePhoneNumber(f)); const unique = [...new Set(normalized)]; @@ -144,25 +160,55 @@ describe("Room ID Generation - ACTUAL generateElizaAppRoomId()", () => { const TEST_AGENT_ID = "b850bc30-45f8-0041-a00a-83df46d8555d"; test("generates valid UUID format", () => { - const roomId = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "123456789"); - expect(roomId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + const roomId = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "123456789", + ); + expect(roomId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("same inputs produce same room ID (deterministic)", () => { - const roomId1 = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "123456789"); - const roomId2 = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "123456789"); + const roomId1 = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "123456789", + ); + const roomId2 = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "123456789", + ); expect(roomId1).toBe(roomId2); }); test("different users get different room IDs", () => { - const roomId1 = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "111111111"); - const roomId2 = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "222222222"); + const roomId1 = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "111111111", + ); + const roomId2 = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "222222222", + ); expect(roomId1).not.toBe(roomId2); }); test("same user on different platforms gets different room IDs", () => { - const telegramRoom = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "user123"); - const imessageRoom = generateElizaAppRoomId("imessage", TEST_AGENT_ID, "user123"); + const telegramRoom = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "user123", + ); + const imessageRoom = generateElizaAppRoomId( + "imessage", + TEST_AGENT_ID, + "user123", + ); expect(telegramRoom).not.toBe(imessageRoom); }); @@ -172,7 +218,11 @@ describe("Room ID Generation - ACTUAL generateElizaAppRoomId()", () => { const expectedHash = createHash("sha256").update(input).digest("hex"); const expectedUuid = `${expectedHash.slice(0, 8)}-${expectedHash.slice(8, 12)}-${expectedHash.slice(12, 16)}-${expectedHash.slice(16, 20)}-${expectedHash.slice(20, 32)}`; - const actualRoomId = generateElizaAppRoomId("telegram", TEST_AGENT_ID, "123456789"); + const actualRoomId = generateElizaAppRoomId( + "telegram", + TEST_AGENT_ID, + "123456789", + ); expect(actualRoomId).toBe(expectedUuid); }); }); @@ -180,7 +230,9 @@ describe("Room ID Generation - ACTUAL generateElizaAppRoomId()", () => { describe("Entity ID Generation - ACTUAL generateElizaAppEntityId()", () => { test("generates valid UUID format", () => { const entityId = generateElizaAppEntityId("telegram", "223116693"); - expect(entityId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + expect(entityId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); }); test("same identifier produces same entity ID", () => { @@ -203,7 +255,9 @@ describe("Rejection Messages - VERIFIED AGAINST ACTUAL WEBHOOK CODE", () => { test("Telegram rejection message exists in webhook (OAuth enforcement)", () => { expect(messages.telegramRejection).not.toBe(""); - expect(messages.telegramRejection.toLowerCase()).toContain("connect your telegram"); + expect(messages.telegramRejection.toLowerCase()).toContain( + "connect your telegram", + ); }); test("Telegram rejection message includes get-started URL", () => { @@ -212,7 +266,9 @@ describe("Rejection Messages - VERIFIED AGAINST ACTUAL WEBHOOK CODE", () => { test("Status not connected message exists in webhook", () => { expect(messages.statusNotConnected).not.toBe(""); - expect(messages.statusNotConnected.toLowerCase()).toContain("not connected"); + expect(messages.statusNotConnected.toLowerCase()).toContain( + "not connected", + ); }); test("Telegram rejection message is welcoming (contains emoji)", () => { @@ -252,7 +308,10 @@ describe("Unified EntityId Architecture", () => { const telegramUserId = "223116693"; const phoneNumber = "+14155551234"; - const oldTelegramEntityId = generateElizaAppEntityId("telegram", telegramUserId); + const oldTelegramEntityId = generateElizaAppEntityId( + "telegram", + telegramUserId, + ); const oldPhoneEntityId = generateElizaAppEntityId("imessage", phoneNumber); // They would be different - which is the BUG we fixed @@ -302,7 +361,8 @@ describe("Command Detection - Verified Against Webhook", () => { test("command detection matches webhook logic", () => { const isCommand = (text: string) => text.startsWith("/"); - const parseCommand = (text: string) => text.trim().split(" ")[0].toLowerCase(); + const parseCommand = (text: string) => + text.trim().split(" ")[0].toLowerCase(); expect(isCommand("/start")).toBe(true); expect(isCommand("/help")).toBe(true); @@ -414,7 +474,9 @@ describe("Email Validation - ACTUAL isValidEmail()", () => { describe("Email Normalization - ACTUAL normalizeEmail()", () => { test("lowercases and trims", () => { expect(normalizeEmail("User@Example.COM")).toBe("user@example.com"); - expect(normalizeEmail(" BERTA.BENJAMIN@gmail.com ")).toBe("berta.benjamin@gmail.com"); + expect(normalizeEmail(" BERTA.BENJAMIN@gmail.com ")).toBe( + "berta.benjamin@gmail.com", + ); expect(normalizeEmail("TEST@GMAIL.COM")).toBe("test@gmail.com"); }); }); @@ -422,7 +484,9 @@ describe("Email Normalization - ACTUAL normalizeEmail()", () => { describe("Email Masking - ACTUAL maskEmailForLogging()", () => { test("standard emails (5+ char prefix)", () => { expect(maskEmailForLogging("benjamin@gmail.com")).toBe("be***in@gmail.com"); - expect(maskEmailForLogging("berta.benjamin@icloud.com")).toBe("be***in@icloud.com"); + expect(maskEmailForLogging("berta.benjamin@icloud.com")).toBe( + "be***in@icloud.com", + ); }); test("short prefixes (3-4 chars)", () => { diff --git a/packages/tests/unit/eliza-app/session-service.test.ts b/packages/tests/unit/eliza-app/session-service.test.ts index 4c017467b..96b60c95a 100644 --- a/packages/tests/unit/eliza-app/session-service.test.ts +++ b/packages/tests/unit/eliza-app/session-service.test.ts @@ -239,7 +239,9 @@ describe("JWT Token Validation", () => { }); test("token signed with wrong key is rejected", async () => { - const wrongKey = new TextEncoder().encode("wrong-secret-key-32-chars-long!!"); + const wrongKey = new TextEncoder().encode( + "wrong-secret-key-32-chars-long!!", + ); const token = await new SignJWT({ userId: "user-123", diff --git a/packages/tests/unit/eliza-app/telegram-auth.test.ts b/packages/tests/unit/eliza-app/telegram-auth.test.ts index 57427cbf7..9dce50795 100644 --- a/packages/tests/unit/eliza-app/telegram-auth.test.ts +++ b/packages/tests/unit/eliza-app/telegram-auth.test.ts @@ -19,14 +19,19 @@ const TEST_BOT_TOKEN = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"; /** * Generate valid Telegram auth hash for testing */ -function generateValidHash(data: Record, botToken: string): string { +function generateValidHash( + data: Record, + botToken: string, +): string { const secretKey = createHash("sha256").update(botToken).digest(); const entries = Object.entries(data) .filter(([key]) => key !== "hash") .sort(([a], [b]) => a.localeCompare(b)); - const checkString = entries.map(([key, value]) => `${key}=${value}`).join("\n"); + const checkString = entries + .map(([key, value]) => `${key}=${value}`) + .join("\n"); return createHmac("sha256", secretKey).update(checkString).digest("hex"); } @@ -65,7 +70,10 @@ describe("Telegram Auth Hash Generation", () => { test("different bot token produces different hash", () => { const data = { id: 123, first_name: "Test", auth_date: 1700000000 }; const hash1 = generateValidHash(data, TEST_BOT_TOKEN); - const hash2 = generateValidHash(data, "987654321:ZYXwvuTSRqponMLKjihGFEdcba"); + const hash2 = generateValidHash( + data, + "987654321:ZYXwvuTSRqponMLKjihGFEdcba", + ); expect(hash1).not.toBe(hash2); }); diff --git a/packages/tests/unit/eliza-app/webhook-validation.test.ts b/packages/tests/unit/eliza-app/webhook-validation.test.ts index 6e0d0da09..2f485c3fd 100644 --- a/packages/tests/unit/eliza-app/webhook-validation.test.ts +++ b/packages/tests/unit/eliza-app/webhook-validation.test.ts @@ -121,7 +121,10 @@ describe("Blooio Signature Verification", () => { } test("valid signature passes verification", () => { - const payload = JSON.stringify({ event: "message.received", text: "Hello" }); + const payload = JSON.stringify({ + event: "message.received", + text: "Hello", + }); const signature = generateBlooioSignature(payload, WEBHOOK_SECRET); const expectedSignature = generateBlooioSignature(payload, WEBHOOK_SECRET); @@ -129,11 +132,23 @@ describe("Blooio Signature Verification", () => { }); test("tampered payload fails verification", () => { - const originalPayload = JSON.stringify({ event: "message.received", text: "Hello" }); - const tamperedPayload = JSON.stringify({ event: "message.received", text: "Hacked" }); + const originalPayload = JSON.stringify({ + event: "message.received", + text: "Hello", + }); + const tamperedPayload = JSON.stringify({ + event: "message.received", + text: "Hacked", + }); - const originalSignature = generateBlooioSignature(originalPayload, WEBHOOK_SECRET); - const tamperedSignature = generateBlooioSignature(tamperedPayload, WEBHOOK_SECRET); + const originalSignature = generateBlooioSignature( + originalPayload, + WEBHOOK_SECRET, + ); + const tamperedSignature = generateBlooioSignature( + tamperedPayload, + WEBHOOK_SECRET, + ); expect(originalSignature).not.toBe(tamperedSignature); }); diff --git a/packages/tests/unit/eliza-app/whatsapp-auth.test.ts b/packages/tests/unit/eliza-app/whatsapp-auth.test.ts index de5c6a7af..2c2017dfe 100644 --- a/packages/tests/unit/eliza-app/whatsapp-auth.test.ts +++ b/packages/tests/unit/eliza-app/whatsapp-auth.test.ts @@ -35,7 +35,9 @@ describe("WhatsApp Webhook Signature Verification", () => { test("rejects tampered payload", () => { const body = '{"object":"whatsapp_business_account","entry":[]}'; const sig = makeSignature(body, TEST_APP_SECRET); - expect(verifyWhatsAppSignature(TEST_APP_SECRET, sig, body + "x")).toBe(false); + expect(verifyWhatsAppSignature(TEST_APP_SECRET, sig, body + "x")).toBe( + false, + ); }); test("rejects empty signature header", () => { @@ -64,7 +66,9 @@ describe("WhatsApp Webhook Signature Verification", () => { test("timing-safe comparison prevents length-based detection", () => { const body = '{"test":true}'; // Create a signature that differs in length by having non-hex chars - expect(verifyWhatsAppSignature(TEST_APP_SECRET, "sha256=short", body)).toBe(false); + expect(verifyWhatsAppSignature(TEST_APP_SECRET, "sha256=short", body)).toBe( + false, + ); }); }); @@ -78,7 +82,8 @@ describe("WhatsApp Webhook Subscription Verification Logic", () => { const challenge = "1234567890"; // Simulating the auth service logic - const isValid = mode === "subscribe" && verifyToken === expectedToken && !!challenge; + const isValid = + mode === "subscribe" && verifyToken === expectedToken && !!challenge; expect(isValid).toBe(true); }); @@ -87,7 +92,8 @@ describe("WhatsApp Webhook Subscription Verification Logic", () => { const verifyToken = TEST_VERIFY_TOKEN; const challenge = "1234567890"; - const isValid = mode === "subscribe" && verifyToken === TEST_VERIFY_TOKEN && !!challenge; + const isValid = + mode === "subscribe" && verifyToken === TEST_VERIFY_TOKEN && !!challenge; expect(isValid).toBe(false); }); @@ -96,7 +102,8 @@ describe("WhatsApp Webhook Subscription Verification Logic", () => { const verifyToken = "wrong_token" as string; const challenge = "1234567890"; - const isValid = mode === "subscribe" && verifyToken === TEST_VERIFY_TOKEN && !!challenge; + const isValid = + mode === "subscribe" && verifyToken === TEST_VERIFY_TOKEN && !!challenge; expect(isValid).toBe(false); }); @@ -105,7 +112,8 @@ describe("WhatsApp Webhook Subscription Verification Logic", () => { const verifyToken = TEST_VERIFY_TOKEN; const challenge = ""; - const isValid = mode === "subscribe" && verifyToken === TEST_VERIFY_TOKEN && !!challenge; + const isValid = + mode === "subscribe" && verifyToken === TEST_VERIFY_TOKEN && !!challenge; expect(isValid).toBe(false); }); }); diff --git a/packages/tests/unit/eliza-app/whatsapp-webhook.test.ts b/packages/tests/unit/eliza-app/whatsapp-webhook.test.ts index 8fed5a944..de6c94877 100644 --- a/packages/tests/unit/eliza-app/whatsapp-webhook.test.ts +++ b/packages/tests/unit/eliza-app/whatsapp-webhook.test.ts @@ -34,34 +34,61 @@ describe("WhatsApp Webhook GET (Verification Handshake)", () => { } test("returns challenge when verification succeeds", () => { - const result = simulateVerification("subscribe", VERIFY_TOKEN, "123456", VERIFY_TOKEN); + const result = simulateVerification( + "subscribe", + VERIFY_TOKEN, + "123456", + VERIFY_TOKEN, + ); expect(result).toBe("123456"); }); test("returns null for wrong mode", () => { - const result = simulateVerification("unsubscribe", VERIFY_TOKEN, "123456", VERIFY_TOKEN); + const result = simulateVerification( + "unsubscribe", + VERIFY_TOKEN, + "123456", + VERIFY_TOKEN, + ); expect(result).toBeNull(); }); test("returns null for wrong verify token", () => { - const result = simulateVerification("subscribe", "wrong_token", "123456", VERIFY_TOKEN); + const result = simulateVerification( + "subscribe", + "wrong_token", + "123456", + VERIFY_TOKEN, + ); expect(result).toBeNull(); }); test("returns null for missing challenge", () => { - const result = simulateVerification("subscribe", VERIFY_TOKEN, null, VERIFY_TOKEN); + const result = simulateVerification( + "subscribe", + VERIFY_TOKEN, + null, + VERIFY_TOKEN, + ); expect(result).toBeNull(); }); test("returns null for null mode", () => { - const result = simulateVerification(null, VERIFY_TOKEN, "123456", VERIFY_TOKEN); + const result = simulateVerification( + null, + VERIFY_TOKEN, + "123456", + VERIFY_TOKEN, + ); expect(result).toBeNull(); }); }); describe("WhatsApp Webhook POST (Message Processing)", () => { function makeSignature(body: string): string { - return "sha256=" + createHmac("sha256", APP_SECRET).update(body).digest("hex"); + return ( + "sha256=" + createHmac("sha256", APP_SECRET).update(body).digest("hex") + ); } test("validates signature and extracts messages", () => { @@ -78,7 +105,9 @@ describe("WhatsApp Webhook POST (Message Processing)", () => { display_phone_number: "+14245074963", phone_number_id: "phone_123", }, - contacts: [{ profile: { name: "Test User" }, wa_id: "14155551234" }], + contacts: [ + { profile: { name: "Test User" }, wa_id: "14155551234" }, + ], messages: [ { id: "wamid.test123", @@ -116,8 +145,11 @@ describe("WhatsApp Webhook POST (Message Processing)", () => { test("rejects message with invalid signature", () => { const body = '{"object":"whatsapp_business_account","entry":[]}'; - const fakeSignature = "sha256=0000000000000000000000000000000000000000000000000000000000000000"; - expect(verifyWhatsAppSignature(APP_SECRET, fakeSignature, body)).toBe(false); + const fakeSignature = + "sha256=0000000000000000000000000000000000000000000000000000000000000000"; + expect(verifyWhatsAppSignature(APP_SECRET, fakeSignature, body)).toBe( + false, + ); }); test("handles payload with no messages (status update)", () => { @@ -168,7 +200,9 @@ describe("WhatsApp Webhook POST (Message Processing)", () => { display_phone_number: "+14245074963", phone_number_id: "phone_123", }, - contacts: [{ profile: { name: "Photo User" }, wa_id: "14155551234" }], + contacts: [ + { profile: { name: "Photo User" }, wa_id: "14155551234" }, + ], messages: [ { id: "wamid.img123", diff --git a/packages/tests/unit/engagement-metrics/cache-keys.test.ts b/packages/tests/unit/engagement-metrics/cache-keys.test.ts index b0e5a2520..a56c2122c 100644 --- a/packages/tests/unit/engagement-metrics/cache-keys.test.ts +++ b/packages/tests/unit/engagement-metrics/cache-keys.test.ts @@ -10,9 +10,15 @@ import { CacheKeys, CacheStaleTTL, CacheTTL } from "@/lib/cache/keys"; describe("CacheKeys.userMetrics", () => { test("overview returns a versioned key scoped by range", () => { - expect(CacheKeys.userMetrics.overview()).toBe("user-metrics:overview:30d:v1"); - expect(CacheKeys.userMetrics.overview(7)).toBe("user-metrics:overview:7d:v1"); - expect(CacheKeys.userMetrics.overview(90)).toBe("user-metrics:overview:90d:v1"); + expect(CacheKeys.userMetrics.overview()).toBe( + "user-metrics:overview:30d:v1", + ); + expect(CacheKeys.userMetrics.overview(7)).toBe( + "user-metrics:overview:7d:v1", + ); + expect(CacheKeys.userMetrics.overview(90)).toBe( + "user-metrics:overview:90d:v1", + ); }); test("daily returns a key scoped by date range", () => { @@ -32,9 +38,15 @@ describe("CacheKeys.userMetrics", () => { }); test("activeUsers returns a key scoped by time range", () => { - expect(CacheKeys.userMetrics.activeUsers("day")).toBe("user-metrics:active:day:v1"); - expect(CacheKeys.userMetrics.activeUsers("7d")).toBe("user-metrics:active:7d:v1"); - expect(CacheKeys.userMetrics.activeUsers("30d")).toBe("user-metrics:active:30d:v1"); + expect(CacheKeys.userMetrics.activeUsers("day")).toBe( + "user-metrics:active:day:v1", + ); + expect(CacheKeys.userMetrics.activeUsers("7d")).toBe( + "user-metrics:active:7d:v1", + ); + expect(CacheKeys.userMetrics.activeUsers("30d")).toBe( + "user-metrics:active:30d:v1", + ); }); test("pattern returns a wildcard for invalidation", () => { @@ -73,8 +85,12 @@ describe("CacheTTL.userMetrics", () => { }); test("pre-computed TTLs are longer than live query TTLs", () => { - expect(CacheTTL.userMetrics.daily).toBeGreaterThan(CacheTTL.userMetrics.overview); - expect(CacheTTL.userMetrics.retention).toBeGreaterThan(CacheTTL.userMetrics.activeUsers); + expect(CacheTTL.userMetrics.daily).toBeGreaterThan( + CacheTTL.userMetrics.overview, + ); + expect(CacheTTL.userMetrics.retention).toBeGreaterThan( + CacheTTL.userMetrics.activeUsers, + ); }); }); @@ -88,7 +104,11 @@ describe("CacheStaleTTL.userMetrics", () => { }); test("stale TTLs are shorter than their corresponding TTLs", () => { - expect(CacheStaleTTL.userMetrics.overview).toBeLessThan(CacheTTL.userMetrics.overview); - expect(CacheStaleTTL.userMetrics.activeUsers).toBeLessThan(CacheTTL.userMetrics.activeUsers); + expect(CacheStaleTTL.userMetrics.overview).toBeLessThan( + CacheTTL.userMetrics.overview, + ); + expect(CacheStaleTTL.userMetrics.activeUsers).toBeLessThan( + CacheTTL.userMetrics.activeUsers, + ); }); }); diff --git a/packages/tests/unit/engagement-metrics/schemas.test.ts b/packages/tests/unit/engagement-metrics/schemas.test.ts index 5f954c59c..e19c89bd5 100644 --- a/packages/tests/unit/engagement-metrics/schemas.test.ts +++ b/packages/tests/unit/engagement-metrics/schemas.test.ts @@ -62,13 +62,17 @@ describe("dailyMetrics schema", () => { }); test("has a unique index on (date, platform)", () => { - const idx = config.indexes.find((i) => i.config.name === "daily_metrics_date_platform_idx"); + const idx = config.indexes.find( + (i) => i.config.name === "daily_metrics_date_platform_idx", + ); expect(idx).toBeDefined(); expect(idx!.config.unique).toBe(true); }); test("has a non-unique index on (date)", () => { - const idx = config.indexes.find((i) => i.config.name === "daily_metrics_date_idx"); + const idx = config.indexes.find( + (i) => i.config.name === "daily_metrics_date_idx", + ); expect(idx).toBeDefined(); expect(idx!.config.unique).toBeFalsy(); }); diff --git a/packages/tests/unit/env-validator.test.ts b/packages/tests/unit/env-validator.test.ts index 5ad6df00d..0ee635e04 100644 --- a/packages/tests/unit/env-validator.test.ts +++ b/packages/tests/unit/env-validator.test.ts @@ -26,10 +26,14 @@ describe("validateEnvironment", () => { const result = validateEnvironment(); expect(result.errors).toEqual( - expect.arrayContaining([expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET" })]), + expect.arrayContaining([ + expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET" }), + ]), ); expect(result.warnings).not.toEqual( - expect.arrayContaining([expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET" })]), + expect.arrayContaining([ + expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET" }), + ]), ); }); @@ -40,10 +44,14 @@ describe("validateEnvironment", () => { const result = validateEnvironment(); expect(result.errors).toEqual( - expect.arrayContaining([expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET_MAX" })]), + expect.arrayContaining([ + expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET_MAX" }), + ]), ); expect(result.warnings).not.toEqual( - expect.arrayContaining([expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET_MAX" })]), + expect.arrayContaining([ + expect.objectContaining({ variable: "ANTHROPIC_COT_BUDGET_MAX" }), + ]), ); }); }); diff --git a/packages/tests/unit/idempotency.test.ts b/packages/tests/unit/idempotency.test.ts index eacd9f725..40a804726 100644 --- a/packages/tests/unit/idempotency.test.ts +++ b/packages/tests/unit/idempotency.test.ts @@ -19,279 +19,275 @@ import { tryClaimForProcessing, } from "@/lib/utils/idempotency"; -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "Idempotency Utility", - () => { - // Use unique keys for each test to avoid conflicts - const testPrefix = `test-${Date.now()}`; - - afterAll(async () => { - // Clean up after all tests - try { - await clearProcessedMessages(); - } catch { - // Ignore cleanup errors in tests - } +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("Idempotency Utility", () => { + // Use unique keys for each test to avoid conflicts + const testPrefix = `test-${Date.now()}`; + + afterAll(async () => { + // Clean up after all tests + try { + await clearProcessedMessages(); + } catch { + // Ignore cleanup errors in tests + } + }); + + describe("isAlreadyProcessed", () => { + it("returns false for new keys", async () => { + const key = `${testPrefix}:new-key-${Date.now()}`; + const result = await isAlreadyProcessed(key); + expect(result).toBe(false); }); - describe("isAlreadyProcessed", () => { - it("returns false for new keys", async () => { - const key = `${testPrefix}:new-key-${Date.now()}`; - const result = await isAlreadyProcessed(key); - expect(result).toBe(false); - }); + it("returns true for processed keys", async () => { + const key = `${testPrefix}:processed-key-${Date.now()}`; - it("returns true for processed keys", async () => { - const key = `${testPrefix}:processed-key-${Date.now()}`; + // Mark as processed + await markAsProcessed(key, "test"); - // Mark as processed - await markAsProcessed(key, "test"); - - // Check if already processed - const result = await isAlreadyProcessed(key); - expect(result).toBe(true); - }); - - it("handles empty key gracefully", async () => { - const result = await isAlreadyProcessed(""); - expect(typeof result).toBe("boolean"); - }); - - it("handles special characters in keys", async () => { - const key = `${testPrefix}:special-!@#$%^&*()-${Date.now()}`; - expect(await isAlreadyProcessed(key)).toBe(false); - - await markAsProcessed(key, "test"); - expect(await isAlreadyProcessed(key)).toBe(true); - }); + // Check if already processed + const result = await isAlreadyProcessed(key); + expect(result).toBe(true); }); - describe("markAsProcessed", () => { - it("marks a key as processed", async () => { - const key = `${testPrefix}:mark-test-${Date.now()}`; - - // Initially not processed - expect(await isAlreadyProcessed(key)).toBe(false); - - // Mark as processed - await markAsProcessed(key, "blooio"); - - // Now should be processed - expect(await isAlreadyProcessed(key)).toBe(true); - }); - - it("accepts source parameter", async () => { - const key = `${testPrefix}:source-test-${Date.now()}`; - - // This should not throw - await markAsProcessed(key, "twilio"); - }); - - it("uses default source when not provided", async () => { - const key = `${testPrefix}:default-source-${Date.now()}`; - - // Should not throw - await markAsProcessed(key); - }); - - it("handles duplicate marking gracefully", async () => { - const key = `${testPrefix}:duplicate-${Date.now()}`; + it("handles empty key gracefully", async () => { + const result = await isAlreadyProcessed(""); + expect(typeof result).toBe("boolean"); + }); - // Mark twice - should not throw - await markAsProcessed(key, "test"); - await markAsProcessed(key, "test"); + it("handles special characters in keys", async () => { + const key = `${testPrefix}:special-!@#$%^&*()-${Date.now()}`; + expect(await isAlreadyProcessed(key)).toBe(false); - // Should still be processed - expect(await isAlreadyProcessed(key)).toBe(true); - }); + await markAsProcessed(key, "test"); + expect(await isAlreadyProcessed(key)).toBe(true); }); + }); - describe("getProcessedMessagesCount", () => { - it("returns a number", async () => { - const count = await getProcessedMessagesCount(); - expect(typeof count).toBe("number"); - expect(count).toBeGreaterThanOrEqual(0); - }); + describe("markAsProcessed", () => { + it("marks a key as processed", async () => { + const key = `${testPrefix}:mark-test-${Date.now()}`; - it("increases after marking a key", async () => { - const initialCount = await getProcessedMessagesCount(); + // Initially not processed + expect(await isAlreadyProcessed(key)).toBe(false); - const key = `${testPrefix}:count-test-${Date.now()}`; - await markAsProcessed(key, "test"); + // Mark as processed + await markAsProcessed(key, "blooio"); - const newCount = await getProcessedMessagesCount(); - expect(newCount).toBeGreaterThanOrEqual(initialCount); - }); + // Now should be processed + expect(await isAlreadyProcessed(key)).toBe(true); }); - describe("cleanupExpiredKeys", () => { - it("returns number of deleted keys", async () => { - const deletedCount = await cleanupExpiredKeys(); - expect(typeof deletedCount).toBe("number"); - expect(deletedCount).toBeGreaterThanOrEqual(0); - }); - - it("does not delete recent keys", async () => { - const key = `${testPrefix}:recent-${Date.now()}`; - await markAsProcessed(key, "test"); + it("accepts source parameter", async () => { + const key = `${testPrefix}:source-test-${Date.now()}`; - // Run cleanup - await cleanupExpiredKeys(); - - // Key should still exist - expect(await isAlreadyProcessed(key)).toBe(true); - }); + // This should not throw + await markAsProcessed(key, "twilio"); }); - describe("clearProcessedMessages", () => { - it("clears all messages without error", async () => { - await clearProcessedMessages(); - }); - }); - }, -); - -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "tryClaimForProcessing", - () => { - const claimPrefix = `claim-${Date.now()}`; - - afterAll(async () => { - try { - await clearProcessedMessages(); - } catch { - // Ignore cleanup errors in tests - } - }); + it("uses default source when not provided", async () => { + const key = `${testPrefix}:default-source-${Date.now()}`; - it("returns true for first claim", async () => { - const key = `${claimPrefix}:first-${Date.now()}`; - expect(await tryClaimForProcessing(key, "test")).toBe(true); + // Should not throw + await markAsProcessed(key); }); - it("returns false for duplicate claim", async () => { - const key = `${claimPrefix}:duplicate-${Date.now()}`; - await tryClaimForProcessing(key, "test"); - expect(await tryClaimForProcessing(key, "test")).toBe(false); - }); + it("handles duplicate marking gracefully", async () => { + const key = `${testPrefix}:duplicate-${Date.now()}`; - it("handles concurrent claims correctly - only one wins", async () => { - const key = `${claimPrefix}:concurrent-${Date.now()}`; - const results = await Promise.all([ - tryClaimForProcessing(key, "test"), - tryClaimForProcessing(key, "test"), - tryClaimForProcessing(key, "test"), - ]); - const successCount = results.filter((r) => r === true).length; - expect(successCount).toBe(1); - }); + // Mark twice - should not throw + await markAsProcessed(key, "test"); + await markAsProcessed(key, "test"); - it("claimed key is visible to isAlreadyProcessed", async () => { - const key = `${claimPrefix}:visible-${Date.now()}`; - await tryClaimForProcessing(key, "test"); + // Should still be processed expect(await isAlreadyProcessed(key)).toBe(true); }); + }); - it("uses default source when not provided", async () => { - const key = `${claimPrefix}:default-source-${Date.now()}`; - await expect(tryClaimForProcessing(key)).resolves.not.toThrow(); + describe("getProcessedMessagesCount", () => { + it("returns a number", async () => { + const count = await getProcessedMessagesCount(); + expect(typeof count).toBe("number"); + expect(count).toBeGreaterThanOrEqual(0); }); - }, -); - -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "releaseProcessingClaim", - () => { - const releasePrefix = `release-${Date.now()}`; - - afterAll(async () => { - try { - await clearProcessedMessages(); - } catch { - // Ignore cleanup errors in tests - } - }); - - it("allows re-claim after release", async () => { - const key = `${releasePrefix}:reclaim-${Date.now()}`; - // First claim succeeds - expect(await tryClaimForProcessing(key, "test")).toBe(true); + it("increases after marking a key", async () => { + const initialCount = await getProcessedMessagesCount(); - // Second claim fails (already claimed) - expect(await tryClaimForProcessing(key, "test")).toBe(false); + const key = `${testPrefix}:count-test-${Date.now()}`; + await markAsProcessed(key, "test"); - // Release the claim - await releaseProcessingClaim(key); + const newCount = await getProcessedMessagesCount(); + expect(newCount).toBeGreaterThanOrEqual(initialCount); + }); + }); - // Now re-claim should succeed - expect(await tryClaimForProcessing(key, "test")).toBe(true); + describe("cleanupExpiredKeys", () => { + it("returns number of deleted keys", async () => { + const deletedCount = await cleanupExpiredKeys(); + expect(typeof deletedCount).toBe("number"); + expect(deletedCount).toBeGreaterThanOrEqual(0); }); - it("released key is no longer visible to isAlreadyProcessed", async () => { - const key = `${releasePrefix}:invisible-${Date.now()}`; + it("does not delete recent keys", async () => { + const key = `${testPrefix}:recent-${Date.now()}`; + await markAsProcessed(key, "test"); - await tryClaimForProcessing(key, "test"); - expect(await isAlreadyProcessed(key)).toBe(true); + // Run cleanup + await cleanupExpiredKeys(); - await releaseProcessingClaim(key); - expect(await isAlreadyProcessed(key)).toBe(false); + // Key should still exist + expect(await isAlreadyProcessed(key)).toBe(true); }); + }); - it("does not throw when releasing a non-existent key", async () => { - const key = `${releasePrefix}:nonexistent-${Date.now()}`; - await expect(releaseProcessingClaim(key)).resolves.not.toThrow(); + describe("clearProcessedMessages", () => { + it("clears all messages without error", async () => { + await clearProcessedMessages(); + }); + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("tryClaimForProcessing", () => { + const claimPrefix = `claim-${Date.now()}`; + + afterAll(async () => { + try { + await clearProcessedMessages(); + } catch { + // Ignore cleanup errors in tests + } + }); + + it("returns true for first claim", async () => { + const key = `${claimPrefix}:first-${Date.now()}`; + expect(await tryClaimForProcessing(key, "test")).toBe(true); + }); + + it("returns false for duplicate claim", async () => { + const key = `${claimPrefix}:duplicate-${Date.now()}`; + await tryClaimForProcessing(key, "test"); + expect(await tryClaimForProcessing(key, "test")).toBe(false); + }); + + it("handles concurrent claims correctly - only one wins", async () => { + const key = `${claimPrefix}:concurrent-${Date.now()}`; + const results = await Promise.all([ + tryClaimForProcessing(key, "test"), + tryClaimForProcessing(key, "test"), + tryClaimForProcessing(key, "test"), + ]); + const successCount = results.filter((r) => r === true).length; + expect(successCount).toBe(1); + }); + + it("claimed key is visible to isAlreadyProcessed", async () => { + const key = `${claimPrefix}:visible-${Date.now()}`; + await tryClaimForProcessing(key, "test"); + expect(await isAlreadyProcessed(key)).toBe(true); + }); + + it("uses default source when not provided", async () => { + const key = `${claimPrefix}:default-source-${Date.now()}`; + await expect(tryClaimForProcessing(key)).resolves.not.toThrow(); + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("releaseProcessingClaim", () => { + const releasePrefix = `release-${Date.now()}`; + + afterAll(async () => { + try { + await clearProcessedMessages(); + } catch { + // Ignore cleanup errors in tests + } + }); + + it("allows re-claim after release", async () => { + const key = `${releasePrefix}:reclaim-${Date.now()}`; + + // First claim succeeds + expect(await tryClaimForProcessing(key, "test")).toBe(true); + + // Second claim fails (already claimed) + expect(await tryClaimForProcessing(key, "test")).toBe(false); + + // Release the claim + await releaseProcessingClaim(key); + + // Now re-claim should succeed + expect(await tryClaimForProcessing(key, "test")).toBe(true); + }); + + it("released key is no longer visible to isAlreadyProcessed", async () => { + const key = `${releasePrefix}:invisible-${Date.now()}`; + + await tryClaimForProcessing(key, "test"); + expect(await isAlreadyProcessed(key)).toBe(true); + + await releaseProcessingClaim(key); + expect(await isAlreadyProcessed(key)).toBe(false); + }); + + it("does not throw when releasing a non-existent key", async () => { + const key = `${releasePrefix}:nonexistent-${Date.now()}`; + await expect(releaseProcessingClaim(key)).resolves.not.toThrow(); + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("Idempotency Security Tests", () => { + const securityPrefix = `security-${Date.now()}`; + + describe("Replay Attack Prevention", () => { + it("prevents duplicate processing of same message", async () => { + const messageId = `${securityPrefix}:replay-${Date.now()}`; + + // First time - not processed + const firstCheck = await isAlreadyProcessed(messageId); + expect(firstCheck).toBe(false); + + // Mark as processed + await markAsProcessed(messageId, "webhook"); + + // Second time - should be detected + const secondCheck = await isAlreadyProcessed(messageId); + expect(secondCheck).toBe(true); }); - }, -); - -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "Idempotency Security Tests", - () => { - const securityPrefix = `security-${Date.now()}`; - - describe("Replay Attack Prevention", () => { - it("prevents duplicate processing of same message", async () => { - const messageId = `${securityPrefix}:replay-${Date.now()}`; - - // First time - not processed - const firstCheck = await isAlreadyProcessed(messageId); - expect(firstCheck).toBe(false); - - // Mark as processed - await markAsProcessed(messageId, "webhook"); - // Second time - should be detected - const secondCheck = await isAlreadyProcessed(messageId); - expect(secondCheck).toBe(true); - }); + it("uses unique keys for different message sources", async () => { + const messageId = "same-id-123"; + const blooioKey = `${securityPrefix}:blooio:${messageId}`; + const twilioKey = `${securityPrefix}:twilio:${messageId}`; - it("uses unique keys for different message sources", async () => { - const messageId = "same-id-123"; - const blooioKey = `${securityPrefix}:blooio:${messageId}`; - const twilioKey = `${securityPrefix}:twilio:${messageId}`; + // Process same message ID from different sources + await markAsProcessed(blooioKey, "blooio"); - // Process same message ID from different sources - await markAsProcessed(blooioKey, "blooio"); + // Twilio version should not be marked as processed + expect(await isAlreadyProcessed(twilioKey)).toBe(false); + }); + }); - // Twilio version should not be marked as processed - expect(await isAlreadyProcessed(twilioKey)).toBe(false); - }); + describe("Key Format Validation", () => { + it("handles very long keys", async () => { + const longKey = `${securityPrefix}:${"x".repeat(500)}`; + expect(await isAlreadyProcessed(longKey)).toBe(false); + await markAsProcessed(longKey, "test"); }); - describe("Key Format Validation", () => { - it("handles very long keys", async () => { - const longKey = `${securityPrefix}:${"x".repeat(500)}`; - expect(await isAlreadyProcessed(longKey)).toBe(false); - await markAsProcessed(longKey, "test"); - }); - - it("handles keys with special webhook-related characters", async () => { - const key = `${securityPrefix}:webhook:msg_123:abc=def&foo=bar`; - expect(await isAlreadyProcessed(key)).toBe(false); - await markAsProcessed(key, "test"); - expect(await isAlreadyProcessed(key)).toBe(true); - }); + it("handles keys with special webhook-related characters", async () => { + const key = `${securityPrefix}:webhook:msg_123:abc=def&foo=bar`; + expect(await isAlreadyProcessed(key)).toBe(false); + await markAsProcessed(key, "test"); + expect(await isAlreadyProcessed(key)).toBe(true); }); - }, -); + }); +}); diff --git a/packages/tests/unit/jsonb-param.test.ts b/packages/tests/unit/jsonb-param.test.ts index 37d89de2c..1b1635128 100644 --- a/packages/tests/unit/jsonb-param.test.ts +++ b/packages/tests/unit/jsonb-param.test.ts @@ -10,12 +10,15 @@ describe("jsonbParam", () => { // Includes the cast token. const hasJsonbCast = chunks.some( - (c) => c?.constructor?.name === "StringChunk" && c?.value?.[0] === "::jsonb", + (c) => + c?.constructor?.name === "StringChunk" && c?.value?.[0] === "::jsonb", ); expect(hasJsonbCast).toBe(true); // The param should be JSON string, not a raw object. - const hasJsonStringParam = chunks.some((c) => typeof c === "string" && c === "{}"); + const hasJsonStringParam = chunks.some( + (c) => typeof c === "string" && c === "{}", + ); expect(hasJsonStringParam).toBe(true); }); }); diff --git a/packages/tests/unit/ledger-source-id.test.ts b/packages/tests/unit/ledger-source-id.test.ts index d975ffb0e..e474cc6f9 100644 --- a/packages/tests/unit/ledger-source-id.test.ts +++ b/packages/tests/unit/ledger-source-id.test.ts @@ -3,9 +3,9 @@ import { normalizeLedgerSourceId } from "@/lib/utils/ledger-source-id"; describe("normalizeLedgerSourceId", () => { test("preserves valid UUID source IDs", () => { - expect(normalizeLedgerSourceId("11111111-1111-4111-8111-111111111111")).toBe( - "11111111-1111-4111-8111-111111111111", - ); + expect( + normalizeLedgerSourceId("11111111-1111-4111-8111-111111111111"), + ).toBe("11111111-1111-4111-8111-111111111111"); }); test("maps composite source IDs into deterministic UUIDs", () => { @@ -16,6 +16,8 @@ describe("normalizeLedgerSourceId", () => { /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, ); expect(normalizeLedgerSourceId(sourceId)).toBe(normalized); - expect(normalizeLedgerSourceId("revenue_split:pi_999:user_456")).not.toBe(normalized); + expect(normalizeLedgerSourceId("revenue_split:pi_999:user_456")).not.toBe( + normalized, + ); }); }); diff --git a/packages/tests/unit/mcp-lib.test.ts b/packages/tests/unit/mcp-lib.test.ts index 10c58034d..562076205 100644 --- a/packages/tests/unit/mcp-lib.test.ts +++ b/packages/tests/unit/mcp-lib.test.ts @@ -135,7 +135,9 @@ describe("authContextStorage", () => { describe("getAuthContext", () => { test("throws when called outside context", () => { - expect(() => getAuthContext()).toThrow("Authentication context not available"); + expect(() => getAuthContext()).toThrow( + "Authentication context not available", + ); }); test("returns context when inside run()", async () => { diff --git a/packages/tests/unit/mcp-tool-visibility.test.ts b/packages/tests/unit/mcp-tool-visibility.test.ts index 0c0f10c83..43f8bcb55 100644 --- a/packages/tests/unit/mcp-tool-visibility.test.ts +++ b/packages/tests/unit/mcp-tool-visibility.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { getCrucialToolsForServer, isCrucialTool } from "@/lib/eliza/plugin-mcp/tool-visibility"; +import { + getCrucialToolsForServer, + isCrucialTool, +} from "@/lib/eliza/plugin-mcp/tool-visibility"; describe("MCP tool visibility", () => { test("keeps core Twitter tools visible and promotes mentions", () => { diff --git a/packages/tests/unit/message-router-service.test.ts b/packages/tests/unit/message-router-service.test.ts index 514a87478..449fc1548 100644 --- a/packages/tests/unit/message-router-service.test.ts +++ b/packages/tests/unit/message-router-service.test.ts @@ -12,508 +12,509 @@ import { describe, expect, it } from "bun:test"; import { messageRouterService } from "@/lib/services/message-router"; -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "MessageRouterService", - () => { - const testOrgId = "88888888-8888-8888-8888-888888888888"; +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("MessageRouterService", () => { + const testOrgId = "88888888-8888-8888-8888-888888888888"; - describe("Phone Number Normalization", () => { - // The private normalizePhoneNumber method is tested indirectly through public methods + describe("Phone Number Normalization", () => { + // The private normalizePhoneNumber method is tested indirectly through public methods - it("handles standard E.164 format in routing", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "Test message", - provider: "twilio", - }); - - // Will fail because no phone mapping, but tests normalization path - expect(result).toHaveProperty("success"); + it("handles standard E.164 format in routing", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "Test message", + provider: "twilio", }); - it("handles phone number without country code", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "5551234567", - to: "+15559876543", - body: "Test message", - provider: "twilio", - }); + // Will fail because no phone mapping, but tests normalization path + expect(result).toHaveProperty("success"); + }); - expect(result).toHaveProperty("success"); + it("handles phone number without country code", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "5551234567", + to: "+15559876543", + body: "Test message", + provider: "twilio", }); - it("handles phone number with spaces and dashes", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+1 555-123-4567", - to: "+15559876543", - body: "Test message", - provider: "twilio", - }); + expect(result).toHaveProperty("success"); + }); - expect(result).toHaveProperty("success"); + it("handles phone number with spaces and dashes", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+1 555-123-4567", + to: "+15559876543", + body: "Test message", + provider: "twilio", }); - it("handles email addresses for iMessage", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "user@example.com", - to: "agent@company.com", - body: "iMessage via email", - provider: "blooio", - }); + expect(result).toHaveProperty("success"); + }); - expect(result).toHaveProperty("success"); + it("handles email addresses for iMessage", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "user@example.com", + to: "agent@company.com", + body: "iMessage via email", + provider: "blooio", }); - it("handles uppercase email addresses", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "USER@EXAMPLE.COM", - to: "agent@company.com", - body: "Uppercase email", - provider: "blooio", - }); - - expect(result).toHaveProperty("success"); - }); + expect(result).toHaveProperty("success"); }); - describe("routeIncomingMessage", () => { - it("returns error when no phone mapping found", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559999999", // Non-existent mapping - body: "Test message", - provider: "twilio", - }); - - expect(result.success).toBe(false); - expect(result.error).toContain("No agent configured"); + it("handles uppercase email addresses", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "USER@EXAMPLE.COM", + to: "agent@company.com", + body: "Uppercase email", + provider: "blooio", }); - it("includes error message in result", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+10000000000", - body: "Test", - provider: "twilio", - }); + expect(result).toHaveProperty("success"); + }); + }); - expect(result.success).toBe(false); - expect(result).toHaveProperty("error"); - expect(typeof result.error).toBe("string"); + describe("routeIncomingMessage", () => { + it("returns error when no phone mapping found", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559999999", // Non-existent mapping + body: "Test message", + provider: "twilio", }); - it("handles message with media URLs", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "Check this out", - provider: "twilio", - mediaUrls: ["https://example.com/image.jpg"], - }); + expect(result.success).toBe(false); + expect(result.error).toContain("No agent configured"); + }); - expect(result).toHaveProperty("success"); + it("includes error message in result", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+10000000000", + body: "Test", + provider: "twilio", }); - it("handles message with metadata", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "With metadata", - provider: "twilio", - metadata: { - fromCity: "San Francisco", - fromState: "CA", - }, - }); + expect(result.success).toBe(false); + expect(result).toHaveProperty("error"); + expect(typeof result.error).toBe("string"); + }); - expect(result).toHaveProperty("success"); + it("handles message with media URLs", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "Check this out", + provider: "twilio", + mediaUrls: ["https://example.com/image.jpg"], }); - it("handles different message types", async () => { - const smsResult = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "SMS", - provider: "twilio", - messageType: "sms", - }); - - const mmsResult = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "MMS", - provider: "twilio", - messageType: "mms", - }); + expect(result).toHaveProperty("success"); + }); - expect(smsResult).toHaveProperty("success"); - expect(mmsResult).toHaveProperty("success"); + it("handles message with metadata", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "With metadata", + provider: "twilio", + metadata: { + fromCity: "San Francisco", + fromState: "CA", + }, }); - it("handles iMessage type", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "iMessage", - provider: "blooio", - messageType: "imessage", - }); + expect(result).toHaveProperty("success"); + }); - expect(result).toHaveProperty("success"); + it("handles different message types", async () => { + const smsResult = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "SMS", + provider: "twilio", + messageType: "sms", }); - it("handles WhatsApp message type", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "14245071234", - to: "+14245074963", - body: "WhatsApp message", - provider: "whatsapp", - messageType: "whatsapp", - }); - - expect(result).toHaveProperty("success"); + const mmsResult = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "MMS", + provider: "twilio", + messageType: "mms", }); - }); - describe("sendMessage", () => { - it("returns false for unknown provider", async () => { - const result = await messageRouterService.sendMessage({ - to: "+15551234567", - from: "+15559876543", - body: "Test message", - // @ts-expect-error - Testing invalid provider - provider: "unknown", - organizationId: testOrgId, - }); + expect(smsResult).toHaveProperty("success"); + expect(mmsResult).toHaveProperty("success"); + }); - expect(result).toBe(false); + it("handles iMessage type", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "iMessage", + provider: "blooio", + messageType: "imessage", }); - it("handles Twilio provider (fails without credentials)", async () => { - const result = await messageRouterService.sendMessage({ - to: "+15551234567", - from: "+15559876543", - body: "Test message", - provider: "twilio", - organizationId: testOrgId, - }); + expect(result).toHaveProperty("success"); + }); - // Will fail due to missing credentials - expect(result).toBe(false); + it("handles WhatsApp message type", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "14245071234", + to: "+14245074963", + body: "WhatsApp message", + provider: "whatsapp", + messageType: "whatsapp", }); - it("handles Blooio provider (fails without credentials)", async () => { - const result = await messageRouterService.sendMessage({ - to: "+15551234567", - from: "+15559876543", - body: "Test message", - provider: "blooio", - organizationId: testOrgId, - }); + expect(result).toHaveProperty("success"); + }); + }); - // Will fail due to missing credentials - expect(result).toBe(false); + describe("sendMessage", () => { + it("returns false for unknown provider", async () => { + const result = await messageRouterService.sendMessage({ + to: "+15551234567", + from: "+15559876543", + body: "Test message", + // @ts-expect-error - Testing invalid provider + provider: "unknown", + organizationId: testOrgId, }); - it("handles WhatsApp provider (fails without credentials)", async () => { - const result = await messageRouterService.sendMessage({ - to: "14245071234", - from: "+14245074963", - body: "Test WhatsApp message", - provider: "whatsapp", - organizationId: testOrgId, - }); - - // Will fail due to missing credentials - expect(result).toBe(false); + expect(result).toBe(false); + }); + + it("handles Twilio provider (fails without credentials)", async () => { + const result = await messageRouterService.sendMessage({ + to: "+15551234567", + from: "+15559876543", + body: "Test message", + provider: "twilio", + organizationId: testOrgId, }); - it("handles message with media URLs", async () => { - const result = await messageRouterService.sendMessage({ - to: "+15551234567", - from: "+15559876543", - body: "With media", - provider: "twilio", - organizationId: testOrgId, - mediaUrls: ["https://example.com/image.jpg"], - }); + // Will fail due to missing credentials + expect(result).toBe(false); + }); - expect(typeof result).toBe("boolean"); + it("handles Blooio provider (fails without credentials)", async () => { + const result = await messageRouterService.sendMessage({ + to: "+15551234567", + from: "+15559876543", + body: "Test message", + provider: "blooio", + organizationId: testOrgId, }); + + // Will fail due to missing credentials + expect(result).toBe(false); }); - describe("getPhoneNumbers", () => { - it("returns array for organization", async () => { - const phoneNumbers = await messageRouterService.getPhoneNumbers(testOrgId); - expect(Array.isArray(phoneNumbers)).toBe(true); + it("handles WhatsApp provider (fails without credentials)", async () => { + const result = await messageRouterService.sendMessage({ + to: "14245071234", + from: "+14245074963", + body: "Test WhatsApp message", + provider: "whatsapp", + organizationId: testOrgId, }); - it("returns empty array for non-existent organization", async () => { - const phoneNumbers = await messageRouterService.getPhoneNumbers( - "99999999-9999-9999-9999-999999999999", - ); - expect(Array.isArray(phoneNumbers)).toBe(true); - }); + // Will fail due to missing credentials + expect(result).toBe(false); }); - describe("getPhoneNumberById", () => { - it("returns null for non-existent phone number", async () => { - const phoneNumber = await messageRouterService.getPhoneNumberById( - "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", - ); - expect(phoneNumber).toBeNull(); + it("handles message with media URLs", async () => { + const result = await messageRouterService.sendMessage({ + to: "+15551234567", + from: "+15559876543", + body: "With media", + provider: "twilio", + organizationId: testOrgId, + mediaUrls: ["https://example.com/image.jpg"], }); - it("throws error for invalid UUID format", async () => { - await expect(messageRouterService.getPhoneNumberById("not-a-uuid")).rejects.toThrow(); - }); + expect(typeof result).toBe("boolean"); }); + }); - describe("deactivatePhoneNumber", () => { - it("does not throw for non-existent phone number", async () => { - await messageRouterService.deactivatePhoneNumber("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); - }); + describe("getPhoneNumbers", () => { + it("returns array for organization", async () => { + const phoneNumbers = + await messageRouterService.getPhoneNumbers(testOrgId); + expect(Array.isArray(phoneNumbers)).toBe(true); }); - describe("Error Handling", () => { - it("handles empty message body", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "", - provider: "twilio", - }); + it("returns empty array for non-existent organization", async () => { + const phoneNumbers = await messageRouterService.getPhoneNumbers( + "99999999-9999-9999-9999-999999999999", + ); + expect(Array.isArray(phoneNumbers)).toBe(true); + }); + }); + + describe("getPhoneNumberById", () => { + it("returns null for non-existent phone number", async () => { + const phoneNumber = await messageRouterService.getPhoneNumberById( + "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + ); + expect(phoneNumber).toBeNull(); + }); - expect(result).toHaveProperty("success"); - }); + it("throws error for invalid UUID format", async () => { + await expect( + messageRouterService.getPhoneNumberById("not-a-uuid"), + ).rejects.toThrow(); + }); + }); - it("handles undefined optional fields", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "Test", - provider: "twilio", - mediaUrls: undefined, - metadata: undefined, - providerMessageId: undefined, - }); + describe("deactivatePhoneNumber", () => { + it("does not throw for non-existent phone number", async () => { + await messageRouterService.deactivatePhoneNumber( + "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + ); + }); + }); - expect(result).toHaveProperty("success"); + describe("Error Handling", () => { + it("handles empty message body", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "", + provider: "twilio", }); - it("handles special characters in message body", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "Hello! 🎉 && \"quotes\"", - provider: "twilio", - }); + expect(result).toHaveProperty("success"); + }); - expect(result).toHaveProperty("success"); + it("handles undefined optional fields", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "Test", + provider: "twilio", + mediaUrls: undefined, + metadata: undefined, + providerMessageId: undefined, }); - it("handles very long message body", async () => { - const longBody = "A".repeat(10000); - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: longBody, - provider: "twilio", - }); - - expect(result).toHaveProperty("success"); - }); + expect(result).toHaveProperty("success"); }); - describe("registerPhoneNumber", () => { - // Note: These tests may fail if database is not available - // They verify the method signatures and basic error handling - - it("has correct method signature", () => { - expect(typeof messageRouterService.registerPhoneNumber).toBe("function"); + it("handles special characters in message body", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "Hello! 🎉 && \"quotes\"", + provider: "twilio", }); - it("requires organizationId, agentId, phoneNumber, and provider", async () => { - // Testing with valid parameters (may succeed or fail based on DB) - const _params = { - organizationId: testOrgId, - agentId: "test-agent-id", - phoneNumber: "+15550001234", - provider: "twilio" as const, - }; - - // This test verifies the method is callable with correct params - expect(typeof messageRouterService.registerPhoneNumber).toBe("function"); - }); + expect(result).toHaveProperty("success"); }); - describe("updateMessageLog", () => { - it("has correct method signature", () => { - expect(typeof messageRouterService.updateMessageLog).toBe("function"); + it("handles very long message body", async () => { + const longBody = "A".repeat(10000); + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: longBody, + provider: "twilio", }); - }); - describe("markMessageFailed", () => { - it("has correct method signature", () => { - expect(typeof messageRouterService.markMessageFailed).toBe("function"); - }); + expect(result).toHaveProperty("success"); }); - }, -); - -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "MessageRouterService ID Generation", - () => { - // Test deterministic ID generation behavior - - describe("Entity ID Generation", () => { - it("generates consistent IDs for same phone number", async () => { - // We test this indirectly through routing behavior - // The entity ID should be deterministic for the same input - const result1 = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "Message 1", - provider: "twilio", - }); + }); - const result2 = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "Message 2", - provider: "twilio", - }); + describe("registerPhoneNumber", () => { + // Note: These tests may fail if database is not available + // They verify the method signatures and basic error handling - // Both should go through same routing logic - expect(result1).toHaveProperty("success"); - expect(result2).toHaveProperty("success"); - }); + it("has correct method signature", () => { + expect(typeof messageRouterService.registerPhoneNumber).toBe("function"); }); - describe("Room ID Generation", () => { - it("generates consistent IDs regardless of direction", async () => { - // Room ID should be the same whether from A->B or B->A - const result1 = await messageRouterService.routeIncomingMessage({ - from: "+15551111111", - to: "+15552222222", - body: "A to B", - provider: "twilio", - }); + it("requires organizationId, agentId, phoneNumber, and provider", async () => { + // Testing with valid parameters (may succeed or fail based on DB) + const _params = { + organizationId: testOrgId, + agentId: "test-agent-id", + phoneNumber: "+15550001234", + provider: "twilio" as const, + }; + + // This test verifies the method is callable with correct params + expect(typeof messageRouterService.registerPhoneNumber).toBe("function"); + }); + }); - const result2 = await messageRouterService.routeIncomingMessage({ - from: "+15552222222", - to: "+15551111111", - body: "B to A", - provider: "twilio", - }); + describe("updateMessageLog", () => { + it("has correct method signature", () => { + expect(typeof messageRouterService.updateMessageLog).toBe("function"); + }); + }); - // Both should work (even if they fail for other reasons) - expect(result1).toHaveProperty("success"); - expect(result2).toHaveProperty("success"); - }); + describe("markMessageFailed", () => { + it("has correct method signature", () => { + expect(typeof messageRouterService.markMessageFailed).toBe("function"); }); - }, -); - -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "MessageRouterService Concurrent Operations", - () => { - const concurrentOrgId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; - - it("handles concurrent message routing", async () => { - const promises = Array(10) - .fill(null) - .map((_, i) => - messageRouterService.routeIncomingMessage({ - from: `+1555000000${i}`, - to: "+15559876543", - body: `Concurrent message ${i}`, - provider: "twilio", - }), - ); - - const results = await Promise.all(promises); - - // All should complete (success or failure) - for (const result of results) { - expect(result).toHaveProperty("success"); - } - }); - - it("handles concurrent message sending", async () => { - const promises = Array(5) - .fill(null) - .map((_, i) => - messageRouterService.sendMessage({ - to: `+1555000000${i}`, - from: "+15559876543", - body: `Concurrent send ${i}`, - provider: "twilio", - organizationId: concurrentOrgId, - }), - ); - - const results = await Promise.all(promises); - - // All should complete (all false since no credentials) - for (const result of results) { - expect(typeof result).toBe("boolean"); - } - }); - - it("handles concurrent phone number lookups", async () => { - const promises = Array(10) - .fill(null) - .map(() => messageRouterService.getPhoneNumbers(concurrentOrgId)); - - const results = await Promise.all(promises); - - // All should return arrays - for (const result of results) { - expect(Array.isArray(result)).toBe(true); - } - }); - }, -); - -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "MessageRouterService Provider Handling", - () => { - it("correctly identifies Twilio messages", async () => { - const result = await messageRouterService.routeIncomingMessage({ + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("MessageRouterService ID Generation", () => { + // Test deterministic ID generation behavior + + describe("Entity ID Generation", () => { + it("generates consistent IDs for same phone number", async () => { + // We test this indirectly through routing behavior + // The entity ID should be deterministic for the same input + const result1 = await messageRouterService.routeIncomingMessage({ from: "+15551234567", to: "+15559876543", - body: "Twilio message", + body: "Message 1", provider: "twilio", - messageType: "sms", }); - expect(result).toHaveProperty("success"); - }); - - it("correctly identifies Blooio messages", async () => { - const result = await messageRouterService.routeIncomingMessage({ + const result2 = await messageRouterService.routeIncomingMessage({ from: "+15551234567", to: "+15559876543", - body: "Blooio message", - provider: "blooio", - messageType: "imessage", + body: "Message 2", + provider: "twilio", }); - expect(result).toHaveProperty("success"); + // Both should go through same routing logic + expect(result1).toHaveProperty("success"); + expect(result2).toHaveProperty("success"); }); + }); + + describe("Room ID Generation", () => { + it("generates consistent IDs regardless of direction", async () => { + // Room ID should be the same whether from A->B or B->A + const result1 = await messageRouterService.routeIncomingMessage({ + from: "+15551111111", + to: "+15552222222", + body: "A to B", + provider: "twilio", + }); - it("handles provider message ID", async () => { - const result = await messageRouterService.routeIncomingMessage({ - from: "+15551234567", - to: "+15559876543", - body: "With provider ID", + const result2 = await messageRouterService.routeIncomingMessage({ + from: "+15552222222", + to: "+15551111111", + body: "B to A", provider: "twilio", - providerMessageId: "SM" + "a".repeat(32), }); + // Both should work (even if they fail for other reasons) + expect(result1).toHaveProperty("success"); + expect(result2).toHaveProperty("success"); + }); + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("MessageRouterService Concurrent Operations", () => { + const concurrentOrgId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; + + it("handles concurrent message routing", async () => { + const promises = Array(10) + .fill(null) + .map((_, i) => + messageRouterService.routeIncomingMessage({ + from: `+1555000000${i}`, + to: "+15559876543", + body: `Concurrent message ${i}`, + provider: "twilio", + }), + ); + + const results = await Promise.all(promises); + + // All should complete (success or failure) + for (const result of results) { expect(result).toHaveProperty("success"); + } + }); + + it("handles concurrent message sending", async () => { + const promises = Array(5) + .fill(null) + .map((_, i) => + messageRouterService.sendMessage({ + to: `+1555000000${i}`, + from: "+15559876543", + body: `Concurrent send ${i}`, + provider: "twilio", + organizationId: concurrentOrgId, + }), + ); + + const results = await Promise.all(promises); + + // All should complete (all false since no credentials) + for (const result of results) { + expect(typeof result).toBe("boolean"); + } + }); + + it("handles concurrent phone number lookups", async () => { + const promises = Array(10) + .fill(null) + .map(() => messageRouterService.getPhoneNumbers(concurrentOrgId)); + + const results = await Promise.all(promises); + + // All should return arrays + for (const result of results) { + expect(Array.isArray(result)).toBe(true); + } + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("MessageRouterService Provider Handling", () => { + it("correctly identifies Twilio messages", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "Twilio message", + provider: "twilio", + messageType: "sms", }); - }, -); + + expect(result).toHaveProperty("success"); + }); + + it("correctly identifies Blooio messages", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "Blooio message", + provider: "blooio", + messageType: "imessage", + }); + + expect(result).toHaveProperty("success"); + }); + + it("handles provider message ID", async () => { + const result = await messageRouterService.routeIncomingMessage({ + from: "+15551234567", + to: "+15559876543", + body: "With provider ID", + provider: "twilio", + providerMessageId: "SM" + "a".repeat(32), + }); + + expect(result).toHaveProperty("success"); + }); +}); diff --git a/packages/tests/unit/milady-gateway-relay.test.ts b/packages/tests/unit/milady-gateway-relay.test.ts index ba1b0a5fc..e6c2d226f 100644 --- a/packages/tests/unit/milady-gateway-relay.test.ts +++ b/packages/tests/unit/milady-gateway-relay.test.ts @@ -9,7 +9,9 @@ afterEach(async () => { await Promise.all( createdSessionIds .splice(0) - .map((sessionId) => miladyGatewayRelayService.disconnectSession(sessionId)), + .map((sessionId) => + miladyGatewayRelayService.disconnectSession(sessionId), + ), ); }); @@ -37,7 +39,9 @@ describe("miladyGatewayRelayService", () => { "UPSTASH_REDIS_REST_TOKEN", "MILADY_ALLOW_EPHEMERAL_CLOUD_STATE", ] as const; - const previousEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const previousEnv = Object.fromEntries( + envKeys.map((key) => [key, process.env[key]]), + ); try { env.NODE_ENV = "production"; @@ -53,15 +57,22 @@ describe("miladyGatewayRelayService", () => { delete env.MILADY_ALLOW_EPHEMERAL_CLOUD_STATE; const imported = await import( - new URL(`../../lib/services/milady-gateway-relay.ts?test=${Date.now()}`, import.meta.url) - .href + new URL( + `../../lib/services/milady-gateway-relay.ts?test=${Date.now()}`, + import.meta.url, + ).href ); expect(imported.miladyGatewayRelayService).toBeDefined(); imported.miladyGatewayRelayService.resetForTests(null); await expect( - imported.miladyGatewayRelayService.listOwnerSessions("org-prod", "user-prod"), - ).rejects.toThrow("Redis-backed shared storage is required in production"); + imported.miladyGatewayRelayService.listOwnerSessions( + "org-prod", + "user-prod", + ), + ).rejects.toThrow( + "Redis-backed shared storage is required in production", + ); } finally { for (const key of envKeys) { const value = previousEnv[key]; @@ -94,7 +105,10 @@ describe("miladyGatewayRelayService", () => { 2_000, ); - const nextRequest = await miladyGatewayRelayService.pollNextRequest(session.id, 500); + const nextRequest = await miladyGatewayRelayService.pollNextRequest( + session.id, + 500, + ); expect(nextRequest).not.toBeNull(); expect(nextRequest?.rpc.method).toBe("message.send"); expect(nextRequest?.rpc.params?.text).toBe("hello from cloud"); @@ -126,7 +140,9 @@ describe("miladyGatewayRelayService", () => { }); createdSessionIds.push(session.id); - await expect(miladyGatewayRelayService.listOwnerSessions("org-2", "user-2")).resolves.toEqual([ + await expect( + miladyGatewayRelayService.listOwnerSessions("org-2", "user-2"), + ).resolves.toEqual([ expect.objectContaining({ id: session.id, runtimeAgentId: "local-agent-2", @@ -136,8 +152,8 @@ describe("miladyGatewayRelayService", () => { await miladyGatewayRelayService.disconnectSession(session.id); createdSessionIds.splice(createdSessionIds.indexOf(session.id), 1); - await expect(miladyGatewayRelayService.listOwnerSessions("org-2", "user-2")).resolves.toEqual( - [], - ); + await expect( + miladyGatewayRelayService.listOwnerSessions("org-2", "user-2"), + ).resolves.toEqual([]); }); }); diff --git a/packages/tests/unit/milady-google-multi-account.test.ts b/packages/tests/unit/milady-google-multi-account.test.ts index d3e4a501b..bb3b22e79 100644 --- a/packages/tests/unit/milady-google-multi-account.test.ts +++ b/packages/tests/unit/milady-google-multi-account.test.ts @@ -40,8 +40,10 @@ function installStubs(overrides: { managedGoogleConnectorDeps.oauthService = { ...originalService, - listConnections: overrides.listConnections ?? originalService.listConnections, - revokeConnection: overrides.revokeConnection ?? originalService.revokeConnection, + listConnections: + overrides.listConnections ?? originalService.listConnections, + revokeConnection: + overrides.revokeConnection ?? originalService.revokeConnection, }; return () => { @@ -135,7 +137,13 @@ describe("listManagedGoogleConnectorAccounts", () => { listConnections: async ({ connectionRole }) => connectionRole === "owner" ? [makeConnection({ id: "owner-1", connectionRole: "owner" })] - : [makeConnection({ id: "agent-1", connectionRole: "agent", userId: undefined })], + : [ + makeConnection({ + id: "agent-1", + connectionRole: "agent", + userId: undefined, + }), + ], }); try { @@ -144,8 +152,12 @@ describe("listManagedGoogleConnectorAccounts", () => { userId: "user-1", }); expect(accounts).toHaveLength(2); - expect(accounts.some((account) => account.connectionId === "owner-1")).toBe(true); - expect(accounts.some((account) => account.connectionId === "agent-1")).toBe(true); + expect( + accounts.some((account) => account.connectionId === "owner-1"), + ).toBe(true); + expect( + accounts.some((account) => account.connectionId === "agent-1"), + ).toBe(true); } finally { restore(); } diff --git a/packages/tests/unit/milady-lifeops-github-complete.test.ts b/packages/tests/unit/milady-lifeops-github-complete.test.ts index 3d3027f6d..9f7e4b291 100644 --- a/packages/tests/unit/milady-lifeops-github-complete.test.ts +++ b/packages/tests/unit/milady-lifeops-github-complete.test.ts @@ -74,7 +74,9 @@ describe("lifeops github completion route", () => { ); expect(response.status).toBe(307); - expect(response.headers.get("location")).toContain("/dashboard/settings?tab=connections"); + expect(response.headers.get("location")).toContain( + "/dashboard/settings?tab=connections", + ); expect(response.headers.get("location")).toContain("github_error=denied"); }); }); diff --git a/packages/tests/unit/milady-web-ui.test.ts b/packages/tests/unit/milady-web-ui.test.ts index 19985198e..8209d6dba 100644 --- a/packages/tests/unit/milady-web-ui.test.ts +++ b/packages/tests/unit/milady-web-ui.test.ts @@ -46,7 +46,9 @@ describe("getMiladyAgentPublicWebUiUrl", () => { baseDomain: "https://milady.shad0w.xyz/dashboard", path: "/chat", }), - ).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.milady.shad0w.xyz/chat"); + ).toBe( + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.milady.shad0w.xyz/chat", + ); }); test("falls back to waifu.fun when env var is unset", () => { @@ -58,15 +60,19 @@ describe("getMiladyAgentPublicWebUiUrl", () => { }); test("returns null when explicit baseDomain fails normalization (no silent default)", () => { - expect(getMiladyAgentPublicWebUiUrl(makeSandbox(), { baseDomain: "" })).toBeNull(); - expect(getMiladyAgentPublicWebUiUrl(makeSandbox(), { baseDomain: " " })).toBeNull(); + expect( + getMiladyAgentPublicWebUiUrl(makeSandbox(), { baseDomain: "" }), + ).toBeNull(); + expect( + getMiladyAgentPublicWebUiUrl(makeSandbox(), { baseDomain: " " }), + ).toBeNull(); }); test("treats baseDomain: undefined like omitted (env then default)", () => { process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN = "custom.example"; - expect(getMiladyAgentPublicWebUiUrl(makeSandbox(), { baseDomain: undefined })).toBe( - "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.custom.example", - ); + expect( + getMiladyAgentPublicWebUiUrl(makeSandbox(), { baseDomain: undefined }), + ).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.custom.example"); }); }); @@ -88,9 +94,9 @@ describe("getPreferredMiladyAgentWebUiUrl", () => { test("uses waifu.fun default even when web_ui_port is missing", () => { delete process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN; - expect(getPreferredMiladyAgentWebUiUrl(makeSandbox({ web_ui_port: null }))).toBe( - "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.waifu.fun", - ); + expect( + getPreferredMiladyAgentWebUiUrl(makeSandbox({ web_ui_port: null })), + ).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.waifu.fun"); }); }); @@ -101,7 +107,8 @@ describe("getClientSafeMiladyAgentWebUiUrl", () => { expect( getClientSafeMiladyAgentWebUiUrl({ ...makeSandbox(), - canonicalWebUiUrl: "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.milady.shad0w.xyz", + canonicalWebUiUrl: + "https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.milady.shad0w.xyz", }), ).toBe("https://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.milady.shad0w.xyz"); }); @@ -115,6 +122,8 @@ describe("getClientSafeMiladyAgentWebUiUrl", () => { describe("getMiladyAgentDirectWebUiUrl", () => { test("returns null when headscale access is unavailable", () => { - expect(getMiladyAgentDirectWebUiUrl(makeSandbox({ headscale_ip: null }))).toBeNull(); + expect( + getMiladyAgentDirectWebUiUrl(makeSandbox({ headscale_ip: null })), + ).toBeNull(); }); }); diff --git a/packages/tests/unit/model-catalog.test.ts b/packages/tests/unit/model-catalog.test.ts index 1accf121f..b7a82eeeb 100644 --- a/packages/tests/unit/model-catalog.test.ts +++ b/packages/tests/unit/model-catalog.test.ts @@ -135,7 +135,10 @@ describe("catalog merging", () => { ); expect(merged).toHaveLength(2); - expect(merged.map((model) => model.id)).toEqual(["openai/gpt-5.4", "groq/compound"]); + expect(merged.map((model) => model.id)).toEqual([ + "openai/gpt-5.4", + "groq/compound", + ]); }); }); diff --git a/packages/tests/unit/n8n-bridge-apikey-cred-map.test.ts b/packages/tests/unit/n8n-bridge-apikey-cred-map.test.ts index 4e6901683..550ba3766 100644 --- a/packages/tests/unit/n8n-bridge-apikey-cred-map.test.ts +++ b/packages/tests/unit/n8n-bridge-apikey-cred-map.test.ts @@ -13,7 +13,10 @@ describe("API_KEY_CRED_TYPES", () => { const mapping = API_KEY_CRED_TYPES["openAiApi"]; expect(mapping).toBeDefined(); - const data = mapping.buildData("eliza_test123", "https://cloud.elizaos.com"); + const data = mapping.buildData( + "eliza_test123", + "https://cloud.elizaos.com", + ); expect(data).toEqual({ apiKey: "eliza_test123", organizationId: "", @@ -23,7 +26,10 @@ describe("API_KEY_CRED_TYPES", () => { }); test("openAiApi appends /api/v1 to base URL", () => { - const data = API_KEY_CRED_TYPES["openAiApi"].buildData("key", "https://example.com"); + const data = API_KEY_CRED_TYPES["openAiApi"].buildData( + "key", + "https://example.com", + ); expect(data.url).toBe("https://example.com/api/v1"); }); diff --git a/packages/tests/unit/n8n-bridge-oauth-cred-map.test.ts b/packages/tests/unit/n8n-bridge-oauth-cred-map.test.ts index 51f7faa5a..fa05de329 100644 --- a/packages/tests/unit/n8n-bridge-oauth-cred-map.test.ts +++ b/packages/tests/unit/n8n-bridge-oauth-cred-map.test.ts @@ -21,7 +21,9 @@ describe("mapCredTypeToCloudPlatform", () => { test("maps google credentials to google", () => { expect(mapCredTypeToCloudPlatform("googleSheetsOAuth2Api")).toBe("google"); expect(mapCredTypeToCloudPlatform("googleDriveOAuth2Api")).toBe("google"); - expect(mapCredTypeToCloudPlatform("googleCalendarOAuth2Api")).toBe("google"); + expect(mapCredTypeToCloudPlatform("googleCalendarOAuth2Api")).toBe( + "google", + ); expect(mapCredTypeToCloudPlatform("googleDocsOAuth2Api")).toBe("google"); expect(mapCredTypeToCloudPlatform("googleApi")).toBe("google"); }); diff --git a/packages/tests/unit/oauth-state-validation.test.ts b/packages/tests/unit/oauth-state-validation.test.ts index bf31a58ea..50adf75c8 100644 --- a/packages/tests/unit/oauth-state-validation.test.ts +++ b/packages/tests/unit/oauth-state-validation.test.ts @@ -130,7 +130,10 @@ describe("OAuth State Schema Validation", () => { }; // Zod strips unknown properties, so this should pass but without __proto__ - const result = OAuthStateSchema.parse(protoAttempt) as Record; + const result = OAuthStateSchema.parse(protoAttempt) as Record< + string, + unknown + >; expect(Object.hasOwn(result, "__proto__")).toBe(false); expect(Object.hasOwn(result, "isAdmin")).toBe(false); }); diff --git a/packages/tests/unit/oauth/connection-adapters.test.ts b/packages/tests/unit/oauth/connection-adapters.test.ts index 1c5a11e16..55ef6b1de 100644 --- a/packages/tests/unit/oauth/connection-adapters.test.ts +++ b/packages/tests/unit/oauth/connection-adapters.test.ts @@ -5,7 +5,10 @@ */ import { describe, expect, it } from "bun:test"; -import { getAdapter, getAllAdapters } from "@/lib/services/oauth/connection-adapters"; +import { + getAdapter, + getAllAdapters, +} from "@/lib/services/oauth/connection-adapters"; import { OAUTH_PROVIDERS } from "@/lib/services/oauth/provider-registry"; describe("Connection Adapters Registry", () => { @@ -117,7 +120,9 @@ describe("Google Adapter", () => { // Google uses platform_credentials with UUID IDs // This requires a database lookup, so in unit tests we can't fully verify // But we can verify the function exists and returns a promise - const result = adapter.ownsConnection("550e8400-e29b-41d4-a716-446655440000"); + const result = adapter.ownsConnection( + "550e8400-e29b-41d4-a716-446655440000", + ); expect(result instanceof Promise).toBe(true); }); @@ -157,7 +162,9 @@ describe("Twitter Adapter", () => { }); it("should return false for UUID IDs", async () => { - const result = await adapter.ownsConnection("550e8400-e29b-41d4-a716-446655440000"); + const result = await adapter.ownsConnection( + "550e8400-e29b-41d4-a716-446655440000", + ); expect(result).toBe(false); }); }); @@ -234,7 +241,9 @@ describe("Connection ID Disambiguation", () => { it("malformed IDs should not be owned by any secrets adapter", async () => { const malformedIds = ["invalid", "", "platform:", ":org-123", "platform::"]; - const secretsAdapters = ["twitter", "twilio", "blooio"].map((p) => getAdapter(p)!); + const secretsAdapters = ["twitter", "twilio", "blooio"].map( + (p) => getAdapter(p)!, + ); for (const id of malformedIds) { for (const adapter of secretsAdapters) { diff --git a/packages/tests/unit/oauth/errors.test.ts b/packages/tests/unit/oauth/errors.test.ts index 7b93fdd61..e1970dd5b 100644 --- a/packages/tests/unit/oauth/errors.test.ts +++ b/packages/tests/unit/oauth/errors.test.ts @@ -17,7 +17,10 @@ import { describe("OAuthError Class", () => { describe("constructor", () => { it("should create error with required parameters", () => { - const error = new OAuthError(OAuthErrorCode.CONNECTION_NOT_FOUND, "Connection not found"); + const error = new OAuthError( + OAuthErrorCode.CONNECTION_NOT_FOUND, + "Connection not found", + ); expect(error.code).toBe(OAuthErrorCode.CONNECTION_NOT_FOUND); expect(error.message).toBe("Connection not found"); @@ -72,7 +75,10 @@ describe("OAuthError Class", () => { }); it("should convert to response with undefined optional fields", () => { - const error = new OAuthError(OAuthErrorCode.UNAUTHORIZED, "Not authorized"); + const error = new OAuthError( + OAuthErrorCode.UNAUTHORIZED, + "Not authorized", + ); const response = error.toResponse(); @@ -190,7 +196,10 @@ describe("Error Factory Functions", () => { }); it("should create error with reason", () => { - const error = Errors.tokenRefreshFailed("Google", "Invalid refresh token"); + const error = Errors.tokenRefreshFailed( + "Google", + "Invalid refresh token", + ); expect(error.message).toContain("Google"); expect(error.message).toContain("Invalid refresh token"); diff --git a/packages/tests/unit/oauth/oauth-service.test.ts b/packages/tests/unit/oauth/oauth-service.test.ts index cd8268c74..275a2bca1 100644 --- a/packages/tests/unit/oauth/oauth-service.test.ts +++ b/packages/tests/unit/oauth/oauth-service.test.ts @@ -10,7 +10,10 @@ import { scopeConnectionsForUser, } from "@/lib/services/oauth/oauth-service"; import { OAUTH_PROVIDERS } from "@/lib/services/oauth/provider-registry"; -import type { OAuthConnection, OAuthProviderInfo } from "@/lib/services/oauth/types"; +import type { + OAuthConnection, + OAuthProviderInfo, +} from "@/lib/services/oauth/types"; describe("OAuth Service Logic", () => { describe("listProviders transformation", () => { @@ -43,7 +46,9 @@ describe("OAuth Service Logic", () => { }); describe("getMostRecentActive logic", () => { - function getMostRecentActive(connections: OAuthConnection[]): OAuthConnection | null { + function getMostRecentActive( + connections: OAuthConnection[], + ): OAuthConnection | null { const active = connections.filter((c) => c.status === "active"); if (active.length === 0) return null; return active.reduce((most, conn) => { @@ -109,7 +114,9 @@ describe("OAuth Service Logic", () => { }); describe("sortConnectionsByRecency logic", () => { - function sortConnectionsByRecency(connections: OAuthConnection[]): OAuthConnection[] { + function sortConnectionsByRecency( + connections: OAuthConnection[], + ): OAuthConnection[] { return connections.sort((a, b) => { const aTime = a.lastUsedAt?.getTime() || a.linkedAt.getTime(); const bTime = b.lastUsedAt?.getTime() || b.linkedAt.getTime(); @@ -126,9 +133,13 @@ describe("OAuth Service Logic", () => { const oneHourAgo = new Date(now.getTime() - 3600000); const twoHoursAgo = new Date(now.getTime() - 7200000); - const conn1 = createMockConnection("active", "1", { lastUsedAt: oneHourAgo }); + const conn1 = createMockConnection("active", "1", { + lastUsedAt: oneHourAgo, + }); const conn2 = createMockConnection("active", "2", { lastUsedAt: now }); - const conn3 = createMockConnection("active", "3", { lastUsedAt: twoHoursAgo }); + const conn3 = createMockConnection("active", "3", { + lastUsedAt: twoHoursAgo, + }); const sorted = sortConnectionsByRecency([conn1, conn2, conn3]); expect(sorted[0].id).toBe("2"); // Most recent @@ -140,7 +151,9 @@ describe("OAuth Service Logic", () => { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 3600000); - const conn1 = createMockConnection("active", "1", { linkedAt: oneHourAgo }); + const conn1 = createMockConnection("active", "1", { + linkedAt: oneHourAgo, + }); const conn2 = createMockConnection("active", "2", { linkedAt: now }); const sorted = sortConnectionsByRecency([conn1, conn2]); @@ -232,9 +245,13 @@ describe("OAuth Service Logic", () => { lastUsedAt: new Date("2026-04-09T11:00:00Z"), }); - expect(getPreferredActiveConnection([shared, owned], "user-1")?.id).toBe("owned"); + expect(getPreferredActiveConnection([shared, owned], "user-1")?.id).toBe( + "owned", + ); expect( - scopeConnectionsForUser([shared, owned], "user-1").map((connection) => connection.id), + scopeConnectionsForUser([shared, owned], "user-1").map( + (connection) => connection.id, + ), ).toEqual(["owned", "shared"]); }); }); @@ -248,7 +265,13 @@ describe("OAuth Service Logic", () => { // 4. Cache the result // 5. Return token - const flowSteps = ["checkCache", "findAdapter", "getToken", "cacheToken", "returnToken"]; + const flowSteps = [ + "checkCache", + "findAdapter", + "getToken", + "cacheToken", + "returnToken", + ]; expect(flowSteps[0]).toBe("checkCache"); expect(flowSteps[flowSteps.length - 1]).toBe("returnToken"); @@ -292,7 +315,11 @@ describe("OAuth Service Logic", () => { ]; const activePlatforms = [ - ...new Set(connections.filter((c) => c.status === "active").map((c) => c.platform)), + ...new Set( + connections + .filter((c) => c.status === "active") + .map((c) => c.platform), + ), ]; expect(activePlatforms).toContain("google"); @@ -340,10 +367,9 @@ describe("OAuth Service Logic", () => { const scoped = scopeConnectionsForUser(connections, "user-1"); - expect(scoped.map((connection: OAuthConnection) => connection.id)).toEqual([ - "owned", - "shared", - ]); + expect( + scoped.map((connection: OAuthConnection) => connection.id), + ).toEqual(["owned", "shared"]); }); }); }); diff --git a/packages/tests/unit/oauth/provider-registry.test.ts b/packages/tests/unit/oauth/provider-registry.test.ts index ae57f9117..e5b25bbc7 100644 --- a/packages/tests/unit/oauth/provider-registry.test.ts +++ b/packages/tests/unit/oauth/provider-registry.test.ts @@ -19,7 +19,9 @@ describe("Provider Registry", () => { let resolveRequestedScopes: PR["resolveRequestedScopes"]; beforeEach(async () => { - const m = await import(`@/lib/services/oauth/provider-registry?t=${Date.now()}`); + const m = await import( + `@/lib/services/oauth/provider-registry?t=${Date.now()}` + ); getProvider = m.getProvider; getAllProviderIds = m.getAllProviderIds; getConfiguredProviders = m.getConfiguredProviders; @@ -73,7 +75,9 @@ describe("Provider Registry", () => { it("should require GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET", () => { expect(OAUTH_PROVIDERS.google.envVars).toContain("GOOGLE_CLIENT_ID"); - expect(OAUTH_PROVIDERS.google.envVars).toContain("GOOGLE_CLIENT_SECRET"); + expect(OAUTH_PROVIDERS.google.envVars).toContain( + "GOOGLE_CLIENT_SECRET", + ); }); it("should use platform_credentials storage", () => { @@ -99,7 +103,9 @@ describe("Provider Registry", () => { it("should require Twitter API keys", () => { expect(OAUTH_PROVIDERS.twitter.envVars).toContain("TWITTER_API_KEY"); - expect(OAUTH_PROVIDERS.twitter.envVars).toContain("TWITTER_API_SECRET_KEY"); + expect(OAUTH_PROVIDERS.twitter.envVars).toContain( + "TWITTER_API_SECRET_KEY", + ); }); it("should use secrets storage", () => { @@ -108,12 +114,18 @@ describe("Provider Registry", () => { it("should have secret patterns defined", () => { expect(OAUTH_PROVIDERS.twitter.secretPatterns).toBeDefined(); - expect(OAUTH_PROVIDERS.twitter.secretPatterns!.accessToken).toBe("TWITTER_ACCESS_TOKEN"); + expect(OAUTH_PROVIDERS.twitter.secretPatterns!.accessToken).toBe( + "TWITTER_ACCESS_TOKEN", + ); expect(OAUTH_PROVIDERS.twitter.secretPatterns!.accessTokenSecret).toBe( "TWITTER_ACCESS_TOKEN_SECRET", ); - expect(OAUTH_PROVIDERS.twitter.secretPatterns!.username).toBe("TWITTER_USERNAME"); - expect(OAUTH_PROVIDERS.twitter.secretPatterns!.userId).toBe("TWITTER_USER_ID"); + expect(OAUTH_PROVIDERS.twitter.secretPatterns!.username).toBe( + "TWITTER_USERNAME", + ); + expect(OAUTH_PROVIDERS.twitter.secretPatterns!.userId).toBe( + "TWITTER_USER_ID", + ); }); }); @@ -132,9 +144,15 @@ describe("Provider Registry", () => { it("should have secret patterns defined", () => { expect(OAUTH_PROVIDERS.twilio.secretPatterns).toBeDefined(); - expect(OAUTH_PROVIDERS.twilio.secretPatterns!.accountSid).toBe("TWILIO_ACCOUNT_SID"); - expect(OAUTH_PROVIDERS.twilio.secretPatterns!.authToken).toBe("TWILIO_AUTH_TOKEN"); - expect(OAUTH_PROVIDERS.twilio.secretPatterns!.phoneNumber).toBe("TWILIO_PHONE_NUMBER"); + expect(OAUTH_PROVIDERS.twilio.secretPatterns!.accountSid).toBe( + "TWILIO_ACCOUNT_SID", + ); + expect(OAUTH_PROVIDERS.twilio.secretPatterns!.authToken).toBe( + "TWILIO_AUTH_TOKEN", + ); + expect(OAUTH_PROVIDERS.twilio.secretPatterns!.phoneNumber).toBe( + "TWILIO_PHONE_NUMBER", + ); }); it("should have empty callback route (API key platforms)", () => { @@ -157,9 +175,15 @@ describe("Provider Registry", () => { it("should have secret patterns defined", () => { expect(OAUTH_PROVIDERS.blooio.secretPatterns).toBeDefined(); - expect(OAUTH_PROVIDERS.blooio.secretPatterns!.apiKey).toBe("BLOOIO_API_KEY"); - expect(OAUTH_PROVIDERS.blooio.secretPatterns!.webhookSecret).toBe("BLOOIO_WEBHOOK_SECRET"); - expect(OAUTH_PROVIDERS.blooio.secretPatterns!.fromNumber).toBe("BLOOIO_FROM_NUMBER"); + expect(OAUTH_PROVIDERS.blooio.secretPatterns!.apiKey).toBe( + "BLOOIO_API_KEY", + ); + expect(OAUTH_PROVIDERS.blooio.secretPatterns!.webhookSecret).toBe( + "BLOOIO_WEBHOOK_SECRET", + ); + expect(OAUTH_PROVIDERS.blooio.secretPatterns!.fromNumber).toBe( + "BLOOIO_FROM_NUMBER", + ); }); }); }); @@ -212,10 +236,9 @@ describe("Provider Registry", () => { defaultScopes: ["scope:a"], }; - expect(resolveRequestedScopes(provider, [" scope:a ", "scope:b", "scope:a"])).toEqual([ - "scope:a", - "scope:b", - ]); + expect( + resolveRequestedScopes(provider, [" scope:a ", "scope:b", "scope:a"]), + ).toEqual(["scope:a", "scope:b"]); }); it("rejects requested scopes outside the allowlist", () => { @@ -225,9 +248,9 @@ describe("Provider Registry", () => { defaultScopes: ["scope:a"], }; - expect(() => resolveRequestedScopes(provider, ["scope:a", "scope:admin"])).toThrow( - "Requested scopes are not allowed", - ); + expect(() => + resolveRequestedScopes(provider, ["scope:a", "scope:admin"]), + ).toThrow("Requested scopes are not allowed"); }); }); diff --git a/packages/tests/unit/oauth/secrets-adapter-utils.test.ts b/packages/tests/unit/oauth/secrets-adapter-utils.test.ts index 5da2d887b..d7769c859 100644 --- a/packages/tests/unit/oauth/secrets-adapter-utils.test.ts +++ b/packages/tests/unit/oauth/secrets-adapter-utils.test.ts @@ -72,7 +72,9 @@ describe("Secrets Adapter Utils", () => { it("should handle partial matches correctly", () => { // Should not match if platform is just a prefix expect(ownsConnectionId("twi", "twitter:org-123")).toBe(false); - expect(ownsConnectionId("twitter", "twitter_variant:org-123")).toBe(false); + expect(ownsConnectionId("twitter", "twitter_variant:org-123")).toBe( + false, + ); }); it("should handle edge cases", () => { @@ -121,7 +123,11 @@ describe("Secrets Adapter Utils", () => { it("should throw when connection ID is a UUID (wrong format)", () => { expect(() => { - verifyConnectionId("twitter", "org-123", "550e8400-e29b-41d4-a716-446655440000"); + verifyConnectionId( + "twitter", + "org-123", + "550e8400-e29b-41d4-a716-446655440000", + ); }).toThrow(); }); }); @@ -132,7 +138,11 @@ describe("Secrets Adapter Utils", () => { const oneHourAgo = new Date(now.getTime() - 3600000); const oneDayAgo = new Date(now.getTime() - 86400000); - const secrets = [{ created_at: now }, { created_at: oneHourAgo }, { created_at: oneDayAgo }]; + const secrets = [ + { created_at: now }, + { created_at: oneHourAgo }, + { created_at: oneDayAgo }, + ]; const earliest = getEarliestSecretDate(secrets); expect(earliest.getTime()).toBe(oneDayAgo.getTime()); @@ -167,7 +177,11 @@ describe("Secrets Adapter Utils", () => { it("should handle identical dates", () => { const date = new Date("2024-01-15"); - const secrets = [{ created_at: date }, { created_at: date }, { created_at: date }]; + const secrets = [ + { created_at: date }, + { created_at: date }, + { created_at: date }, + ]; const earliest = getEarliestSecretDate(secrets); expect(earliest.getTime()).toBe(date.getTime()); diff --git a/packages/tests/unit/org-rate-limits.test.ts b/packages/tests/unit/org-rate-limits.test.ts index 8d28f5fbc..2a51acbbc 100644 --- a/packages/tests/unit/org-rate-limits.test.ts +++ b/packages/tests/unit/org-rate-limits.test.ts @@ -298,31 +298,46 @@ describe("Org Rate Limits — Endpoint Mapping", () => { describe("Org Rate Limits — Route Wiring", () => { test("chat/completions calls enforceOrgRateLimit with 'completions'", () => { - const source = readFileSync(join(REPO_ROOT, "app/api/v1/chat/completions/route.ts"), "utf8"); + const source = readFileSync( + join(REPO_ROOT, "app/api/v1/chat/completions/route.ts"), + "utf8", + ); expect(source).toMatch(/enforceOrgRateLimit\s*\(/); expect(source).toMatch(/["']completions["']/); }); test("embeddings calls enforceOrgRateLimit with 'embeddings'", () => { - const source = readFileSync(join(REPO_ROOT, "app/api/v1/embeddings/route.ts"), "utf8"); + const source = readFileSync( + join(REPO_ROOT, "app/api/v1/embeddings/route.ts"), + "utf8", + ); expect(source).toMatch(/enforceOrgRateLimit\s*\(/); expect(source).toMatch(/["']embeddings["']/); }); test("responses calls enforceOrgRateLimit with 'completions' (shared counter)", () => { - const source = readFileSync(join(REPO_ROOT, "app/api/v1/responses/route.ts"), "utf8"); + const source = readFileSync( + join(REPO_ROOT, "app/api/v1/responses/route.ts"), + "utf8", + ); expect(source).toMatch(/enforceOrgRateLimit\s*\(/); // Intentionally shares completions counter expect(source).toMatch(/["']completions["']/); }); test("responses guards anonymous users (null org_id check)", () => { - const source = readFileSync(join(REPO_ROOT, "app/api/v1/responses/route.ts"), "utf8"); + const source = readFileSync( + join(REPO_ROOT, "app/api/v1/responses/route.ts"), + "utf8", + ); expect(source).toMatch(/if\s*\(\s*user\.organization_id\s*\)/); }); test("stripe webhook invalidates tier cache", () => { - const source = readFileSync(join(REPO_ROOT, "app/api/stripe/webhook/route.ts"), "utf8"); + const source = readFileSync( + join(REPO_ROOT, "app/api/stripe/webhook/route.ts"), + "utf8", + ); expect(source).toMatch(/invalidateOrgTierCache/); }); diff --git a/packages/tests/unit/origin-validation.test.ts b/packages/tests/unit/origin-validation.test.ts index 034ea3017..043f1cbaf 100644 --- a/packages/tests/unit/origin-validation.test.ts +++ b/packages/tests/unit/origin-validation.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { isAllowedOrigin, normalizeOrigin } from "@/lib/security/origin-validation"; +import { + isAllowedOrigin, + normalizeOrigin, +} from "@/lib/security/origin-validation"; describe("origin validation", () => { test("normalizes full URLs to their origin", () => { @@ -18,18 +21,26 @@ describe("origin validation", () => { }); test("matches wildcard subdomain origins", () => { - expect(isAllowedOrigin(["https://*.example.com"], "https://tenant.example.com/callback")).toBe( - true, - ); + expect( + isAllowedOrigin( + ["https://*.example.com"], + "https://tenant.example.com/callback", + ), + ).toBe(true); }); test("rejects unregistered redirect URIs", () => { - expect(isAllowedOrigin(["https://app.example.com"], "https://evil.example.net/callback")).toBe( - false, - ); + expect( + isAllowedOrigin( + ["https://app.example.com"], + "https://evil.example.net/callback", + ), + ).toBe(false); }); test("rejects invalid protocols", () => { - expect(isAllowedOrigin(["https://app.example.com"], "javascript:alert(1)")).toBe(false); + expect( + isAllowedOrigin(["https://app.example.com"], "javascript:alert(1)"), + ).toBe(false); }); }); diff --git a/packages/tests/unit/outbound-url.test.ts b/packages/tests/unit/outbound-url.test.ts index 5e8cbd22b..f158fdf45 100644 --- a/packages/tests/unit/outbound-url.test.ts +++ b/packages/tests/unit/outbound-url.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; -const moduleUrl = new URL("../../lib/security/outbound-url.ts", import.meta.url).href; +const moduleUrl = new URL("../../lib/security/outbound-url.ts", import.meta.url) + .href; function runIsolatedSnippet(source: string) { return Bun.spawnSync({ @@ -27,7 +28,9 @@ describe("outbound URL safety", () => { `); expect(result.exitCode).toBe(1); - expect(new TextDecoder().decode(result.stderr)).toMatch(/private|reserved|localhost/i); + expect(new TextDecoder().decode(result.stderr)).toMatch( + /private|reserved|localhost/i, + ); const linkLocal = runIsolatedSnippet(` const mod = await import(${JSON.stringify(`${moduleUrl}?case=linklocal`)}); @@ -43,7 +46,9 @@ describe("outbound URL safety", () => { `); expect(linkLocal.exitCode).toBe(1); - expect(new TextDecoder().decode(linkLocal.stderr)).toMatch(/private|reserved/i); + expect(new TextDecoder().decode(linkLocal.stderr)).toMatch( + /private|reserved/i, + ); }); test("classifies private IPv4 destinations as forbidden", () => { @@ -74,7 +79,9 @@ describe("outbound URL safety", () => { `); expect(result.exitCode).toBe(0); - expect(new TextDecoder().decode(result.stdout).trim()).toBe("https://93.184.216.34/mcp"); + expect(new TextDecoder().decode(result.stdout).trim()).toBe( + "https://93.184.216.34/mcp", + ); }); test("rejects URLs that embed credentials", () => { diff --git a/packages/tests/unit/performance-optimizations.test.ts b/packages/tests/unit/performance-optimizations.test.ts index f42e86f34..d0fb6e6f5 100644 --- a/packages/tests/unit/performance-optimizations.test.ts +++ b/packages/tests/unit/performance-optimizations.test.ts @@ -15,12 +15,17 @@ describe("Action validation cache", () => { beforeEach(async () => { // Module-level cache is a singleton — tests use unique message IDs to avoid interference. - const mod = await import("@/lib/eliza/plugin-cloud-bootstrap/providers/actions"); + const mod = await import( + "@/lib/eliza/plugin-cloud-bootstrap/providers/actions" + ); actionsProvider = mod.actionsProvider; invalidateActionValidationCache = mod.invalidateActionValidationCache; }); - function makeRuntime(actions: Action[], mcpToolCount?: number): IAgentRuntime { + function makeRuntime( + actions: Action[], + mcpToolCount?: number, + ): IAgentRuntime { return { actions, getService: () => @@ -215,7 +220,11 @@ describe("Action validation cache", () => { getService: () => undefined, } as unknown as IAgentRuntime; - const result = await actionsProvider.get!(runtime, makeMessage("msg-no-mcp-1"), emptyState); + const result = await actionsProvider.get!( + runtime, + makeMessage("msg-no-mcp-1"), + emptyState, + ); expect(providerValues(result).discoverableToolCount).toBe(""); }); @@ -441,7 +450,10 @@ describe("Retry config", () => { test("delay never exceeds max even at extreme attempt numbers", () => { const { baseDelayMs, maxDelayMs, backoffMultiplier } = sourceRetryConfig; for (let attempt = 1; attempt <= 100; attempt++) { - const delay = Math.min(baseDelayMs * backoffMultiplier ** (attempt - 1), maxDelayMs); + const delay = Math.min( + baseDelayMs * backoffMultiplier ** (attempt - 1), + maxDelayMs, + ); expect(delay).toBeLessThanOrEqual(maxDelayMs); expect(Number.isFinite(delay)).toBe(true); } @@ -455,22 +467,30 @@ describe("Multi-step decision template", () => { let summaryTemplate: string; beforeEach(async () => { - const mod = await import("@/lib/eliza/plugin-cloud-bootstrap/templates/multi-step"); + const mod = await import( + "@/lib/eliza/plugin-cloud-bootstrap/templates/multi-step" + ); decisionTemplate = mod.multiStepDecisionTemplate; summaryTemplate = mod.multiStepSummaryTemplate; }); test("contains minimize-iterations and strengthened FINISH rules", () => { expect(decisionTemplate).toContain("Minimize iterations"); - expect(decisionTemplate).toContain("Most tasks need only 1 action + FINISH"); + expect(decisionTemplate).toContain( + "Most tasks need only 1 action + FINISH", + ); expect(decisionTemplate).toContain("call FINISH immediately"); expect(decisionTemplate).toContain("Do NOT add extra iterations"); }); test("preserves OAuth/connect rules from ux-overhaul", () => { expect(decisionTemplate).toContain("OAUTH_CONNECT"); - expect(decisionTemplate).toContain("links expire and must be freshly generated"); - expect(decisionTemplate).toContain("Always execute actions for user requests"); + expect(decisionTemplate).toContain( + "links expire and must be freshly generated", + ); + expect(decisionTemplate).toContain( + "Always execute actions for user requests", + ); }); test("all 8 rules present and numbered", () => { @@ -494,13 +514,19 @@ describe("Multi-step decision template", () => { describe("MCP timeout constant", () => { test("DEFAULT_MCP_TIMEOUT_MS is 15000ms", async () => { - const { DEFAULT_MCP_TIMEOUT_MS } = await import("@/lib/eliza/plugin-mcp/types"); + const { DEFAULT_MCP_TIMEOUT_MS } = await import( + "@/lib/eliza/plugin-mcp/types" + ); expect(DEFAULT_MCP_TIMEOUT_MS).toBe(15000); }); test("other MCP constants unchanged", async () => { - const { DEFAULT_MAX_RETRIES, MAX_RECONNECT_ATTEMPTS, BACKOFF_MULTIPLIER, INITIAL_RETRY_DELAY } = - await import("@/lib/eliza/plugin-mcp/types"); + const { + DEFAULT_MAX_RETRIES, + MAX_RECONNECT_ATTEMPTS, + BACKOFF_MULTIPLIER, + INITIAL_RETRY_DELAY, + } = await import("@/lib/eliza/plugin-mcp/types"); expect(DEFAULT_MAX_RETRIES).toBe(2); expect(MAX_RECONNECT_ATTEMPTS).toBe(5); @@ -509,8 +535,12 @@ describe("MCP timeout constant", () => { }); test("service uses the millisecond timeout constant when timeoutInMillis is absent", async () => { - const source = await Bun.file("packages/lib/eliza/plugin-mcp/service.ts").text(); - expect(source).toContain("config.timeoutInMillis || DEFAULT_MCP_TIMEOUT_MS"); + const source = await Bun.file( + "packages/lib/eliza/plugin-mcp/service.ts", + ).text(); + expect(source).toContain( + "config.timeoutInMillis || DEFAULT_MCP_TIMEOUT_MS", + ); }); }); @@ -530,7 +560,9 @@ describe("Parse retry defaults", () => { const source = await Bun.file( "packages/lib/eliza/plugin-cloud-bootstrap/services/cloud-bootstrap-message-service.ts", ).text(); - const match = source.match(/MULTISTEP_SUMMARY_PARSE_RETRIES.*?\?\?\s*"(\d+)"/); + const match = source.match( + /MULTISTEP_SUMMARY_PARSE_RETRIES.*?\?\?\s*"(\d+)"/, + ); expect(match).not.toBeNull(); expect(match![1]).toBe("2"); }); @@ -547,7 +579,9 @@ describe("MCP wait removal", () => { }); test("RuntimeFactory still calls waitForMcpServiceIfNeeded", async () => { - const source = await Bun.file("packages/lib/eliza/runtime-factory.ts").text(); + const source = await Bun.file( + "packages/lib/eliza/runtime-factory.ts", + ).text(); expect(source).toContain("waitForMcpServiceIfNeeded"); expect(source).toContain("waitForInitialization"); }); diff --git a/packages/tests/unit/persistence-guard.test.ts b/packages/tests/unit/persistence-guard.test.ts index e0417ef7c..3030856ab 100644 --- a/packages/tests/unit/persistence-guard.test.ts +++ b/packages/tests/unit/persistence-guard.test.ts @@ -59,16 +59,18 @@ describe("persistence guard", () => { test("allows ephemeral fallback by default outside production", () => { expect(allowEphemeralCloudStateFallback()).toBe(true); - expect(() => assertPersistentCloudStateConfigured("test-feature", false)).not.toThrow(); + expect(() => + assertPersistentCloudStateConfigured("test-feature", false), + ).not.toThrow(); }); test("rejects ephemeral fallback in production-like environments", () => { env.NODE_ENV = "production"; expect(allowEphemeralCloudStateFallback()).toBe(false); - expect(() => assertPersistentCloudStateConfigured("test-feature", false)).toThrow( - "Redis-backed shared storage is required in production", - ); + expect(() => + assertPersistentCloudStateConfigured("test-feature", false), + ).toThrow("Redis-backed shared storage is required in production"); }); test("allows explicit override in production-like environments", () => { @@ -76,6 +78,8 @@ describe("persistence guard", () => { env.MILADY_ALLOW_EPHEMERAL_CLOUD_STATE = "true"; expect(allowEphemeralCloudStateFallback()).toBe(true); - expect(() => assertPersistentCloudStateConfigured("test-feature", false)).not.toThrow(); + expect(() => + assertPersistentCloudStateConfigured("test-feature", false), + ).not.toThrow(); }); }); diff --git a/packages/tests/unit/phone-normalization.test.ts b/packages/tests/unit/phone-normalization.test.ts index 6cade99d4..d5a423203 100644 --- a/packages/tests/unit/phone-normalization.test.ts +++ b/packages/tests/unit/phone-normalization.test.ts @@ -104,7 +104,9 @@ describe("Phone Normalization Utilities", () => { it("handles whitespace", () => { expect(normalizePhoneNumber(" +14155552671 ")).toBe("+14155552671"); - expect(normalizePhoneNumber(" user@example.com ")).toBe("user@example.com"); + expect(normalizePhoneNumber(" user@example.com ")).toBe( + "user@example.com", + ); }); it("uses default country code for normalization", () => { diff --git a/packages/tests/unit/platform-credentials-multi-account-schema.test.ts b/packages/tests/unit/platform-credentials-multi-account-schema.test.ts index 8a9ac12c3..bee2b784d 100644 --- a/packages/tests/unit/platform-credentials-multi-account-schema.test.ts +++ b/packages/tests/unit/platform-credentials-multi-account-schema.test.ts @@ -18,7 +18,9 @@ function readMigration(tag: string): string { describe("platform_credentials multi-account migrations", () => { it("0067 drops platform_credentials_user_platform_idx", () => { const sql = readMigration("0067_allow_multiple_oauth_connections_per_user"); - expect(sql).toMatch(/DROP INDEX IF EXISTS "platform_credentials_user_platform_idx"/); + expect(sql).toMatch( + /DROP INDEX IF EXISTS "platform_credentials_user_platform_idx"/, + ); }); it("leaves the (org, platform, platform_user_id) unique index from migration 0019 in place", () => { @@ -26,7 +28,11 @@ describe("platform_credentials multi-account migrations", () => { // (organization_id, platform, platform_user_id). No later migration up to // and including 0067 should drop that index — it is the only remaining // uniqueness guarantee against linking the same Google account twice. - const drop0067 = readMigration("0067_allow_multiple_oauth_connections_per_user"); - expect(drop0067).not.toMatch(/DROP INDEX[^;]*platform_credentials_platform_user_idx/i); + const drop0067 = readMigration( + "0067_allow_multiple_oauth_connections_per_user", + ); + expect(drop0067).not.toMatch( + /DROP INDEX[^;]*platform_credentials_platform_user_idx/i, + ); }); }); diff --git a/packages/tests/unit/pr385-round2-fixes.test.ts b/packages/tests/unit/pr385-round2-fixes.test.ts index 5c4964cc7..ce367f638 100644 --- a/packages/tests/unit/pr385-round2-fixes.test.ts +++ b/packages/tests/unit/pr385-round2-fixes.test.ts @@ -34,7 +34,10 @@ describe("compat-envelope default domain", () => { // Dynamic import to pick up env change const mod = await import( - new URL(`../../lib/api/compat-envelope.ts?t=${Date.now()}`, import.meta.url).href + new URL( + `../../lib/api/compat-envelope.ts?t=${Date.now()}`, + import.meta.url, + ).href ); const sandbox = { id: "test-agent-id", @@ -82,7 +85,9 @@ describe("compat-envelope default domain", () => { describe("handleCompatError", () => { test("maps ForbiddenError to 403", async () => { const { ForbiddenError } = await import("../../lib/api/errors"); - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new ForbiddenError("no access")); expect(res.status).toBe(403); @@ -92,21 +97,27 @@ describe("handleCompatError", () => { }); test("maps Error with 'Unauthorized' to 401", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Unauthorized token")); expect(res.status).toBe(401); }); test("maps Error with 'Forbidden' to 403", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Forbidden access")); expect(res.status).toBe(403); }); test("maps unknown errors to 500", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError("something broke"); expect(res.status).toBe(500); @@ -115,7 +126,9 @@ describe("handleCompatError", () => { }); test("maps generic Error to 500", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("db connection lost")); expect(res.status).toBe(500); @@ -139,7 +152,11 @@ describe("service-key HMAC digest compare", () => { env: Record = {}, ): { ok: boolean; value?: unknown; errorName?: string } { const mergedEnv = { ...process.env } as Record; - for (const key of ["WAIFU_SERVICE_KEY", "WAIFU_SERVICE_ORG_ID", "WAIFU_SERVICE_USER_ID"]) { + for (const key of [ + "WAIFU_SERVICE_KEY", + "WAIFU_SERVICE_ORG_ID", + "WAIFU_SERVICE_USER_ID", + ]) { if (key in env) { const v = env[key]; if (v === undefined) delete mergedEnv[key]; @@ -214,10 +231,14 @@ describe("JSONB typeof guards for token fields", () => { // Simulates the guard logic extracted from the route function extractTokenFromConfig(cfg: Record | null) { return { - tokenAddress: typeof cfg?.tokenContractAddress === "string" ? cfg.tokenContractAddress : null, + tokenAddress: + typeof cfg?.tokenContractAddress === "string" + ? cfg.tokenContractAddress + : null, tokenChain: typeof cfg?.chain === "string" ? cfg.chain : null, tokenName: typeof cfg?.tokenName === "string" ? cfg.tokenName : null, - tokenTicker: typeof cfg?.tokenTicker === "string" ? cfg.tokenTicker : null, + tokenTicker: + typeof cfg?.tokenTicker === "string" ? cfg.tokenTicker : null, }; } @@ -269,7 +290,10 @@ describe("JSONB typeof guards for token fields", () => { describe("tail parameter clamping", () => { function clampTail(raw: string | null): number { const rawTail = parseInt(raw ?? "100", 10); - return Math.max(1, Math.min(Number.isFinite(rawTail) ? rawTail : 100, 5000)); + return Math.max( + 1, + Math.min(Number.isFinite(rawTail) ? rawTail : 100, 5000), + ); } test("defaults to 100 when absent", () => { @@ -318,7 +342,8 @@ describe("waifu-bridge warn-once on missing JWT secret", () => { test("authenticateWaifuBridge returns null without crashing when secret unset", async () => { const { authenticateWaifuBridge } = await import( - new URL(`../../lib/auth/waifu-bridge.ts?t=${Date.now()}`, import.meta.url).href + new URL(`../../lib/auth/waifu-bridge.ts?t=${Date.now()}`, import.meta.url) + .href ); // Minimal NextRequest-like object diff --git a/packages/tests/unit/pr385-round5-fixes.test.ts b/packages/tests/unit/pr385-round5-fixes.test.ts index 3013c8ad1..ad3e6b1a7 100644 --- a/packages/tests/unit/pr385-round5-fixes.test.ts +++ b/packages/tests/unit/pr385-round5-fixes.test.ts @@ -17,68 +17,92 @@ import { describe, expect, test } from "bun:test"; describe("handleCompatError — narrowed Invalid heuristic", () => { test("'Invalid API key' → 401", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid API key")); expect(res.status).toBe(401); }); test("'Invalid token' → 401", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid token")); expect(res.status).toBe(401); }); test("'Invalid credentials' → 401", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid credentials")); expect(res.status).toBe(401); }); test("'Invalid service key' → 401", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid service key")); expect(res.status).toBe(401); }); test("'Invalid agent config' → 500 (not 401)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid agent config")); expect(res.status).toBe(500); }); test("'Invalid JSON body' → 500 (not 401)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid JSON body")); expect(res.status).toBe(500); }); test("'Invalid request data' → 500 (not 401)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Invalid request data")); expect(res.status).toBe(500); }); test("'Invalid parameter: limit must be positive' → 500 (not 401)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); - const res = handleCompatError(new Error("Invalid parameter: limit must be positive")); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); + const res = handleCompatError( + new Error("Invalid parameter: limit must be positive"), + ); expect(res.status).toBe(500); }); // Existing behaviour preserved test("'Unauthorized' → 401 (unchanged)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Unauthorized")); expect(res.status).toBe(401); }); test("'Forbidden access' → 403 (unchanged)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("Forbidden access")); expect(res.status).toBe(403); }); test("generic error → 500 (unchanged)", async () => { - const { handleCompatError } = await import("../../../app/api/compat/_lib/error-handler"); + const { handleCompatError } = await import( + "../../../app/api/compat/_lib/error-handler" + ); const res = handleCompatError(new Error("db connection lost")); expect(res.status).toBe(500); }); @@ -128,13 +152,21 @@ describe("waifu-bridge slugFromUserId determinism", () => { .replace(/[^a-zA-Z0-9-]/g, "-") .toLowerCase() .slice(0, 40); - const hash = crypto.createHash("sha256").update(userId).digest("hex").slice(0, 16); + const hash = crypto + .createHash("sha256") + .update(userId) + .digest("hex") + .slice(0, 16); return `${base}-${hash}`; } test("same userId always produces the same slug", () => { - const slug1 = slugFromUserId("waifu:0xABCDEF1234567890abcdef1234567890abcdef12"); - const slug2 = slugFromUserId("waifu:0xABCDEF1234567890abcdef1234567890abcdef12"); + const slug1 = slugFromUserId( + "waifu:0xABCDEF1234567890abcdef1234567890abcdef12", + ); + const slug2 = slugFromUserId( + "waifu:0xABCDEF1234567890abcdef1234567890abcdef12", + ); expect(slug1).toBe(slug2); }); @@ -273,7 +305,10 @@ describe("suspend route — org-scoped getAgent structural check", () => { test("suspend route source contains getAgent call", async () => { const fs = require("fs"); const source = fs.readFileSync( - require("path").resolve(__dirname, "../../../app/api/compat/agents/[id]/suspend/route.ts"), + require("path").resolve( + __dirname, + "../../../app/api/compat/agents/[id]/suspend/route.ts", + ), "utf-8", ); // The call may be split across multiple lines; check key fragments @@ -289,11 +324,17 @@ describe("suspend route — org-scoped getAgent structural check", () => { const fs = require("fs"); const path = require("path"); const suspendSrc = fs.readFileSync( - path.resolve(__dirname, "../../../app/api/compat/agents/[id]/suspend/route.ts"), + path.resolve( + __dirname, + "../../../app/api/compat/agents/[id]/suspend/route.ts", + ), "utf-8", ); const resumeSrc = fs.readFileSync( - path.resolve(__dirname, "../../../app/api/compat/agents/[id]/resume/route.ts"), + path.resolve( + __dirname, + "../../../app/api/compat/agents/[id]/resume/route.ts", + ), "utf-8", ); // Both should have the getAgentForWrite pre-check (call may span multiple lines) diff --git a/packages/tests/unit/proxy-cors.test.ts b/packages/tests/unit/proxy-cors.test.ts index 96a0cd2b7..298a3aa23 100644 --- a/packages/tests/unit/proxy-cors.test.ts +++ b/packages/tests/unit/proxy-cors.test.ts @@ -1,18 +1,26 @@ import { describe, expect, it } from "bun:test"; -import { applyCorsHeaders, getCorsHeaders, handleCorsOptions } from "@/lib/services/proxy/cors"; +import { + applyCorsHeaders, + getCorsHeaders, + handleCorsOptions, +} from "@/lib/services/proxy/cors"; describe("proxy CORS helpers", () => { it("allows service-key and Milady client headers for direct browser calls", () => { const headers = getCorsHeaders("GET, POST, OPTIONS"); expect(headers["Access-Control-Allow-Headers"]).toContain("X-Service-Key"); - expect(headers["Access-Control-Allow-Headers"]).toContain("X-Milady-Client-Id"); + expect(headers["Access-Control-Allow-Headers"]).toContain( + "X-Milady-Client-Id", + ); }); it("returns wildcard preflight responses", () => { const response = handleCorsOptions("GET, OPTIONS"); expect(response.status).toBe(204); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); - expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET"); + expect(response.headers.get("Access-Control-Allow-Methods")).toContain( + "GET", + ); }); it("applies CORS headers to an existing response", async () => { @@ -25,7 +33,9 @@ describe("proxy CORS helpers", () => { ); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); - expect(response.headers.get("Access-Control-Allow-Methods")).toBe("POST, OPTIONS"); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "POST, OPTIONS", + ); expect(await response.json()).toEqual({ ok: true }); }); }); diff --git a/packages/tests/unit/rate-limit-presets.test.ts b/packages/tests/unit/rate-limit-presets.test.ts index e1e6981f2..3a6bc4e85 100644 --- a/packages/tests/unit/rate-limit-presets.test.ts +++ b/packages/tests/unit/rate-limit-presets.test.ts @@ -36,8 +36,13 @@ describe("Embeddings rate-limit parity", () => { }); test("embeddings route is wired to RELAXED", () => { - const source = readFileSync(join(REPO_ROOT, "app/api/v1/embeddings/route.ts"), "utf8"); + const source = readFileSync( + join(REPO_ROOT, "app/api/v1/embeddings/route.ts"), + "utf8", + ); // Whitespace-tolerant regex to survive auto-formatter rewrites. - expect(source).toMatch(/withRateLimit\(\s*handlePOST\s*,\s*RateLimitPresets\.RELAXED\s*\)/); + expect(source).toMatch( + /withRateLimit\(\s*handlePOST\s*,\s*RateLimitPresets\.RELAXED\s*\)/, + ); }); }); diff --git a/packages/tests/unit/redirect-validation.test.ts b/packages/tests/unit/redirect-validation.test.ts index dc875b4f5..4b6e1b354 100644 --- a/packages/tests/unit/redirect-validation.test.ts +++ b/packages/tests/unit/redirect-validation.test.ts @@ -9,21 +9,26 @@ import { describe("redirect validation", () => { test("accepts allowlisted absolute redirect URLs", () => { expect( - isAllowedAbsoluteRedirectUrl("https://app.example.com/success", ["https://app.example.com"]), + isAllowedAbsoluteRedirectUrl("https://app.example.com/success", [ + "https://app.example.com", + ]), ).toBe(true); }); test("rejects redirect URLs on untrusted origins", () => { expect( - isAllowedAbsoluteRedirectUrl("https://evil.example.com/success", ["https://app.example.com"]), + isAllowedAbsoluteRedirectUrl("https://evil.example.com/success", [ + "https://app.example.com", + ]), ).toBe(false); }); test("rejects redirect URLs with embedded credentials", () => { expect( - isAllowedAbsoluteRedirectUrl("https://user:pass@app.example.com/success", [ - "https://app.example.com", - ]), + isAllowedAbsoluteRedirectUrl( + "https://user:pass@app.example.com/success", + ["https://app.example.com"], + ), ).toBe(false); }); @@ -34,7 +39,9 @@ describe("redirect validation", () => { "/dashboard", ); - expect(target.toString()).toBe("https://app.example.com/dashboard/settings?tab=connections"); + expect(target.toString()).toBe( + "https://app.example.com/dashboard/settings?tab=connections", + ); }); test("falls back when an external redirect target is provided", () => { @@ -54,7 +61,8 @@ describe("resolveOAuthSuccessRedirectUrl", () => { test("accepts allowlisted cross-origin Milady URL (production)", () => { const { target, rejected } = resolveOAuthSuccessRedirectUrl({ - value: "https://app.milady.ai/api/lifeops/connectors/google/success?side=owner", + value: + "https://app.milady.ai/api/lifeops/connectors/google/success?side=owner", baseUrl, fallbackPath, allowedAbsoluteOrigins: ["https://app.milady.ai"], @@ -90,7 +98,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(false); - expect(target.toString()).toBe("http://127.0.0.1:31337/api/lifeops/connectors/google/success"); + expect(target.toString()).toBe( + "http://127.0.0.1:31337/api/lifeops/connectors/google/success", + ); }); test("accepts same-origin absolute URL without being in allowlist", () => { @@ -102,7 +112,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(false); - expect(target.toString()).toBe("https://www.elizacloud.ai/auth/success?platform=google"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/auth/success?platform=google", + ); }); test("accepts safe relative paths without consulting allowlist", () => { @@ -114,7 +126,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(false); - expect(target.toString()).toBe("https://www.elizacloud.ai/dashboard/settings?tab=agents"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/dashboard/settings?tab=agents", + ); }); test("rejects cross-origin URL not on allowlist and marks result as rejected", () => { @@ -126,7 +140,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(true); - expect(target.toString()).toBe("https://www.elizacloud.ai/dashboard/settings?tab=connections"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/dashboard/settings?tab=connections", + ); }); test("rejects protocol-relative URLs", () => { @@ -138,7 +154,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(true); - expect(target.toString()).toBe("https://www.elizacloud.ai/dashboard/settings?tab=connections"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/dashboard/settings?tab=connections", + ); }); test("rejects redirect URLs with embedded credentials even on allowlisted origin", () => { @@ -150,7 +168,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(true); - expect(target.toString()).toBe("https://www.elizacloud.ai/dashboard/settings?tab=connections"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/dashboard/settings?tab=connections", + ); }); test("rejects non-http schemes", () => { @@ -162,7 +182,9 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(true); - expect(target.toString()).toBe("https://www.elizacloud.ai/dashboard/settings?tab=connections"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/dashboard/settings?tab=connections", + ); }); test("returns fallback when value is empty, without marking rejected", () => { @@ -174,6 +196,8 @@ describe("resolveOAuthSuccessRedirectUrl", () => { }); expect(rejected).toBe(false); - expect(target.toString()).toBe("https://www.elizacloud.ai/dashboard/settings?tab=connections"); + expect(target.toString()).toBe( + "https://www.elizacloud.ai/dashboard/settings?tab=connections", + ); }); }); diff --git a/packages/tests/unit/referral-me.test.ts b/packages/tests/unit/referral-me.test.ts index a54de63cd..84c61c195 100644 --- a/packages/tests/unit/referral-me.test.ts +++ b/packages/tests/unit/referral-me.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { coerceNonNegativeIntegerCount, parseReferralMeResponse } from "@/lib/types/referral-me"; +import { + coerceNonNegativeIntegerCount, + parseReferralMeResponse, +} from "@/lib/types/referral-me"; describe("coerceNonNegativeIntegerCount", () => { test("accepts non-negative integers and digit-only strings", () => { @@ -7,7 +10,9 @@ describe("coerceNonNegativeIntegerCount", () => { expect(coerceNonNegativeIntegerCount(42)).toBe(42); expect(coerceNonNegativeIntegerCount(" 7 ")).toBe(7); expect(coerceNonNegativeIntegerCount(0n)).toBe(0); - expect(coerceNonNegativeIntegerCount(9007199254740991n)).toBe(Number.MAX_SAFE_INTEGER); + expect(coerceNonNegativeIntegerCount(9007199254740991n)).toBe( + Number.MAX_SAFE_INTEGER, + ); }); test("rejects null, boolean, float, empty string, decimal string, unsafe bigint", () => { @@ -18,7 +23,9 @@ describe("coerceNonNegativeIntegerCount", () => { expect(coerceNonNegativeIntegerCount("")).toBeNull(); expect(coerceNonNegativeIntegerCount("01")).toBeNull(); expect(coerceNonNegativeIntegerCount("1.5")).toBeNull(); - expect(coerceNonNegativeIntegerCount(BigInt(Number.MAX_SAFE_INTEGER) + 1n)).toBeNull(); + expect( + coerceNonNegativeIntegerCount(BigInt(Number.MAX_SAFE_INTEGER) + 1n), + ).toBeNull(); }); }); diff --git a/packages/tests/unit/security-validations.test.ts b/packages/tests/unit/security-validations.test.ts index f30c9f655..5acbff759 100644 --- a/packages/tests/unit/security-validations.test.ts +++ b/packages/tests/unit/security-validations.test.ts @@ -257,12 +257,14 @@ describe("Webhook Security", () => { describe("Organization ID Validation", () => { it("validates UUID format for organization IDs", () => { const validUUID = "550e8400-e29b-41d4-a716-446655440000"; - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; expect(uuidRegex.test(validUUID)).toBe(true); }); it("rejects invalid organization IDs", () => { - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; expect(uuidRegex.test("not-a-uuid")).toBe(false); expect(uuidRegex.test("")).toBe(false); diff --git a/packages/tests/unit/seo-environment.test.ts b/packages/tests/unit/seo-environment.test.ts index 886a2b778..feb9ee9ee 100644 --- a/packages/tests/unit/seo-environment.test.ts +++ b/packages/tests/unit/seo-environment.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { generateRobotsFile, getRobotsMetadata, shouldIndexSite } from "../../lib/seo"; +import { + generateRobotsFile, + getRobotsMetadata, + shouldIndexSite, +} from "../../lib/seo"; const ORIGINAL_ENV = { ...process.env }; diff --git a/packages/tests/unit/service-key-auth.test.ts b/packages/tests/unit/service-key-auth.test.ts index ed6387042..e1c57c28c 100644 --- a/packages/tests/unit/service-key-auth.test.ts +++ b/packages/tests/unit/service-key-auth.test.ts @@ -56,9 +56,14 @@ function runServiceKeyCase( return JSON.parse(result.stdout.toString()) as ServiceKeyResult; } -function buildScript(action: "validate" | "require", headerValue?: string): string { +function buildScript( + action: "validate" | "require", + headerValue?: string, +): string { const headersObject = - headerValue === undefined ? "{}" : JSON.stringify({ "X-Service-Key": headerValue }); + headerValue === undefined + ? "{}" + : JSON.stringify({ "X-Service-Key": headerValue }); return ` import { validateServiceKey, requireServiceKey } from "./lib/auth/service-key"; @@ -112,16 +117,21 @@ describe("Service Key Auth", () => { }); test("returns null when WAIFU_SERVICE_KEY env is not set", () => { - const result = runServiceKeyCase(buildScript("validate", "test-secret-key-abc123"), { - WAIFU_SERVICE_KEY: undefined, - WAIFU_SERVICE_ORG_ID: "org-uuid-123", - WAIFU_SERVICE_USER_ID: "user-uuid-456", - }); + const result = runServiceKeyCase( + buildScript("validate", "test-secret-key-abc123"), + { + WAIFU_SERVICE_KEY: undefined, + WAIFU_SERVICE_ORG_ID: "org-uuid-123", + WAIFU_SERVICE_USER_ID: "user-uuid-456", + }, + ); expect(result).toEqual({ ok: true, value: null }); }); test("returns identity when key matches", () => { - const result = runServiceKeyCase(buildScript("validate", "test-secret-key-abc123")); + const result = runServiceKeyCase( + buildScript("validate", "test-secret-key-abc123"), + ); expect(result).toEqual({ ok: true, value: { @@ -132,11 +142,14 @@ describe("Service Key Auth", () => { }); test("throws when key matches but org/user env vars are missing", () => { - const result = runServiceKeyCase(buildScript("validate", "test-secret-key-abc123"), { - WAIFU_SERVICE_KEY: "test-secret-key-abc123", - WAIFU_SERVICE_ORG_ID: undefined, - WAIFU_SERVICE_USER_ID: "user-uuid-456", - }); + const result = runServiceKeyCase( + buildScript("validate", "test-secret-key-abc123"), + { + WAIFU_SERVICE_KEY: "test-secret-key-abc123", + WAIFU_SERVICE_ORG_ID: undefined, + WAIFU_SERVICE_USER_ID: "user-uuid-456", + }, + ); expect(result.ok).toBe(false); expect(result.errorMessage).toContain( "WAIFU_SERVICE_ORG_ID and WAIFU_SERVICE_USER_ID must be set", @@ -144,11 +157,14 @@ describe("Service Key Auth", () => { }); test("throws when key matches but user env var is missing", () => { - const result = runServiceKeyCase(buildScript("validate", "test-secret-key-abc123"), { - WAIFU_SERVICE_KEY: "test-secret-key-abc123", - WAIFU_SERVICE_ORG_ID: "org-uuid-123", - WAIFU_SERVICE_USER_ID: undefined, - }); + const result = runServiceKeyCase( + buildScript("validate", "test-secret-key-abc123"), + { + WAIFU_SERVICE_KEY: "test-secret-key-abc123", + WAIFU_SERVICE_ORG_ID: "org-uuid-123", + WAIFU_SERVICE_USER_ID: undefined, + }, + ); expect(result.ok).toBe(false); expect(result.errorMessage).toContain( "WAIFU_SERVICE_ORG_ID and WAIFU_SERVICE_USER_ID must be set", @@ -158,7 +174,9 @@ describe("Service Key Auth", () => { describe("requireServiceKey", () => { test("returns identity for valid key", () => { - const result = runServiceKeyCase(buildScript("require", "test-secret-key-abc123")); + const result = runServiceKeyCase( + buildScript("require", "test-secret-key-abc123"), + ); expect(result).toEqual({ ok: true, value: { diff --git a/packages/tests/unit/sidebar-data.test.ts b/packages/tests/unit/sidebar-data.test.ts index 91d73f462..72d8a9a07 100644 --- a/packages/tests/unit/sidebar-data.test.ts +++ b/packages/tests/unit/sidebar-data.test.ts @@ -9,15 +9,22 @@ describe("sidebarSections", () => { const [dashboardSection, infrastructureSection] = sidebarSections; expect(dashboardSection?.title).toBeUndefined(); - expect(dashboardSection?.items.map((item) => item.label)).toEqual(["Dashboard"]); + expect(dashboardSection?.items.map((item) => item.label)).toEqual([ + "Dashboard", + ]); expect(infrastructureSection?.title).toBe("Infrastructure"); - expect(infrastructureSection?.items.map((item) => item.label)).toEqual(["Instances", "MCPs"]); + expect(infrastructureSection?.items.map((item) => item.label)).toEqual([ + "Instances", + "MCPs", + ]); expect(infrastructureSection?.items[0]?.href).toBe("/dashboard/milady"); }); test("does not expose Containers as a sidebar item", () => { - const labels = sidebarSections.flatMap((section) => section.items.map((item) => item.label)); + const labels = sidebarSections.flatMap((section) => + section.items.map((item) => item.label), + ); expect(labels).not.toContain("Containers"); }); }); diff --git a/packages/tests/unit/token-agent-linkage.test.ts b/packages/tests/unit/token-agent-linkage.test.ts index cb13b5170..2344a8f3a 100644 --- a/packages/tests/unit/token-agent-linkage.test.ts +++ b/packages/tests/unit/token-agent-linkage.test.ts @@ -48,7 +48,9 @@ describe("Token linkage schema validation", () => { }); expect(result.success).toBe(true); if (result.success) { - expect(result.data.tokenAddress).toBe("0x1234567890abcdef1234567890abcdef12345678"); + expect(result.data.tokenAddress).toBe( + "0x1234567890abcdef1234567890abcdef12345678", + ); expect(result.data.tokenChain).toBe("ethereum"); expect(result.data.tokenName).toBe("TestToken"); expect(result.data.tokenTicker).toBe("TST"); @@ -214,7 +216,9 @@ describe("normalizeTokenAddress", () => { test("lowercases 42-char 0x addresses even when chain is unknown", () => { const addr = "0xAbCdEf1234567890AbCdEf1234567890AbCdEf12"; - expect(normalizeTokenAddress(addr, "some-new-evm-chain")).toBe(addr.toLowerCase()); + expect(normalizeTokenAddress(addr, "some-new-evm-chain")).toBe( + addr.toLowerCase(), + ); }); test("preserves Solana base58 addresses (case-sensitive)", () => { @@ -255,7 +259,12 @@ describe("Token lookup logic", () => { * exact match only — preserves case-sensitivity. */ function findByTokenAddress( - characters: Array<{ id: string; token_address: string; token_chain: string; name: string }>, + characters: Array<{ + id: string; + token_address: string; + token_chain: string; + name: string; + }>, address: string, chain?: string, ) { @@ -263,7 +272,8 @@ describe("Token lookup logic", () => { const isLowered = normalized === normalized.toLowerCase(); return characters.find((c) => { const addressMatch = isLowered - ? c.token_address === normalized || c.token_address.toLowerCase() === normalized + ? c.token_address === normalized || + c.token_address.toLowerCase() === normalized : c.token_address === normalized; return addressMatch && (chain == null || c.token_chain === chain); }); @@ -271,26 +281,56 @@ describe("Token lookup logic", () => { test("findByTokenAddress filters by address and chain", () => { const characters = [ - { id: "1", token_address: "0xaaa", token_chain: "ethereum", name: "ETH Agent" }, - { id: "2", token_address: "0xaaa", token_chain: "base", name: "Base Agent" }, - { id: "3", token_address: "SoLANAaddr123", token_chain: "solana", name: "Sol Agent" }, + { + id: "1", + token_address: "0xaaa", + token_chain: "ethereum", + name: "ETH Agent", + }, + { + id: "2", + token_address: "0xaaa", + token_chain: "base", + name: "Base Agent", + }, + { + id: "3", + token_address: "SoLANAaddr123", + token_chain: "solana", + name: "Sol Agent", + }, ]; - expect(findByTokenAddress(characters, "0xAAA", "ethereum")?.name).toBe("ETH Agent"); - expect(findByTokenAddress(characters, "0xAAA", "base")?.name).toBe("Base Agent"); - expect(findByTokenAddress(characters, "SoLANAaddr123")?.name).toBe("Sol Agent"); + expect(findByTokenAddress(characters, "0xAAA", "ethereum")?.name).toBe( + "ETH Agent", + ); + expect(findByTokenAddress(characters, "0xAAA", "base")?.name).toBe( + "Base Agent", + ); + expect(findByTokenAddress(characters, "SoLANAaddr123")?.name).toBe( + "Sol Agent", + ); expect(findByTokenAddress(characters, "0xCCC")).toBeUndefined(); }); test("findByTokenAddress matches EVM addresses regardless of checksum casing", () => { const characters = [ - { id: "1", token_address: "0xabcdef", token_chain: "ethereum", name: "ETH Agent" }, + { + id: "1", + token_address: "0xabcdef", + token_chain: "ethereum", + name: "ETH Agent", + }, ]; // Uppercase variant should still match - expect(findByTokenAddress(characters, "0xABCDEF", "ethereum")?.name).toBe("ETH Agent"); + expect(findByTokenAddress(characters, "0xABCDEF", "ethereum")?.name).toBe( + "ETH Agent", + ); // Mixed-case (checksum) variant should still match - expect(findByTokenAddress(characters, "0xAbCdEf", "ethereum")?.name).toBe("ETH Agent"); + expect(findByTokenAddress(characters, "0xAbCdEf", "ethereum")?.name).toBe( + "ETH Agent", + ); }); test("Solana addresses remain case-sensitive", () => { @@ -305,19 +345,36 @@ describe("Token lookup logic", () => { // Exact match works expect( - findByTokenAddress(characters, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "solana") - ?.name, + findByTokenAddress( + characters, + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "solana", + )?.name, ).toBe("Sol Agent"); // Wrong case should NOT match (base58 is case-sensitive) expect( - findByTokenAddress(characters, "epjfwdd5aufqssqem2qn1xzybapC8G4wEGGkZwyTDt1v", "solana"), + findByTokenAddress( + characters, + "epjfwdd5aufqssqem2qn1xzybapC8G4wEGGkZwyTDt1v", + "solana", + ), ).toBeUndefined(); }); test("listTokenLinked filters by chain", () => { const characters = [ - { id: "1", token_address: "0xAAA", token_chain: "ethereum", name: "ETH Agent" }, - { id: "2", token_address: "0xBBB", token_chain: "base", name: "Base Agent" }, + { + id: "1", + token_address: "0xAAA", + token_chain: "ethereum", + name: "ETH Agent", + }, + { + id: "2", + token_address: "0xBBB", + token_chain: "base", + name: "Base Agent", + }, { id: "3", token_address: null as string | null, @@ -328,7 +385,8 @@ describe("Token lookup logic", () => { function listTokenLinked(chain?: string) { return characters.filter( - (c) => c.token_address != null && (chain == null || c.token_chain === chain), + (c) => + c.token_address != null && (chain == null || c.token_chain === chain), ); } @@ -349,7 +407,8 @@ describe("Token lookup logic", () => { const normalizedIncoming = normalizeTokenAddress(incoming, "base"); const isDuplicate = - existingChar.token_address === normalizedIncoming && existingChar.token_chain === "base"; + existingChar.token_address === normalizedIncoming && + existingChar.token_chain === "base"; expect(isDuplicate).toBe(true); @@ -426,12 +485,18 @@ describe("JSONB fallback for milady agents", () => { }; const char = undefined as - | { token_address?: string; token_chain?: string; token_name?: string; token_ticker?: string } + | { + token_address?: string; + token_chain?: string; + token_name?: string; + token_ticker?: string; + } | undefined; const cfg = agentConfig; const resolved = { - token_address: char?.token_address ?? (cfg?.tokenContractAddress as string) ?? null, + token_address: + char?.token_address ?? (cfg?.tokenContractAddress as string) ?? null, token_chain: char?.token_chain ?? (cfg?.chain as string) ?? null, token_name: char?.token_name ?? (cfg?.tokenName as string) ?? null, token_ticker: char?.token_ticker ?? (cfg?.tokenTicker as string) ?? null, @@ -459,7 +524,8 @@ describe("JSONB fallback for milady agents", () => { }; const resolved = { - token_address: char?.token_address ?? (cfg?.tokenContractAddress as string) ?? null, + token_address: + char?.token_address ?? (cfg?.tokenContractAddress as string) ?? null, token_chain: char?.token_chain ?? (cfg?.chain as string) ?? null, token_name: char?.token_name ?? (cfg?.tokenName as string) ?? null, token_ticker: char?.token_ticker ?? (cfg?.tokenTicker as string) ?? null, diff --git a/packages/tests/unit/twilio-automation-service.test.ts b/packages/tests/unit/twilio-automation-service.test.ts index b17bbb677..71868aaa6 100644 --- a/packages/tests/unit/twilio-automation-service.test.ts +++ b/packages/tests/unit/twilio-automation-service.test.ts @@ -12,429 +12,458 @@ import { beforeEach, describe, expect, it } from "bun:test"; import { twilioAutomationService } from "@/lib/services/twilio-automation"; -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "TwilioAutomationService", - () => { - const testOrgId = "11111111-1111-1111-1111-111111111111"; - const _testUserId = "22222222-2222-2222-2222-222222222222"; - const testAccountSid = "ACtest12345678901234567890123456"; - const testAuthToken = "test_auth_token_12345678901234567"; - - beforeEach(() => { - // Clear the status cache before each test - twilioAutomationService.invalidateStatusCache(testOrgId); - }); - - describe("validateCredentials", () => { - it("returns invalid when accountSid is empty", async () => { - const result = await twilioAutomationService.validateCredentials("", testAuthToken); - expect(result.valid).toBe(false); - expect(result.error).toContain("required"); - }); - - it("returns invalid when authToken is empty", async () => { - const result = await twilioAutomationService.validateCredentials(testAccountSid, ""); - expect(result.valid).toBe(false); - expect(result.error).toContain("required"); - }); +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("TwilioAutomationService", () => { + const testOrgId = "11111111-1111-1111-1111-111111111111"; + const _testUserId = "22222222-2222-2222-2222-222222222222"; + const testAccountSid = "ACtest12345678901234567890123456"; + const testAuthToken = "test_auth_token_12345678901234567"; + + beforeEach(() => { + // Clear the status cache before each test + twilioAutomationService.invalidateStatusCache(testOrgId); + }); + + describe("validateCredentials", () => { + it("returns invalid when accountSid is empty", async () => { + const result = await twilioAutomationService.validateCredentials( + "", + testAuthToken, + ); + expect(result.valid).toBe(false); + expect(result.error).toContain("required"); + }); - it("returns invalid when both are empty", async () => { - const result = await twilioAutomationService.validateCredentials("", ""); - expect(result.valid).toBe(false); - expect(result.error).toContain("required"); - }); + it("returns invalid when authToken is empty", async () => { + const result = await twilioAutomationService.validateCredentials( + testAccountSid, + "", + ); + expect(result.valid).toBe(false); + expect(result.error).toContain("required"); + }); - // Note: Full validation tests would require mocking twilioApiRequest - // or actual Twilio credentials + it("returns invalid when both are empty", async () => { + const result = await twilioAutomationService.validateCredentials("", ""); + expect(result.valid).toBe(false); + expect(result.error).toContain("required"); }); - describe("invalidateStatusCache", () => { - it("clears cache for organization", () => { - expect(() => { - twilioAutomationService.invalidateStatusCache(testOrgId); - }).not.toThrow(); - }); + // Note: Full validation tests would require mocking twilioApiRequest + // or actual Twilio credentials + }); - it("handles multiple invalidations", () => { - expect(() => { - twilioAutomationService.invalidateStatusCache(testOrgId); - twilioAutomationService.invalidateStatusCache(testOrgId); - twilioAutomationService.invalidateStatusCache("33333333-3000-3000-3000-333333333333"); - }).not.toThrow(); - }); + describe("invalidateStatusCache", () => { + it("clears cache for organization", () => { + expect(() => { + twilioAutomationService.invalidateStatusCache(testOrgId); + }).not.toThrow(); }); - describe("getWebhookUrl", () => { - it("returns correct webhook URL format", () => { - const url = twilioAutomationService.getWebhookUrl(testOrgId); - expect(url).toContain("/api/webhooks/twilio/"); - expect(url).toContain(testOrgId); - }); + it("handles multiple invalidations", () => { + expect(() => { + twilioAutomationService.invalidateStatusCache(testOrgId); + twilioAutomationService.invalidateStatusCache(testOrgId); + twilioAutomationService.invalidateStatusCache( + "33333333-3000-3000-3000-333333333333", + ); + }).not.toThrow(); + }); + }); - it("includes organization ID in URL", () => { - const orgId = "44444444-4444-4444-4444-444444444444"; - const url = twilioAutomationService.getWebhookUrl(orgId); - expect(url).toContain(orgId); - }); + describe("getWebhookUrl", () => { + it("returns correct webhook URL format", () => { + const url = twilioAutomationService.getWebhookUrl(testOrgId); + expect(url).toContain("/api/webhooks/twilio/"); + expect(url).toContain(testOrgId); }); - describe("sendMessage", () => { - it("returns error when Twilio is not configured", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "Test message", - }); + it("includes organization ID in URL", () => { + const orgId = "44444444-4444-4444-4444-444444444444"; + const url = twilioAutomationService.getWebhookUrl(orgId); + expect(url).toContain(orgId); + }); + }); - expect(result.success).toBe(false); - expect(result.error).toContain("not configured"); + describe("sendMessage", () => { + it("returns error when Twilio is not configured", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "Test message", }); - it("validates E.164 phone number format", async () => { - // Invalid phone number format - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "5551234567", // Missing + - body: "Test message", - }); + expect(result.success).toBe(false); + expect(result.error).toContain("not configured"); + }); - // Should fail - either due to validation or not configured - expect(result.success).toBe(false); + it("validates E.164 phone number format", async () => { + // Invalid phone number format + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "5551234567", // Missing + + body: "Test message", }); - it("accepts valid E.164 format", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "Test message", - }); + // Should fail - either due to validation or not configured + expect(result.success).toBe(false); + }); - // Will fail because not configured, but format is valid - expect(result.success).toBe(false); + it("accepts valid E.164 format", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "Test message", }); - it("handles message with media URLs", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "Check this out", - mediaUrl: ["https://example.com/image.jpg"], - }); + // Will fail because not configured, but format is valid + expect(result.success).toBe(false); + }); - expect(result.success).toBe(false); + it("handles message with media URLs", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "Check this out", + mediaUrl: ["https://example.com/image.jpg"], }); - it("handles message with multiple media URLs", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "Multiple images", - mediaUrl: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"], - }); + expect(result.success).toBe(false); + }); - expect(result.success).toBe(false); + it("handles message with multiple media URLs", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "Multiple images", + mediaUrl: [ + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + ], }); - it("handles message with status callback", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "With callback", - statusCallback: "https://example.com/callback", - }); + expect(result.success).toBe(false); + }); - expect(result.success).toBe(false); + it("handles message with status callback", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "With callback", + statusCallback: "https://example.com/callback", }); + + expect(result.success).toBe(false); }); + }); - describe("isConfigured", () => { - it("returns false when no credentials stored", async () => { - const result = await twilioAutomationService.isConfigured(testOrgId); - expect(typeof result).toBe("boolean"); - }); + describe("isConfigured", () => { + it("returns false when no credentials stored", async () => { + const result = await twilioAutomationService.isConfigured(testOrgId); + expect(typeof result).toBe("boolean"); }); + }); - describe("getConnectionStatus", () => { - it("returns unconfigured status when no credentials", async () => { - const status = await twilioAutomationService.getConnectionStatus(testOrgId); + describe("getConnectionStatus", () => { + it("returns unconfigured status when no credentials", async () => { + const status = + await twilioAutomationService.getConnectionStatus(testOrgId); - expect(status).toHaveProperty("connected"); - expect(status).toHaveProperty("configured"); - expect(typeof status.connected).toBe("boolean"); - expect(typeof status.configured).toBe("boolean"); - }); + expect(status).toHaveProperty("connected"); + expect(status).toHaveProperty("configured"); + expect(typeof status.connected).toBe("boolean"); + expect(typeof status.configured).toBe("boolean"); + }); - it("caches status for performance", async () => { - const status1 = await twilioAutomationService.getConnectionStatus(testOrgId); - const status2 = await twilioAutomationService.getConnectionStatus(testOrgId); + it("caches status for performance", async () => { + const status1 = + await twilioAutomationService.getConnectionStatus(testOrgId); + const status2 = + await twilioAutomationService.getConnectionStatus(testOrgId); - // Results should be identical (from cache) - expect(status1.connected).toBe(status2.connected); - expect(status1.configured).toBe(status2.configured); - }); + // Results should be identical (from cache) + expect(status1.connected).toBe(status2.connected); + expect(status1.configured).toBe(status2.configured); + }); - it("respects skipCache option", async () => { - const _status1 = await twilioAutomationService.getConnectionStatus(testOrgId); - const status2 = await twilioAutomationService.getConnectionStatus(testOrgId, { + it("respects skipCache option", async () => { + const _status1 = + await twilioAutomationService.getConnectionStatus(testOrgId); + const status2 = await twilioAutomationService.getConnectionStatus( + testOrgId, + { skipCache: true, - }); - - expect(status2).toHaveProperty("connected"); - }); + }, + ); - it("includes phoneNumber when available", async () => { - const status = await twilioAutomationService.getConnectionStatus(testOrgId); - - // phoneNumber is optional - if (status.connected) { - expect(typeof status.phoneNumber === "string" || status.phoneNumber === undefined).toBe( - true, - ); - } - }); + expect(status2).toHaveProperty("connected"); }); - describe("Credential Retrieval Methods", () => { - describe("getAccountSid", () => { - it("returns null when no credential stored", async () => { - const sid = await twilioAutomationService.getAccountSid( - "55555555-5555-5555-5555-555555555555", - ); - expect(sid === null || typeof sid === "string").toBe(true); - }); - }); + it("includes phoneNumber when available", async () => { + const status = + await twilioAutomationService.getConnectionStatus(testOrgId); - describe("getAuthToken", () => { - it("returns null when no credential stored", async () => { - const token = await twilioAutomationService.getAuthToken( - "55555555-5555-5555-5555-555555555555", - ); - expect(token === null || typeof token === "string").toBe(true); - }); - }); + // phoneNumber is optional + if (status.connected) { + expect( + typeof status.phoneNumber === "string" || + status.phoneNumber === undefined, + ).toBe(true); + } + }); + }); - describe("getPhoneNumber", () => { - it("returns null when no phone number stored", async () => { - const phone = await twilioAutomationService.getPhoneNumber( - "55555555-5555-5555-5555-555555555555", - ); - expect(phone === null || typeof phone === "string").toBe(true); - }); + describe("Credential Retrieval Methods", () => { + describe("getAccountSid", () => { + it("returns null when no credential stored", async () => { + const sid = await twilioAutomationService.getAccountSid( + "55555555-5555-5555-5555-555555555555", + ); + expect(sid === null || typeof sid === "string").toBe(true); }); }); - describe("Error Handling", () => { - it("handles empty organization ID", async () => { - const status = await twilioAutomationService.getConnectionStatus( - "00000000-0000-0000-0000-000000000000", + describe("getAuthToken", () => { + it("returns null when no credential stored", async () => { + const token = await twilioAutomationService.getAuthToken( + "55555555-5555-5555-5555-555555555555", ); - expect(status).toHaveProperty("connected"); + expect(token === null || typeof token === "string").toBe(true); }); + }); - it("handles special characters in organization ID", async () => { - const status = await twilioAutomationService.getConnectionStatus( - "00000000-0000-0000-0000-000000000001", + describe("getPhoneNumber", () => { + it("returns null when no phone number stored", async () => { + const phone = await twilioAutomationService.getPhoneNumber( + "55555555-5555-5555-5555-555555555555", ); - expect(status).toHaveProperty("connected"); + expect(phone === null || typeof phone === "string").toBe(true); }); }); + }); - describe("Phone Number Validation", () => { - // Test E.164 format validation through sendMessage + describe("Error Handling", () => { + it("handles empty organization ID", async () => { + const status = await twilioAutomationService.getConnectionStatus( + "00000000-0000-0000-0000-000000000000", + ); + expect(status).toHaveProperty("connected"); + }); - it("rejects phone number without plus sign", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "15551234567", - body: "Test", - }); - expect(result.success).toBe(false); - }); + it("handles special characters in organization ID", async () => { + const status = await twilioAutomationService.getConnectionStatus( + "00000000-0000-0000-0000-000000000001", + ); + expect(status).toHaveProperty("connected"); + }); + }); - it("rejects phone number with spaces", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+1 555 123 4567", - body: "Test", - }); - expect(result.success).toBe(false); - }); + describe("Phone Number Validation", () => { + // Test E.164 format validation through sendMessage - it("rejects phone number with dashes", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+1-555-123-4567", - body: "Test", - }); - expect(result.success).toBe(false); + it("rejects phone number without plus sign", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "15551234567", + body: "Test", }); + expect(result.success).toBe(false); + }); - it("rejects phone number with parentheses", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+1 (555) 123-4567", - body: "Test", - }); - expect(result.success).toBe(false); + it("rejects phone number with spaces", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+1 555 123 4567", + body: "Test", }); + expect(result.success).toBe(false); + }); - it("accepts international phone number", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+442071234567", // UK number - body: "Test", - }); - // Will fail because not configured, but format is valid - expect(result.success).toBe(false); + it("rejects phone number with dashes", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+1-555-123-4567", + body: "Test", }); + expect(result.success).toBe(false); }); - describe("Message Request Handling", () => { - it("handles body-only message", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "Simple text message", - }); - expect(result).toHaveProperty("success"); + it("rejects phone number with parentheses", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+1 (555) 123-4567", + body: "Test", }); + expect(result.success).toBe(false); + }); - it("handles empty body with media", async () => { - // Twilio allows sending MMS with just media (no body) - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - mediaUrl: ["https://example.com/image.jpg"], - }); - expect(result).toHaveProperty("success"); + it("accepts international phone number", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+442071234567", // UK number + body: "Test", }); + // Will fail because not configured, but format is valid + expect(result.success).toBe(false); + }); + }); - it("handles very long message body", async () => { - const longBody = "A".repeat(1600); // SMS segment limit - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: longBody, - }); - expect(result).toHaveProperty("success"); + describe("Message Request Handling", () => { + it("handles body-only message", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "Simple text message", }); + expect(result).toHaveProperty("success"); + }); - it("handles unicode in message body", async () => { - const result = await twilioAutomationService.sendMessage(testOrgId, { - to: "+15551234567", - body: "Hello 世界! 🎉 Émojis work too", - }); - expect(result).toHaveProperty("success"); + it("handles empty body with media", async () => { + // Twilio allows sending MMS with just media (no body) + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + mediaUrl: ["https://example.com/image.jpg"], }); + expect(result).toHaveProperty("success"); }); - describe("Credential Lifecycle", () => { - it("exposes storeCredentials method", () => { - expect(typeof twilioAutomationService.storeCredentials).toBe("function"); + it("handles very long message body", async () => { + const longBody = "A".repeat(1600); // SMS segment limit + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: longBody, }); + expect(result).toHaveProperty("success"); + }); - it("exposes removeCredentials method", () => { - expect(typeof twilioAutomationService.removeCredentials).toBe("function"); + it("handles unicode in message body", async () => { + const result = await twilioAutomationService.sendMessage(testOrgId, { + to: "+15551234567", + body: "Hello 世界! 🎉 Émojis work too", }); - - // Full credential lifecycle tests would require database integration - // These are tested in integration tests + expect(result).toHaveProperty("success"); }); - }, -); + }); -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "TwilioAutomationService Cache Behavior", - () => { - const cacheTestOrgId = "66666666-6666-6666-6666-666666666666"; - - beforeEach(() => { - twilioAutomationService.invalidateStatusCache(cacheTestOrgId); + describe("Credential Lifecycle", () => { + it("exposes storeCredentials method", () => { + expect(typeof twilioAutomationService.storeCredentials).toBe("function"); }); - it("cache TTL is reasonable (5 minutes)", async () => { - // This test verifies cache behavior without waiting for actual TTL - const status1 = await twilioAutomationService.getConnectionStatus(cacheTestOrgId); - - // Immediately after, should return cached result - const status2 = await twilioAutomationService.getConnectionStatus(cacheTestOrgId); - - expect(status1.connected).toBe(status2.connected); - expect(status1.configured).toBe(status2.configured); + it("exposes removeCredentials method", () => { + expect(typeof twilioAutomationService.removeCredentials).toBe("function"); }); - it("skipCache forces fresh fetch", async () => { - const status1 = await twilioAutomationService.getConnectionStatus(cacheTestOrgId); + // Full credential lifecycle tests would require database integration + // These are tested in integration tests + }); +}); - // With skipCache, should make fresh call - const status2 = await twilioAutomationService.getConnectionStatus(cacheTestOrgId, { - skipCache: true, - }); +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("TwilioAutomationService Cache Behavior", () => { + const cacheTestOrgId = "66666666-6666-6666-6666-666666666666"; - // Both should be valid responses - expect(status1).toHaveProperty("connected"); - expect(status2).toHaveProperty("connected"); - }); + beforeEach(() => { + twilioAutomationService.invalidateStatusCache(cacheTestOrgId); + }); - it("invalidateStatusCache clears cache", async () => { - // First call to populate cache + it("cache TTL is reasonable (5 minutes)", async () => { + // This test verifies cache behavior without waiting for actual TTL + const status1 = await twilioAutomationService.getConnectionStatus(cacheTestOrgId); - // Invalidate - twilioAutomationService.invalidateStatusCache(cacheTestOrgId); - - // Should not throw - const status = await twilioAutomationService.getConnectionStatus(cacheTestOrgId); - expect(status).toHaveProperty("connected"); - }); - }, -); + // Immediately after, should return cached result + const status2 = + await twilioAutomationService.getConnectionStatus(cacheTestOrgId); -describe.skipIf(!process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1")( - "TwilioAutomationService Edge Cases", - () => { - it("handles concurrent status checks for same org", async () => { - const orgId = "77777777-7777-7777-7777-777777777777"; - twilioAutomationService.invalidateStatusCache(orgId); + expect(status1.connected).toBe(status2.connected); + expect(status1.configured).toBe(status2.configured); + }); - // Make multiple concurrent requests - const promises = Array(5) - .fill(null) - .map(() => twilioAutomationService.getConnectionStatus(orgId)); + it("skipCache forces fresh fetch", async () => { + const status1 = + await twilioAutomationService.getConnectionStatus(cacheTestOrgId); - const results = await Promise.all(promises); + // With skipCache, should make fresh call + const status2 = await twilioAutomationService.getConnectionStatus( + cacheTestOrgId, + { + skipCache: true, + }, + ); - // All should succeed - for (const status of results) { - expect(status).toHaveProperty("connected"); - expect(status).toHaveProperty("configured"); - } - }); + // Both should be valid responses + expect(status1).toHaveProperty("connected"); + expect(status2).toHaveProperty("connected"); + }); - it("handles concurrent status checks for different orgs", async () => { - const orgIds = [ - "88888888-8888-8888-8888-888888888881", - "88888888-8888-8888-8888-888888888882", - "88888888-8888-8888-8888-888888888883", - "88888888-8888-8888-8888-888888888884", - "88888888-8888-8888-8888-888888888885", - ]; + it("invalidateStatusCache clears cache", async () => { + // First call to populate cache + await twilioAutomationService.getConnectionStatus(cacheTestOrgId); - for (const orgId of orgIds) { - twilioAutomationService.invalidateStatusCache(orgId); - } + // Invalidate + twilioAutomationService.invalidateStatusCache(cacheTestOrgId); - const promises = orgIds.map((orgId) => twilioAutomationService.getConnectionStatus(orgId)); + // Should not throw + const status = + await twilioAutomationService.getConnectionStatus(cacheTestOrgId); + expect(status).toHaveProperty("connected"); + }); +}); + +describe.skipIf( + !process.env.DATABASE_URL || process.env.SKIP_DB_DEPENDENT === "1", +)("TwilioAutomationService Edge Cases", () => { + it("handles concurrent status checks for same org", async () => { + const orgId = "77777777-7777-7777-7777-777777777777"; + twilioAutomationService.invalidateStatusCache(orgId); + + // Make multiple concurrent requests + const promises = Array(5) + .fill(null) + .map(() => twilioAutomationService.getConnectionStatus(orgId)); + + const results = await Promise.all(promises); + + // All should succeed + for (const status of results) { + expect(status).toHaveProperty("connected"); + expect(status).toHaveProperty("configured"); + } + }); + + it("handles concurrent status checks for different orgs", async () => { + const orgIds = [ + "88888888-8888-8888-8888-888888888881", + "88888888-8888-8888-8888-888888888882", + "88888888-8888-8888-8888-888888888883", + "88888888-8888-8888-8888-888888888884", + "88888888-8888-8888-8888-888888888885", + ]; + + for (const orgId of orgIds) { + twilioAutomationService.invalidateStatusCache(orgId); + } - const results = await Promise.all(promises); + const promises = orgIds.map((orgId) => + twilioAutomationService.getConnectionStatus(orgId), + ); - for (const status of results) { - expect(status).toHaveProperty("connected"); - } - }); + const results = await Promise.all(promises); - it("handles concurrent message sends", async () => { - const promises = Array(5) - .fill(null) - .map((_, i) => - twilioAutomationService.sendMessage("99999999-9999-9999-9999-999999999999", { + for (const status of results) { + expect(status).toHaveProperty("connected"); + } + }); + + it("handles concurrent message sends", async () => { + const promises = Array(5) + .fill(null) + .map((_, i) => + twilioAutomationService.sendMessage( + "99999999-9999-9999-9999-999999999999", + { to: `+1555000000${i}`, body: `Concurrent message ${i}`, - }), - ); - - const results = await Promise.all(promises); - - // All should return (success or failure), not throw - for (const result of results) { - expect(result).toHaveProperty("success"); - } - }); - }, -); + }, + ), + ); + + const results = await Promise.all(promises); + + // All should return (success or failure), not throw + for (const result of results) { + expect(result).toHaveProperty("success"); + } + }); +}); diff --git a/packages/tests/unit/users-repository-compat.test.ts b/packages/tests/unit/users-repository-compat.test.ts index 74a097906..690d364ff 100644 --- a/packages/tests/unit/users-repository-compat.test.ts +++ b/packages/tests/unit/users-repository-compat.test.ts @@ -29,11 +29,14 @@ describe("UsersRepository WhatsApp schema compatibility cache", () => { }, }; - await expect(repository.getWhatsAppColumnSupport(fakeDatabase, "read")).rejects.toThrow( - "transient schema probe failure", - ); + await expect( + repository.getWhatsAppColumnSupport(fakeDatabase, "read"), + ).rejects.toThrow("transient schema probe failure"); - const support = await repository.getWhatsAppColumnSupport(fakeDatabase, "read"); + const support = await repository.getWhatsAppColumnSupport( + fakeDatabase, + "read", + ); expect(support).toEqual({ users: true, diff --git a/packages/tests/unit/validation.test.ts b/packages/tests/unit/validation.test.ts index d062892fc..fc76cefaf 100644 --- a/packages/tests/unit/validation.test.ts +++ b/packages/tests/unit/validation.test.ts @@ -49,7 +49,9 @@ describe("isValidUUID", () => { expect(isValidUUID("12345678")).toBe(false); expect(isValidUUID("")).toBe(false); expect(isValidUUID("550e8400-e29b-41d4-a716")).toBe(false); // Too short - expect(isValidUUID("550e8400-e29b-41d4-a716-446655440000-extra")).toBe(false); // Too long + expect(isValidUUID("550e8400-e29b-41d4-a716-446655440000-extra")).toBe( + false, + ); // Too long }); test("returns false for UUIDs with invalid characters", () => { @@ -152,7 +154,11 @@ describe("sanitizeUUID", () => { test("rejects UUIDs with embedded invalid characters (fail-fast)", () => { // Embedded invalid chars should cause rejection, not sanitization expect(sanitizeUUID("550e8400-e29b-INVALID-446655440000")).toBeUndefined(); - expect(sanitizeUUID("550e8400\\e29b-41d4-a716-446655440000")).toBeUndefined(); - expect(sanitizeUUID("550e8400-e29b-41d4-a716-446655440000\x00")).toBeUndefined(); + expect( + sanitizeUUID("550e8400\\e29b-41d4-a716-446655440000"), + ).toBeUndefined(); + expect( + sanitizeUUID("550e8400-e29b-41d4-a716-446655440000\x00"), + ).toBeUndefined(); }); }); diff --git a/packages/tests/unit/whatsapp-api-util.test.ts b/packages/tests/unit/whatsapp-api-util.test.ts index 4cefe6f36..2853a8825 100644 --- a/packages/tests/unit/whatsapp-api-util.test.ts +++ b/packages/tests/unit/whatsapp-api-util.test.ts @@ -33,28 +33,42 @@ describe("WhatsApp Webhook Signature Verification", () => { } test("accepts valid signature", () => { - const body = JSON.stringify({ object: "whatsapp_business_account", entry: [] }); + const body = JSON.stringify({ + object: "whatsapp_business_account", + entry: [], + }); const signature = generateSignature(body, APP_SECRET); expect(verifyWhatsAppSignature(APP_SECRET, signature, body)).toBe(true); }); test("rejects invalid signature", () => { - const body = JSON.stringify({ object: "whatsapp_business_account", entry: [] }); - const signature = "sha256=0000000000000000000000000000000000000000000000000000000000000000"; + const body = JSON.stringify({ + object: "whatsapp_business_account", + entry: [], + }); + const signature = + "sha256=0000000000000000000000000000000000000000000000000000000000000000"; expect(verifyWhatsAppSignature(APP_SECRET, signature, body)).toBe(false); }); test("rejects tampered body", () => { - const body = JSON.stringify({ object: "whatsapp_business_account", entry: [] }); + const body = JSON.stringify({ + object: "whatsapp_business_account", + entry: [], + }); const signature = generateSignature(body, APP_SECRET); const tamperedBody = body + "tampered"; - expect(verifyWhatsAppSignature(APP_SECRET, signature, tamperedBody)).toBe(false); + expect(verifyWhatsAppSignature(APP_SECRET, signature, tamperedBody)).toBe( + false, + ); }); test("rejects wrong app secret", () => { const body = JSON.stringify({ object: "whatsapp_business_account" }); const signature = generateSignature(body, APP_SECRET); - expect(verifyWhatsAppSignature("wrong_secret", signature, body)).toBe(false); + expect(verifyWhatsAppSignature("wrong_secret", signature, body)).toBe( + false, + ); }); test("rejects empty signature header", () => { @@ -125,11 +139,15 @@ describe("WhatsApp Webhook Payload Parsing", () => { }); test("rejects payload with wrong object type", () => { - expect(() => parseWhatsAppWebhookPayload({ ...validPayload, object: "page" })).toThrow(); + expect(() => + parseWhatsAppWebhookPayload({ ...validPayload, object: "page" }), + ).toThrow(); }); test("rejects payload with missing entry", () => { - expect(() => parseWhatsAppWebhookPayload({ object: "whatsapp_business_account" })).toThrow(); + expect(() => + parseWhatsAppWebhookPayload({ object: "whatsapp_business_account" }), + ).toThrow(); }); test("rejects invalid JSON structure", () => { @@ -273,7 +291,9 @@ describe("WhatsApp Message Extraction", () => { display_phone_number: "+14245074963", phone_number_id: "phone_id_123", }, - contacts: [{ profile: { name: "User A" }, wa_id: "14245071111" }], + contacts: [ + { profile: { name: "User A" }, wa_id: "14245071111" }, + ], messages: [ { id: "wamid.msg1", diff --git a/packages/tests/unit/whatsapp-automation.test.ts b/packages/tests/unit/whatsapp-automation.test.ts index cc1779da6..eb4d4b8cb 100644 --- a/packages/tests/unit/whatsapp-automation.test.ts +++ b/packages/tests/unit/whatsapp-automation.test.ts @@ -11,7 +11,10 @@ import { describe, expect, test } from "bun:test"; import crypto from "crypto"; -import { isValidWhatsAppId, verifyWhatsAppSignature } from "../../lib/utils/whatsapp-api"; +import { + isValidWhatsAppId, + verifyWhatsAppSignature, +} from "../../lib/utils/whatsapp-api"; // ============================================================================ // Verify Token Generation @@ -62,27 +65,39 @@ describe("WhatsApp Automation - Webhook Subscription Verification", () => { } test("returns challenge when all params match", () => { - expect(verifySubscription("subscribe", STORED_TOKEN, "12345", STORED_TOKEN)).toBe("12345"); + expect( + verifySubscription("subscribe", STORED_TOKEN, "12345", STORED_TOKEN), + ).toBe("12345"); }); test("rejects when mode is not subscribe", () => { - expect(verifySubscription("unsubscribe", STORED_TOKEN, "12345", STORED_TOKEN)).toBeNull(); + expect( + verifySubscription("unsubscribe", STORED_TOKEN, "12345", STORED_TOKEN), + ).toBeNull(); }); test("rejects when verify token does not match", () => { - expect(verifySubscription("subscribe", "wrong_token", "12345", STORED_TOKEN)).toBeNull(); + expect( + verifySubscription("subscribe", "wrong_token", "12345", STORED_TOKEN), + ).toBeNull(); }); test("rejects when challenge is missing", () => { - expect(verifySubscription("subscribe", STORED_TOKEN, null, STORED_TOKEN)).toBeNull(); + expect( + verifySubscription("subscribe", STORED_TOKEN, null, STORED_TOKEN), + ).toBeNull(); }); test("rejects when mode is null", () => { - expect(verifySubscription(null, STORED_TOKEN, "12345", STORED_TOKEN)).toBeNull(); + expect( + verifySubscription(null, STORED_TOKEN, "12345", STORED_TOKEN), + ).toBeNull(); }); test("rejects when no stored token exists", () => { - expect(verifySubscription("subscribe", STORED_TOKEN, "12345", null)).toBeNull(); + expect( + verifySubscription("subscribe", STORED_TOKEN, "12345", null), + ).toBeNull(); }); }); @@ -94,7 +109,9 @@ describe("WhatsApp Automation - Signature Verification Delegation", () => { const APP_SECRET = "org_specific_app_secret_123"; function makeSignature(body: string, secret: string): string { - return "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex"); + return ( + "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex") + ); } test("accepts valid signature with org-specific secret", () => { @@ -199,9 +216,13 @@ describe("WhatsApp Automation - Secret Name Constants", () => { const { SECRET_NAMES } = await import("../../lib/constants/secrets"); expect(SECRET_NAMES.WHATSAPP.ACCESS_TOKEN).toBe("WHATSAPP_ACCESS_TOKEN"); - expect(SECRET_NAMES.WHATSAPP.PHONE_NUMBER_ID).toBe("WHATSAPP_PHONE_NUMBER_ID"); + expect(SECRET_NAMES.WHATSAPP.PHONE_NUMBER_ID).toBe( + "WHATSAPP_PHONE_NUMBER_ID", + ); expect(SECRET_NAMES.WHATSAPP.APP_SECRET).toBe("WHATSAPP_APP_SECRET"); expect(SECRET_NAMES.WHATSAPP.VERIFY_TOKEN).toBe("WHATSAPP_VERIFY_TOKEN"); - expect(SECRET_NAMES.WHATSAPP.BUSINESS_PHONE).toBe("WHATSAPP_BUSINESS_PHONE"); + expect(SECRET_NAMES.WHATSAPP.BUSINESS_PHONE).toBe( + "WHATSAPP_BUSINESS_PHONE", + ); }); }); diff --git a/packages/tests/unit/x402/facilitator-service.test.ts b/packages/tests/unit/x402/facilitator-service.test.ts index 155af9ed1..24b5c7991 100644 --- a/packages/tests/unit/x402/facilitator-service.test.ts +++ b/packages/tests/unit/x402/facilitator-service.test.ts @@ -82,7 +82,9 @@ function createValidPayload( }; } -function createValidRequirements(overrides?: Partial): PaymentRequirements { +function createValidRequirements( + overrides?: Partial, +): PaymentRequirements { return { scheme: "exact", network: "eip155:84532", @@ -148,7 +150,10 @@ describe("x402 Payment Payload Validation", () => { payTo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", }); - expect(payload.accepted.payTo.toLowerCase() !== requirements.payTo.toLowerCase()).toBe(true); + expect( + payload.accepted.payTo.toLowerCase() !== + requirements.payTo.toLowerCase(), + ).toBe(true); }); it("should be case-insensitive for payTo", () => { @@ -159,17 +164,24 @@ describe("x402 Payment Payload Validation", () => { payTo: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }); - expect(payload.accepted.payTo.toLowerCase() === requirements.payTo.toLowerCase()).toBe(true); + expect( + payload.accepted.payTo.toLowerCase() === + requirements.payTo.toLowerCase(), + ).toBe(true); }); }); describe("deadline validation", () => { it("should reject expired deadlines", () => { const pastDeadline = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago - const payload = createValidPayload({ validBefore: pastDeadline.toString() }); + const payload = createValidPayload({ + validBefore: pastDeadline.toString(), + }); const now = Math.floor(Date.now() / 1000); - expect(BigInt(payload.payload.authorization.validBefore) <= BigInt(now)).toBe(true); + expect( + BigInt(payload.payload.authorization.validBefore) <= BigInt(now), + ).toBe(true); }); it("should accept future deadlines", () => { @@ -179,7 +191,9 @@ describe("x402 Payment Payload Validation", () => { }); const now = Math.floor(Date.now() / 1000); - expect(BigInt(payload.payload.authorization.validBefore) > BigInt(now)).toBe(true); + expect( + BigInt(payload.payload.authorization.validBefore) > BigInt(now), + ).toBe(true); }); }); @@ -194,7 +208,12 @@ describe("x402 Payment Payload Validation", () => { describe("network validation", () => { it("should validate CAIP-2 format", () => { - const validNetworks = ["eip155:8453", "eip155:84532", "eip155:1", "eip155:11155111"]; + const validNetworks = [ + "eip155:8453", + "eip155:84532", + "eip155:1", + "eip155:11155111", + ]; for (const network of validNetworks) { expect(network).toMatch(/^eip155:\d+$/); diff --git a/packages/tests/unit/x402/plugin-cloud-integration.test.ts b/packages/tests/unit/x402/plugin-cloud-integration.test.ts index eeb8a2c04..1b775a68b 100644 --- a/packages/tests/unit/x402/plugin-cloud-integration.test.ts +++ b/packages/tests/unit/x402/plugin-cloud-integration.test.ts @@ -9,7 +9,8 @@ import { describe, expect, it } from "bun:test"; import { getAddress, type Hex, verifyTypedData } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -const TEST_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; +const TEST_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; const EXPECTED_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Hex; const FACILITATOR_ADDRESS = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" as Hex; @@ -39,7 +40,13 @@ async function simulateCloudFacilitatorVerify( // Decode the payment header (same as facilitator middleware) let payload: { x402Version: number; - accepted: { scheme: string; network: string; asset: string; amount: string; payTo: string }; + accepted: { + scheme: string; + network: string; + asset: string; + amount: string; + payTo: string; + }; payload: { signature: string; authorization: { @@ -52,7 +59,9 @@ async function simulateCloudFacilitatorVerify( }; }; try { - const decoded = Buffer.from(paymentHeaderBase64, "base64").toString("utf-8"); + const decoded = Buffer.from(paymentHeaderBase64, "base64").toString( + "utf-8", + ); payload = JSON.parse(decoded); } catch { return { isValid: false, invalidReason: "invalid_payment_header" }; @@ -125,7 +134,11 @@ async function simulateCloudFacilitatorVerify( }); if (!isValid) { - return { isValid: false, invalidReason: "invalid_permit_signature", payer }; + return { + isValid: false, + invalidReason: "invalid_permit_signature", + payer, + }; } } catch (err) { return { @@ -441,7 +454,8 @@ describe("x402 Plugin ↔ Eliza Cloud Integration", () => { }); it("should verify payment from a different wallet fails payer check", async () => { - const otherKey = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" as Hex; + const otherKey = + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" as Hex; const otherAccount = privateKeyToAccount(otherKey); const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); diff --git a/packages/tests/unit/z-anthropic-web-search.test.ts b/packages/tests/unit/z-anthropic-web-search.test.ts index 7159d754d..816b9389a 100644 --- a/packages/tests/unit/z-anthropic-web-search.test.ts +++ b/packages/tests/unit/z-anthropic-web-search.test.ts @@ -26,7 +26,9 @@ function requireWebSearchToolMetadata(tool: unknown): { expect(typedTool.args).toBeDefined(); if (!typedTool.args || typeof typedTool.args !== "object") { - throw new Error("Expected the Anthropic web_search tool args to be registered"); + throw new Error( + "Expected the Anthropic web_search tool args to be registered", + ); } return { @@ -39,15 +41,25 @@ function requireWebSearchToolMetadata(tool: unknown): { describe("anthropic web search helpers", () => { test("supports allowlisted Anthropic models and dated variants", () => { expect(supportsAnthropicWebSearch("claude-sonnet-4-6")).toBe(true); - expect(supportsAnthropicWebSearch("anthropic/claude-opus-4-6-20260301")).toBe(true); + expect( + supportsAnthropicWebSearch("anthropic/claude-opus-4-6-20260301"), + ).toBe(true); expect(supportsAnthropicWebSearch("claude-haiku-4-5")).toBe(false); }); test("only enables web search for supported Anthropic models when explicitly requested", () => { - expect(isAnthropicWebSearchEnabled("anthropic", "claude-sonnet-4-6", true)).toBe(true); - expect(isAnthropicWebSearchEnabled("anthropic", "claude-sonnet-4-6", false)).toBe(false); - expect(isAnthropicWebSearchEnabled("openai", "gpt-4o-mini", true)).toBe(false); - expect(isAnthropicWebSearchEnabled("anthropic", "claude-haiku-4-5", true)).toBe(false); + expect( + isAnthropicWebSearchEnabled("anthropic", "claude-sonnet-4-6", true), + ).toBe(true); + expect( + isAnthropicWebSearchEnabled("anthropic", "claude-sonnet-4-6", false), + ).toBe(false); + expect(isAnthropicWebSearchEnabled("openai", "gpt-4o-mini", true)).toBe( + false, + ); + expect( + isAnthropicWebSearchEnabled("anthropic", "claude-haiku-4-5", true), + ).toBe(false); }); test("builds Anthropic provider-native tools with the default maxUses", () => { @@ -66,7 +78,9 @@ describe("anthropic web search helpers", () => { expect(webSearchTool.type).toBe("provider"); expect(webSearchTool.id).toBe("anthropic.web_search_20260209"); - expect(webSearchTool.args.maxUses).toBe(DEFAULT_ANTHROPIC_WEB_SEARCH_MAX_USES); + expect(webSearchTool.args.maxUses).toBe( + DEFAULT_ANTHROPIC_WEB_SEARCH_MAX_USES, + ); }); test("clamps requested maxUses and skips unsupported requests", () => { diff --git a/packages/types/monaco-editor.d.ts b/packages/types/monaco-editor.d.ts index fe1ea3159..62beff63a 100644 --- a/packages/types/monaco-editor.d.ts +++ b/packages/types/monaco-editor.d.ts @@ -7,7 +7,9 @@ declare module "monaco-editor" { dispose(): void; focus(): void; getDomNode(): HTMLElement | null; - onDidChangeModelContent(listener: (e: unknown) => void): { dispose(): void }; + onDidChangeModelContent(listener: (e: unknown) => void): { + dispose(): void; + }; layout(dimension?: { width: number; height: number }): void; updateOptions(options: Record): void; } diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx index 03acc7dfc..0e20aadc9 100644 --- a/packages/ui/src/components/accordion.tsx +++ b/packages/ui/src/components/accordion.tsx @@ -15,7 +15,11 @@ const AccordionItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); AccordionItem.displayName = "AccordionItem"; diff --git a/packages/ui/src/components/account/account-details.tsx b/packages/ui/src/components/account/account-details.tsx index 0efa301cf..e3c8e7ee9 100644 --- a/packages/ui/src/components/account/account-details.tsx +++ b/packages/ui/src/components/account/account-details.tsx @@ -38,19 +38,25 @@ export function AccountDetails({ user }: AccountDetailsProps) {

    Account Details

    -

    View your account status and important dates

    +

    + View your account status and important dates +

    -

    Account ID

    +

    + Account ID +

    {user.id}

    {user.email && (
    -

    Email Verification

    +

    + Email Verification +

    {user.email_verified ? ( <> @@ -98,7 +104,9 @@ export function AccountDetails({ user }: AccountDetailsProps) { )}
    -

    Account Status

    +

    + Account Status +

    @@ -107,7 +115,9 @@ export function AccountDetails({ user }: AccountDetailsProps) {
    -

    Role

    +

    + Role +

    {user.role} @@ -118,7 +128,9 @@ export function AccountDetails({ user }: AccountDetailsProps) { Account Created

    -

    {formatDate(user.created_at)}

    +

    + {formatDate(user.created_at)} +

    @@ -126,7 +138,9 @@ export function AccountDetails({ user }: AccountDetailsProps) { Last Updated

    -

    {formatDate(user.updated_at)}

    +

    + {formatDate(user.updated_at)} +

    @@ -137,7 +151,9 @@ export function AccountDetails({ user }: AccountDetailsProps) { Wallet Address

    -

    {user.wallet_address}

    +

    + {user.wallet_address} +

    {user.wallet_chain_type && ( {user.wallet_chain_type} @@ -151,7 +167,9 @@ export function AccountDetails({ user }: AccountDetailsProps) {

    Authentication Provider

    -

    Privy ID: {user.privy_user_id}

    +

    + Privy ID: {user.privy_user_id} +

    )}
    diff --git a/packages/ui/src/components/account/account-page-client.tsx b/packages/ui/src/components/account/account-page-client.tsx index 63f3b8d07..e44812f2e 100644 --- a/packages/ui/src/components/account/account-page-client.tsx +++ b/packages/ui/src/components/account/account-page-client.tsx @@ -44,7 +44,8 @@ export function AccountPageClient({ user }: AccountPageClientProps) { !

    - You're part of {user.organization?.name}{" "} + You're part of{" "} + {user.organization?.name}{" "} organization

    @@ -60,7 +61,9 @@ export function AccountPageClient({ user }: AccountPageClientProps) { {/* Right Column - Additional Info */}
    - {user.organization && } + {user.organization && ( + + )}
    diff --git a/packages/ui/src/components/account/organization-info.tsx b/packages/ui/src/components/account/organization-info.tsx index 7d8d150f6..80cab95b5 100644 --- a/packages/ui/src/components/account/organization-info.tsx +++ b/packages/ui/src/components/account/organization-info.tsx @@ -40,19 +40,27 @@ export function OrganizationInfo({ organization }: OrganizationInfoProps) {

    Organization

    -

    Information about your organization

    +

    + Information about your organization +

    -

    Organization Name

    +

    + Organization Name +

    {organization.name}

    -

    Slug

    -

    {organization.slug}

    +

    + Slug +

    +

    + {organization.slug} +

    @@ -66,7 +74,9 @@ export function OrganizationInfo({ organization }: OrganizationInfoProps) {
    -

    Status

    +

    + Status +

    @@ -79,13 +89,17 @@ export function OrganizationInfo({ organization }: OrganizationInfoProps) { Member Since

    -

    {formatDate(organization.created_at)}

    +

    + {formatDate(organization.created_at)} +

    {organization.billing_email && (
    -

    Billing Email

    +

    + Billing Email +

    {organization.billing_email}

    )} diff --git a/packages/ui/src/components/account/profile-form.tsx b/packages/ui/src/components/account/profile-form.tsx index 5c9716b94..1b6cc0a6f 100644 --- a/packages/ui/src/components/account/profile-form.tsx +++ b/packages/ui/src/components/account/profile-form.tsx @@ -19,7 +19,16 @@ import { CornerBrackets, Input, } from "@elizaos/cloud-ui"; -import { Check, ImagePlus, Loader2, Mail, Shield, Upload, User, X } from "lucide-react"; +import { + Check, + ImagePlus, + Loader2, + Mail, + Shield, + Upload, + User, + X, +} from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useCallback, useRef, useState, useTransition } from "react"; @@ -28,7 +37,12 @@ import { updateEmail, updateProfile, uploadAvatar } from "@/app/actions/users"; import type { UserWithOrganization } from "@/lib/types"; // Constants for validation -const VALID_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; +const VALID_IMAGE_TYPES = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/webp", +]; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB interface ProfileFormProps { @@ -50,7 +64,11 @@ export function ProfileForm({ user }: ProfileFormProps) { const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); - const getInitials = (name: string | null, email: string | null, walletAddress: string | null) => { + const getInitials = ( + name: string | null, + email: string | null, + walletAddress: string | null, + ) => { if (name) { return name .split(" ") @@ -229,7 +247,9 @@ export function ProfileForm({ user }: ProfileFormProps) {
    -

    Profile Information

    +

    + Profile Information +

    Update your profile information and manage your account settings @@ -249,7 +269,8 @@ export function ProfileForm({ user }: ProfileFormProps) {

    - Adding an email allows you to receive important notifications and updates. + Adding an email allows you to receive important notifications and + updates.

    - Email cannot be changed. Please contact support if you need to update this. + Email cannot be changed. Please contact support if you need to + update this.

    )} @@ -310,10 +332,13 @@ export function ProfileForm({ user }: ProfileFormProps) {
    -

    Email Added Successfully!

    +

    + Email Added Successfully! +

    - Your email has been added and will appear here after the page refreshes. + Your email has been added and will appear here after the page + refreshes.

    )} @@ -387,7 +412,9 @@ export function ProfileForm({ user }: ProfileFormProps) {
    -

    {pendingFile.name}

    +

    + {pendingFile.name} +

    {(pendingFile.size / 1024).toFixed(1)} KB

    @@ -432,7 +459,9 @@ export function ProfileForm({ user }: ProfileFormProps) { role="button" tabIndex={0} onClick={() => fileInputRef.current?.click()} - onKeyDown={(e) => e.key === "Enter" && fileInputRef.current?.click()} + onKeyDown={(e) => + e.key === "Enter" && fileInputRef.current?.click() + } onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} @@ -452,7 +481,9 @@ export function ProfileForm({ user }: ProfileFormProps) { ) : ( <> - Click or drag image to upload + + Click or drag image to upload + )}
    @@ -542,14 +573,21 @@ export function ProfileForm({ user }: ProfileFormProps) { {/* Messages */} {error && ( - - {error} + + + {error} + )} {success && ( - {success} + + {success} + )} diff --git a/packages/ui/src/components/account/security-preferences.tsx b/packages/ui/src/components/account/security-preferences.tsx index 139589f47..bd2e84a81 100644 --- a/packages/ui/src/components/account/security-preferences.tsx +++ b/packages/ui/src/components/account/security-preferences.tsx @@ -17,7 +17,9 @@ export function SecurityPreferences() {
    -

    Security & Preferences

    +

    + Security & Preferences +

    Manage your security settings and notification preferences @@ -53,7 +55,9 @@ export function SecurityPreferences() {

    -

    Two-Factor Authentication

    +

    + Two-Factor Authentication +

    Coming Soon @@ -76,12 +80,16 @@ export function SecurityPreferences() {
    -

    Notification Preferences

    +

    + Notification Preferences +

    Coming Soon
    -

    Control how you receive updates and alerts

    +

    + Control how you receive updates and alerts +

    @@ -106,8 +114,8 @@ export function SecurityPreferences() {

    Delete Account

    - Permanently delete your account and all associated data. This action cannot be - undone. + Permanently delete your account and all associated data. This + action cannot be undone.

    { setLoading(true); try { - const res = await fetch(`/api/v1/admin/metrics?view=overview&timeRange=${timeRange}`); + const res = await fetch( + `/api/v1/admin/metrics?view=overview&timeRange=${timeRange}`, + ); if (!res.ok) throw new Error("Failed to fetch metrics"); setOverview(await res.json()); } catch { @@ -217,9 +219,14 @@ export function AdminMetricsClient() { .map((c) => ({ date: formatDateUTC(c.cohort_date), cohortSize: c.cohort_size, - d1: c.d1_retained != null ? (c.d1_retained / c.cohort_size) * 100 : null, - d7: c.d7_retained != null ? (c.d7_retained / c.cohort_size) * 100 : null, - d30: c.d30_retained != null ? (c.d30_retained / c.cohort_size) * 100 : null, + d1: + c.d1_retained != null ? (c.d1_retained / c.cohort_size) * 100 : null, + d7: + c.d7_retained != null ? (c.d7_retained / c.cohort_size) * 100 : null, + d30: + c.d30_retained != null + ? (c.d30_retained / c.cohort_size) * 100 + : null, })); }, [overview]); @@ -238,7 +245,9 @@ export function AdminMetricsClient() {
    -

    No metrics data available yet.

    +

    + No metrics data available yet. +

    - Adjust the time range to refocus the engagement metrics surface. All widgets update - in real time. + Adjust the time range to refocus the engagement metrics surface. + All widgets update in real time.

    @@ -358,7 +367,9 @@ export function AdminMetricsClient() { label="OAuth Rate" icon={Link2} loading={loading} - value={overview ? `${(overview.oauthRate.rate * 100).toFixed(1)}%` : "0%"} + value={ + overview ? `${(overview.oauthRate.rate * 100).toFixed(1)}%` : "0%" + } helper={ overview ? `${overview.oauthRate.connected_users} of ${overview.oauthRate.total_users} users` @@ -410,12 +421,25 @@ export function AdminMetricsClient() { - - + + - + { const src = payload?.[0]; - if (src && typeof src === "object" && "payload" in src) { + if ( + src && + typeof src === "object" && + "payload" in src + ) { return ( ( src as { @@ -476,10 +504,19 @@ export function AdminMetricsClient() { > - + } /> - + ) : ( @@ -510,10 +547,19 @@ export function AdminMetricsClient() { > - + } /> - + ) : ( @@ -605,18 +651,20 @@ export function AdminMetricsClient() { className="h-full transition-all" style={{ width: `${Math.max(1, (count / total) * 100)}%`, - backgroundColor: PLATFORM_COLORS[platform] || "#888", + backgroundColor: + PLATFORM_COLORS[platform] || "#888", }} />
    ); })} - {overview && Object.keys(overview.platformBreakdown).length === 0 && ( -

    - No platform data yet -

    - )} + {overview && + Object.keys(overview.platformBreakdown).length === 0 && ( +

    + No platform data yet +

    + )} )} @@ -653,7 +701,12 @@ export function AdminMetricsClient() { > - + - value != null ? `${Number(value).toFixed(1)}%` : "N/A" + value != null + ? `${Number(value).toFixed(1)}%` + : "N/A" } /> } @@ -726,11 +781,16 @@ export function AdminMetricsClient() { ) : (
    - {overview ? (overview.oauthRate.rate * 100).toFixed(1) : "0"}% + {overview + ? (overview.oauthRate.rate * 100).toFixed(1) + : "0"} + %

    - {overview?.oauthRate.connected_users.toLocaleString() ?? 0} of{" "} - {overview?.oauthRate.total_users.toLocaleString() ?? 0} users + {overview?.oauthRate.connected_users.toLocaleString() ?? + 0}{" "} + of {overview?.oauthRate.total_users.toLocaleString() ?? 0}{" "} + users

    )} @@ -768,7 +828,11 @@ export function AdminMetricsClient() { width={80} /> } /> - + ) : ( @@ -807,14 +871,18 @@ function StatCell({ className={`backdrop-blur-sm bg-[rgba(10,10,10,0.75)] p-3 md:p-4 space-y-1 ${className ?? ""}`} >
    -

    {label}

    +

    + {label} +

    {loading ? ( ) : ( <> -

    {value}

    +

    + {value} +

    {helper}

    )} diff --git a/packages/ui/src/components/admin/infrastructure-dashboard.tsx b/packages/ui/src/components/admin/infrastructure-dashboard.tsx index b8ca473d4..8eeb26b06 100644 --- a/packages/ui/src/components/admin/infrastructure-dashboard.tsx +++ b/packages/ui/src/components/admin/infrastructure-dashboard.tsx @@ -86,7 +86,11 @@ class TabErrorBoundary extends Component< return { error }; } componentDidCatch(error: Error, info: ErrorInfo) { - console.error(`[TabErrorBoundary] ${this.props.fallback}:`, error, info.componentStack); + console.error( + `[TabErrorBoundary] ${this.props.fallback}:`, + error, + info.componentStack, + ); } render() { if (this.state.error) { @@ -340,21 +344,29 @@ function NodeStatusBadge({ status }: { status: string }) { label: "Healthy", variant: "default", icon: CheckCircle2, - className: "bg-green-500/15 text-green-700 dark:text-green-400 border-green-500/30", + className: + "bg-green-500/15 text-green-700 dark:text-green-400 border-green-500/30", }, degraded: { label: "Degraded", variant: "secondary", icon: AlertTriangle, - className: "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", + className: + "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", }, offline: { label: "Offline", variant: "destructive", icon: XCircle, - className: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", + className: + "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", + }, + unknown: { + label: "Unknown", + variant: "outline", + icon: Clock, + className: "", }, - unknown: { label: "Unknown", variant: "outline", icon: Clock, className: "" }, }; const cfg = map[status] ?? map.unknown; const Icon = cfg.icon; @@ -366,7 +378,13 @@ function NodeStatusBadge({ status }: { status: string }) { ); } -function LiveHealthBadge({ health, severity }: { health: string; severity: string }) { +function LiveHealthBadge({ + health, + severity, +}: { + health: string; + severity: string; +}) { const config: Record< string, { @@ -377,45 +395,58 @@ function LiveHealthBadge({ health, severity }: { health: string; severity: strin > = { healthy: { variant: "default", - className: "bg-green-500/15 text-green-700 dark:text-green-400 border-green-500/30", + className: + "bg-green-500/15 text-green-700 dark:text-green-400 border-green-500/30", icon: CheckCircle2, }, warming: { variant: "outline", - className: "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30", + className: + "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30", icon: Loader2, }, degraded: { variant: "secondary", - className: "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", + className: + "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", icon: AlertTriangle, }, stale: { variant: "destructive", - className: "bg-orange-500/15 text-orange-700 dark:text-orange-400 border-orange-500/30", + className: + "bg-orange-500/15 text-orange-700 dark:text-orange-400 border-orange-500/30", icon: Clock, }, missing: { variant: "destructive", - className: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", + className: + "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", icon: XCircle, }, failed: { variant: "destructive", - className: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", + className: + "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", icon: XCircle, }, stopped: { variant: "secondary", - className: "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30", + className: + "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30", icon: Square, }, }; - const cfg = config[health] ?? { variant: "outline" as const, className: "", icon: Clock }; + const cfg = config[health] ?? { + variant: "outline" as const, + className: "", + icon: Clock, + }; const Icon = cfg.icon; return ( - + {health} ); @@ -424,48 +455,61 @@ function LiveHealthBadge({ health, severity }: { health: string; severity: strin function ContainerStatusBadge({ status }: { status: string }) { const map: Record< string, - { variant: "default" | "secondary" | "destructive" | "outline"; className: string } + { + variant: "default" | "secondary" | "destructive" | "outline"; + className: string; + } > = { running: { variant: "default", - className: "bg-green-500/15 text-green-700 dark:text-green-400 border-green-500/30", + className: + "bg-green-500/15 text-green-700 dark:text-green-400 border-green-500/30", }, stopped: { variant: "secondary", - className: "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", + className: + "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", }, error: { variant: "destructive", - className: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", + className: + "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", }, provisioning: { variant: "outline", - className: "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30", + className: + "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30", }, pending: { variant: "outline", - className: "bg-purple-500/15 text-purple-700 dark:text-purple-400 border-purple-500/30", + className: + "bg-purple-500/15 text-purple-700 dark:text-purple-400 border-purple-500/30", }, disconnected: { variant: "secondary", - className: "bg-orange-500/15 text-orange-700 dark:text-orange-400 border-orange-500/30", + className: + "bg-orange-500/15 text-orange-700 dark:text-orange-400 border-orange-500/30", }, // Ghost container states exited: { variant: "secondary", - className: "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30", + className: + "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30", }, dead: { variant: "destructive", - className: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", + className: + "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30", }, created: { variant: "outline", - className: "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30", + className: + "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30", }, restarting: { variant: "outline", - className: "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", + className: + "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30", }, }; const cfg = map[status] ?? { variant: "outline" as const, className: "" }; @@ -476,7 +520,13 @@ function ContainerStatusBadge({ status }: { status: string }) { ); } -type SortField = "containerName" | "agentName" | "nodeId" | "status" | "liveHealth" | "createdAt"; +type SortField = + | "containerName" + | "agentName" + | "nodeId" + | "status" + | "liveHealth" + | "createdAt"; type SortDirection = "asc" | "desc"; /** Minimal type for docker inspect data to avoid `as any` casts throughout */ @@ -494,7 +544,10 @@ interface DockerInspectData { Platform?: string; Driver?: string; NetworkSettings?: { - Ports?: Record | null>; + Ports?: Record< + string, + Array<{ HostIp?: string; HostPort?: string }> | null + >; }; [key: string]: unknown; } @@ -563,7 +616,9 @@ export function InfrastructureDashboard() { // ---- Data state ---- const [nodes, setNodes] = useState([]); - const [infraSnapshot, setInfraSnapshot] = useState(null); + const [infraSnapshot, setInfraSnapshot] = useState( + null, + ); const [headscale, setHeadscale] = useState(null); // ---- Loading flags ---- @@ -572,7 +627,8 @@ export function InfrastructureDashboard() { const [loadingHeadscale, setLoadingHeadscale] = useState(false); // ---- Container filters ---- - const [containerStatusFilter, setContainerStatusFilter] = useState("all"); + const [containerStatusFilter, setContainerStatusFilter] = + useState("all"); const [containerNodeFilter, setContainerNodeFilter] = useState("all"); const [containerTypeFilter, setContainerTypeFilter] = useState("all"); const [containerSearchQuery, setContainerSearchQuery] = useState(""); @@ -585,7 +641,9 @@ export function InfrastructureDashboard() { const [expandedRows, setExpandedRows] = useState>(new Set()); // ---- Health check loading per node ---- - const [healthChecking, setHealthChecking] = useState>({}); + const [healthChecking, setHealthChecking] = useState>( + {}, + ); // ---- Add Node dialog ---- const [addNodeOpen, setAddNodeOpen] = useState(false); @@ -610,7 +668,9 @@ export function InfrastructureDashboard() { const [editNodeLoading, setEditNodeLoading] = useState(false); // ---- Delete Node confirm ---- - const [deleteNodeTarget, setDeleteNodeTarget] = useState(null); + const [deleteNodeTarget, setDeleteNodeTarget] = useState( + null, + ); const [deleteNodeLoading, setDeleteNodeLoading] = useState(false); // ---- Container logs dialog ---- @@ -623,12 +683,19 @@ export function InfrastructureDashboard() { // ---- Container details dialog ---- const [detailsOpen, setDetailsOpen] = useState(false); const [detailsTarget, setDetailsTarget] = useState(null); - const [detailsData, setDetailsData] = useState(null); - const [detailsResources, setDetailsResources] = useState | null>(null); + const [detailsData, setDetailsData] = useState( + null, + ); + const [detailsResources, setDetailsResources] = useState | null>(null); const [detailsLoading, setDetailsLoading] = useState(false); // ---- Container action loading ---- - const [actionLoading, setActionLoading] = useState>({}); + const [actionLoading, setActionLoading] = useState>( + {}, + ); // ---- Incidents collapsed ---- const [incidentsExpanded, setIncidentsExpanded] = useState(false); @@ -639,7 +706,9 @@ export function InfrastructureDashboard() { const [auditLoading, setAuditLoading] = useState(false); // ---- Post-action refresh timer (cleaned up on unmount) ---- - const actionRefreshTimerRef = useRef | null>(null); + const actionRefreshTimerRef = useRef | null>( + null, + ); useEffect(() => { return () => { @@ -661,7 +730,9 @@ export function InfrastructureDashboard() { if (!json.success) throw new Error(json.error); setNodes(json.data?.nodes ?? []); } catch (err) { - toast.error(`Failed to load nodes: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Failed to load nodes: ${err instanceof Error ? err.message : String(err)}`, + ); } finally { setLoadingNodes(false); } @@ -691,7 +762,9 @@ export function InfrastructureDashboard() { if (!json.success) throw new Error(json.error); setHeadscale(json.data); } catch (err) { - toast.error(`Failed to load headscale: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Failed to load headscale: ${err instanceof Error ? err.message : String(err)}`, + ); setHeadscale(null); } finally { setLoadingHeadscale(false); @@ -757,7 +830,11 @@ export function InfrastructureDashboard() { nodeHostname: node.hostname, status: g.state, liveHealth: - g.state === "running" ? "healthy" : g.state === "exited" ? "stopped" : "degraded", + g.state === "running" + ? "healthy" + : g.state === "exited" + ? "stopped" + : "degraded", liveHealthSeverity: g.state === "running" ? "info" : "warning", liveHealthReason: g.status || g.state, runtimeState: g.state, @@ -826,7 +903,9 @@ export function InfrastructureDashboard() { if (containerStatusFilter !== "all") { rows = rows.filter( - (r) => r.status === containerStatusFilter || r.liveHealth === containerStatusFilter, + (r) => + r.status === containerStatusFilter || + r.liveHealth === containerStatusFilter, ); } if (containerNodeFilter !== "all") { @@ -907,7 +986,10 @@ export function InfrastructureDashboard() { // --------------------------------------------------------------------------- const performContainerAction = useCallback( - async (row: ContainerRow, action: "restart" | "stop" | "start" | "pull-image") => { + async ( + row: ContainerRow, + action: "restart" | "stop" | "start" | "pull-image", + ) => { if (row.nodeId === "unassigned") { toast.error("Cannot perform actions on unassigned containers"); return; @@ -915,15 +997,18 @@ export function InfrastructureDashboard() { const loadingKey = `${row.key}-${action}`; setActionLoading((prev) => ({ ...prev, [loadingKey]: true })); try { - const res = await fetch("/api/v1/admin/infrastructure/containers/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - action, - nodeId: row.nodeId, - containerName: row.containerName, - }), - }); + const res = await fetch( + "/api/v1/admin/infrastructure/containers/actions", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action, + nodeId: row.nodeId, + containerName: row.containerName, + }), + }, + ); const json = await res.json(); if (!json.success) throw new Error(json.error); toast.success(`${action} successful: ${row.containerName}`); @@ -937,7 +1022,9 @@ export function InfrastructureDashboard() { loadInfraSnapshot(); }, 2000); } catch (err) { - toast.error(`${action} failed: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `${action} failed: ${err instanceof Error ? err.message : String(err)}`, + ); } finally { setActionLoading((prev) => ({ ...prev, [loadingKey]: false })); } @@ -945,35 +1032,43 @@ export function InfrastructureDashboard() { [loadInfraSnapshot], ); - const viewContainerLogs = useCallback(async (row: ContainerRow, lines = 200) => { - if (row.nodeId === "unassigned") { - toast.error("Cannot fetch logs for unassigned containers"); - return; - } - setLogsTarget(row); - setLogsContent(""); - setLogsOpen(true); - setLogsLoading(true); - try { - const res = await fetch("/api/v1/admin/infrastructure/containers/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - action: "logs", - nodeId: row.nodeId, - containerName: row.containerName, - lines, - }), - }); - const json = await res.json(); - if (!json.success) throw new Error(json.error); - setLogsContent(json.data?.logs ?? "(no output)"); - } catch (err) { - setLogsContent(`Error: ${err instanceof Error ? err.message : String(err)}`); - } finally { - setLogsLoading(false); - } - }, []); + const viewContainerLogs = useCallback( + async (row: ContainerRow, lines = 200) => { + if (row.nodeId === "unassigned") { + toast.error("Cannot fetch logs for unassigned containers"); + return; + } + setLogsTarget(row); + setLogsContent(""); + setLogsOpen(true); + setLogsLoading(true); + try { + const res = await fetch( + "/api/v1/admin/infrastructure/containers/actions", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "logs", + nodeId: row.nodeId, + containerName: row.containerName, + lines, + }), + }, + ); + const json = await res.json(); + if (!json.success) throw new Error(json.error); + setLogsContent(json.data?.logs ?? "(no output)"); + } catch (err) { + setLogsContent( + `Error: ${err instanceof Error ? err.message : String(err)}`, + ); + } finally { + setLogsLoading(false); + } + }, + [], + ); const viewContainerDetails = useCallback(async (row: ContainerRow) => { if (row.nodeId === "unassigned") { @@ -986,21 +1081,26 @@ export function InfrastructureDashboard() { setDetailsOpen(true); setDetailsLoading(true); try { - const res = await fetch("/api/v1/admin/infrastructure/containers/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - action: "inspect", - nodeId: row.nodeId, - containerName: row.containerName, - }), - }); + const res = await fetch( + "/api/v1/admin/infrastructure/containers/actions", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "inspect", + nodeId: row.nodeId, + containerName: row.containerName, + }), + }, + ); const json = await res.json(); if (!json.success) throw new Error(json.error); setDetailsData(json.data?.inspect ?? null); setDetailsResources(json.data?.resourceUsage ?? null); } catch (err) { - toast.error(`Inspect failed: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Inspect failed: ${err instanceof Error ? err.message : String(err)}`, + ); setDetailsOpen(false); } finally { setDetailsLoading(false); @@ -1042,15 +1142,22 @@ export function InfrastructureDashboard() { async (node: DockerNode) => { setHealthChecking((prev) => ({ ...prev, [node.nodeId]: true })); try { - const res = await fetch(`/api/v1/admin/docker-nodes/${node.nodeId}/health-check`, { - method: "POST", - }); + const res = await fetch( + `/api/v1/admin/docker-nodes/${node.nodeId}/health-check`, + { + method: "POST", + }, + ); const json = await res.json(); if (!json.success) throw new Error(json.error); - toast.success(`Health check complete: ${node.nodeId} is ${json.data?.status ?? "checked"}`); + toast.success( + `Health check complete: ${node.nodeId} is ${json.data?.status ?? "checked"}`, + ); await loadNodes(); } catch (err) { - toast.error(`Health check failed: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Health check failed: ${err instanceof Error ? err.message : String(err)}`, + ); } finally { setHealthChecking((prev) => ({ ...prev, [node.nodeId]: false })); } @@ -1073,23 +1180,28 @@ export function InfrastructureDashboard() { if (!editNodeTarget) return; setEditNodeLoading(true); try { - const res = await fetch(`/api/v1/admin/docker-nodes/${editNodeTarget.nodeId}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - capacity: parseInt(editNodeForm.capacity, 10), - hostname: editNodeForm.hostname, - sshPort: parseInt(editNodeForm.sshPort, 10), - enabled: editNodeForm.enabled, - }), - }); + const res = await fetch( + `/api/v1/admin/docker-nodes/${editNodeTarget.nodeId}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + capacity: parseInt(editNodeForm.capacity, 10), + hostname: editNodeForm.hostname, + sshPort: parseInt(editNodeForm.sshPort, 10), + enabled: editNodeForm.enabled, + }), + }, + ); const json = await res.json(); if (!json.success) throw new Error(json.error); toast.success(`Node ${editNodeTarget.nodeId} updated`); setEditNodeOpen(false); await loadNodes(); } catch (err) { - toast.error(`Failed to update node: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Failed to update node: ${err instanceof Error ? err.message : String(err)}`, + ); } finally { setEditNodeLoading(false); } @@ -1099,16 +1211,21 @@ export function InfrastructureDashboard() { if (!deleteNodeTarget) return; setDeleteNodeLoading(true); try { - const res = await fetch(`/api/v1/admin/docker-nodes/${deleteNodeTarget.nodeId}`, { - method: "DELETE", - }); + const res = await fetch( + `/api/v1/admin/docker-nodes/${deleteNodeTarget.nodeId}`, + { + method: "DELETE", + }, + ); const json = await res.json(); if (!json.success) throw new Error(json.error); toast.success(`Node ${deleteNodeTarget.nodeId} deregistered`); setDeleteNodeTarget(null); await loadNodes(); } catch (err) { - toast.error(`Failed to delete node: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Failed to delete node: ${err instanceof Error ? err.message : String(err)}`, + ); } finally { setDeleteNodeLoading(false); } @@ -1132,10 +1249,18 @@ export function InfrastructureDashboard() { if (!json.success) throw new Error(json.error); toast.success(`Node ${addNodeForm.nodeId} registered`); setAddNodeOpen(false); - setAddNodeForm({ nodeId: "", hostname: "", sshPort: "22", capacity: "8", sshUser: "root" }); + setAddNodeForm({ + nodeId: "", + hostname: "", + sshPort: "22", + capacity: "8", + sshUser: "root", + }); await loadNodes(); } catch (err) { - toast.error(`Failed to register node: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Failed to register node: ${err instanceof Error ? err.message : String(err)}`, + ); } finally { setAddNodeLoading(false); } @@ -1146,12 +1271,16 @@ export function InfrastructureDashboard() { setAuditResult(null); setAuditOpen(true); try { - const res = await fetch("/api/v1/admin/docker-containers/audit", { method: "POST" }); + const res = await fetch("/api/v1/admin/docker-containers/audit", { + method: "POST", + }); const json = await res.json(); if (!json.success) throw new Error(json.error); setAuditResult(json.data); } catch (err) { - toast.error(`Audit failed: ${err instanceof Error ? err.message : String(err)}`); + toast.error( + `Audit failed: ${err instanceof Error ? err.message : String(err)}`, + ); setAuditOpen(false); } finally { setAuditLoading(false); @@ -1170,7 +1299,8 @@ export function InfrastructureDashboard() { const nodesUnknown = nodes.filter((n) => n.status === "unknown").length; const totalCapacity = nodes.reduce((s, n) => s + n.capacity, 0); const totalAllocated = nodes.reduce((s, n) => s + n.allocatedCount, 0); - const utilizationPct = totalCapacity > 0 ? Math.round((totalAllocated / totalCapacity) * 100) : 0; + const utilizationPct = + totalCapacity > 0 ? Math.round((totalAllocated / totalCapacity) * 100) : 0; // --------------------------------------------------------------------------- // Refresh all @@ -1224,10 +1354,14 @@ export function InfrastructureDashboard() {

    {nodesOnline} online {nodesOffline > 0 && ( - {nodesOffline} offline + + {nodesOffline} offline + )} {nodesUnknown > 0 && ( - {nodesUnknown} unchecked + + {nodesUnknown} unchecked + )}

    @@ -1254,11 +1388,14 @@ export function InfrastructureDashboard() { {containerRows.filter((r) => r.type === "ghost").length > 0 && ( - {containerRows.filter((r) => r.type === "ghost").length} untracked + {containerRows.filter((r) => r.type === "ghost").length}{" "} + untracked )} {summary?.errorContainers ? ( - {summary.errorContainers} error + + {summary.errorContainers} error + ) : null}

    @@ -1295,7 +1432,9 @@ export function InfrastructureDashboard() { {/* Headscale card */} - Mesh (Headscale) + + Mesh (Headscale) + @@ -1305,10 +1444,14 @@ export function InfrastructureDashboard() { <>
    - {headscale.summary.total} + + {headscale.summary.total} +

    - {headscale.summary.online} online + + {headscale.summary.online} online + {headscale.summary.offline > 0 && ( {headscale.summary.offline} offline @@ -1320,7 +1463,9 @@ export function InfrastructureDashboard() { ) : (

    - Unavailable + + Unavailable +
    )}
    @@ -1337,7 +1482,9 @@ export function InfrastructureDashboard() { const warningCount = infraSnapshot.incidents.filter( (i) => i.severity === "warning", ).length; - const infoCount = infraSnapshot.incidents.filter((i) => i.severity === "info").length; + const infoCount = infraSnapshot.incidents.filter( + (i) => i.severity === "info", + ).length; const COLLAPSED_LIMIT = 3; const visibleIncidents = incidentsExpanded ? infraSnapshot.incidents @@ -1379,7 +1526,8 @@ export function InfrastructureDashboard() { {!incidentsExpanded && hasMore && (

    - +{infraSnapshot.incidents.length - COLLAPSED_LIMIT} more — click to expand + +{infraSnapshot.incidents.length - COLLAPSED_LIMIT} more — + click to expand

    )} {incidentsExpanded && ( @@ -1398,7 +1546,9 @@ export function InfrastructureDashboard() { > {incident.severity} - {incident.title} + + {incident.title} + — {incident.detail} @@ -1436,7 +1586,9 @@ export function InfrastructureDashboard() {
    Docker Nodes - Registered Docker execution nodes + + Registered Docker execution nodes +
    - )} - {filteredContainerRows.length} of {containerRows.length} containers + {filteredContainerRows.length} of {containerRows.length}{" "} + containers @@ -1672,407 +1845,500 @@ export function InfrastructureDashboard() { ) : (
    - {Array.from(containersByNode.entries()).map(([nodeId, rows]) => { - const nodeInfo = infraSnapshot?.nodes.find((n) => n.nodeId === nodeId); - return ( -
    - {/* Node group header */} -
    - - {nodeId} - {nodeInfo && ( - <> - - {nodeInfo.hostname}:{nodeInfo.sshPort} - - - {nodeInfo.runtime.reachable && ( + {Array.from(containersByNode.entries()).map( + ([nodeId, rows]) => { + const nodeInfo = infraSnapshot?.nodes.find( + (n) => n.nodeId === nodeId, + ); + return ( +
    + {/* Node group header */} +
    + + + {nodeId} + + {nodeInfo && ( + <> - SSH: {nodeInfo.runtime.sshLatencyMs}ms - {nodeInfo.runtime.diskUsedPercent !== null && - ` · Disk: ${nodeInfo.runtime.diskUsedPercent}%`} - {nodeInfo.runtime.memoryUsedPercent !== null && - ` · Mem: ${nodeInfo.runtime.memoryUsedPercent}%`} - {nodeInfo.runtime.loadAverage && - ` · Load: ${nodeInfo.runtime.loadAverage}`} + {nodeInfo.hostname}:{nodeInfo.sshPort} - )} - {!nodeInfo.runtime.reachable && ( - - Unreachable - - )} - - )} - - {rows.length} container{rows.length !== 1 ? "s" : ""} - -
    - - - - - - - - - - Runtime - Heartbeat - Actions - - - - {rows.map((row) => { - const isExpanded = expandedRows.has(row.key); - const isGhost = row.type === "ghost"; - return ( - <> - toggleRowExpand(row.key)} + + {nodeInfo.runtime.reachable && ( + + SSH: {nodeInfo.runtime.sshLatencyMs}ms + {nodeInfo.runtime.diskUsedPercent !== + null && + ` · Disk: ${nodeInfo.runtime.diskUsedPercent}%`} + {nodeInfo.runtime.memoryUsedPercent !== + null && + ` · Mem: ${nodeInfo.runtime.memoryUsedPercent}%`} + {nodeInfo.runtime.loadAverage && + ` · Load: ${nodeInfo.runtime.loadAverage}`} + + )} + {!nodeInfo.runtime.reachable && ( + - - {isExpanded ? ( - - ) : ( - - )} - - -
    - {row.containerName} - {isGhost && ( - - ghost - - )} -
    -
    - - {row.agentName ?? ( - - {row.sandboxId ? `${row.sandboxId.slice(0, 8)}…` : "—"} - - )} - - - - {row.errorCount > 0 && ( - - ({row.errorCount}x) - - )} - - - - - - {row.runtimeStatus ?? "—"} - - - {row.lastHeartbeatAt ? ( - 5 - ? "text-amber-600" - : "" - } - > - {formatRelativeTime(row.lastHeartbeatAt)} - - ) : ( - "—" - )} - - e.stopPropagation()} + Unreachable +
    + )} + + )} + + {rows.length} container + {rows.length !== 1 ? "s" : ""} + + + +
    + + + + + + + + Runtime + Heartbeat + + Actions + + + + + {rows.map((row) => { + const isExpanded = expandedRows.has(row.key); + const isGhost = row.type === "ghost"; + return ( + <> + toggleRowExpand(row.key)} > -
    - - - - {row.runtimeState === "running" ? ( +
    + + + {row.agentName ?? ( + + {row.sandboxId + ? `${row.sandboxId.slice(0, 8)}…` + : "—"} + + )} + + + + {row.errorCount > 0 && ( + + ({row.errorCount}x) + + )} + + + + + + {row.runtimeStatus ?? "—"} + + + {row.lastHeartbeatAt ? ( + 5 + ? "text-amber-600" + : "" + } + > + {formatRelativeTime( + row.lastHeartbeatAt, + )} + + ) : ( + "—" + )} + + e.stopPropagation()} + > +
    - ) : ( - )} - {row.bridgeUrl && ( - )} -
    -
    -
    - {/* Expanded details row */} - {isExpanded && ( - - - -
    -
    - - Docker Image - -

    - {row.dockerImage ?? "—"} -

    -
    -
    - VPN IP -

    {row.headscaleIp ?? "—"}

    -
    -
    - - Bridge Port - -

    {row.bridgePort ?? "—"}

    -
    -
    - - Web UI Port - -

    {row.webUiPort ?? "—"}

    -
    -
    - - Health Reason - -

    + performContainerAction( + row, + "stop", + ) + } + disabled={ + actionLoading[ + `${row.key}-stop` + ] || + row.nodeId === "unassigned" } > - {row.liveHealthReason} -

    -
    -
    - Error -

    - {row.errorMessage ?? "None"} -

    -
    -
    - Created -

    - {row.createdAt - ? new Date(row.createdAt).toLocaleString() - : "—"} -

    -
    -
    - - Sandbox ID - -

    - {row.sandboxId ?? "—"} -

    -
    - {/* SSH command */} -
    - - SSH Command - -
    - - ssh {row.sshUser}@{row.nodeHostname} -p{" "} - {row.sshPort} docker logs -f {row.containerName} - - -
    -
    - {/* Quick links */} -
    - {row.bridgeUrl && ( - - )} - {row.healthUrl && ( - - )} - {row.headscaleIp && row.webUiPort && ( - - )} + {actionLoading[ + `${row.key}-stop` + ] ? ( + + ) : ( + + )} + + ) : ( -
    + )} + {row.bridgeUrl && ( + + )}
    - )} - - ); - })} -
    -
    -
    - ); - })} + {/* Expanded details row */} + {isExpanded && ( + + + +
    +
    + + Docker Image + +

    + {row.dockerImage ?? "—"} +

    +
    +
    + + VPN IP + +

    + {row.headscaleIp ?? "—"} +

    +
    +
    + + Bridge Port + +

    + {row.bridgePort ?? "—"} +

    +
    +
    + + Web UI Port + +

    + {row.webUiPort ?? "—"} +

    +
    +
    + + Health Reason + +

    + {row.liveHealthReason} +

    +
    +
    + + Error + +

    + {row.errorMessage ?? "None"} +

    +
    +
    + + Created + +

    + {row.createdAt + ? new Date( + row.createdAt, + ).toLocaleString() + : "—"} +

    +
    +
    + + Sandbox ID + +

    + {row.sandboxId ?? "—"} +

    +
    + {/* SSH command */} +
    + + SSH Command + +
    + + ssh {row.sshUser}@ + {row.nodeHostname} -p{" "} + {row.sshPort} docker logs -f{" "} + {row.containerName} + + +
    +
    + {/* Quick links */} +
    + {row.bridgeUrl && ( + + )} + {row.healthUrl && ( + + )} + {row.headscaleIp && + row.webUiPort && ( + + )} + +
    +
    +
    +
    + )} + + ); + })} + + +
    + ); + }, + )} {filteredContainerRows.length === 0 && (
    @@ -2100,7 +2366,11 @@ export function InfrastructureDashboard() { VPN node connectivity via Tailscale-compatible Headscale
    - @@ -2333,7 +2628,9 @@ export function InfrastructureDashboard() { setEditNodeForm((f) => ({ ...f, hostname: e.target.value }))} + onChange={(e) => + setEditNodeForm((f) => ({ ...f, hostname: e.target.value })) + } />
    @@ -2345,7 +2642,9 @@ export function InfrastructureDashboard() { min={1} max={65535} value={editNodeForm.sshPort} - onChange={(e) => setEditNodeForm((f) => ({ ...f, sshPort: e.target.value }))} + onChange={(e) => + setEditNodeForm((f) => ({ ...f, sshPort: e.target.value })) + } />
    @@ -2355,7 +2654,9 @@ export function InfrastructureDashboard() { type="number" min={1} value={editNodeForm.capacity} - onChange={(e) => setEditNodeForm((f) => ({ ...f, capacity: e.target.value }))} + onChange={(e) => + setEditNodeForm((f) => ({ ...f, capacity: e.target.value })) + } />
    @@ -2365,7 +2666,9 @@ export function InfrastructureDashboard() { type="checkbox" className="h-4 w-4 rounded border-border" checked={editNodeForm.enabled} - onChange={(e) => setEditNodeForm((f) => ({ ...f, enabled: e.target.checked }))} + onChange={(e) => + setEditNodeForm((f) => ({ ...f, enabled: e.target.checked })) + } /> @@ -2379,7 +2682,9 @@ export function InfrastructureDashboard() { Cancel @@ -2389,16 +2694,23 @@ export function InfrastructureDashboard() { {/* -------------------------------------------------------------------- */} {/* DELETE NODE CONFIRM DIALOG */} {/* -------------------------------------------------------------------- */} - !open && setDeleteNodeTarget(null)}> + !open && setDeleteNodeTarget(null)} + > Deregister Node Are you sure you want to deregister{" "} - {deleteNodeTarget?.nodeId}? + + {deleteNodeTarget?.nodeId} + + ? {deleteNodeTarget && deleteNodeTarget.allocatedCount > 0 && ( - ⚠ This node has {deleteNodeTarget.allocatedCount} active containers. + ⚠ This node has {deleteNodeTarget.allocatedCount} active + containers. )} @@ -2411,8 +2723,14 @@ export function InfrastructureDashboard() { > Cancel - @@ -2451,7 +2769,8 @@ export function InfrastructureDashboard() { variant="outline" size="sm" onClick={() => - logsTarget && viewContainerLogs(logsTarget, parseInt(logsLines, 10) || 200) + logsTarget && + viewContainerLogs(logsTarget, parseInt(logsLines, 10) || 200) } disabled={logsLoading} > @@ -2518,13 +2837,17 @@ export function InfrastructureDashboard() { {detailsResources.cpuPercent && (

    CPU

    -

    {detailsResources.cpuPercent}

    +

    + {detailsResources.cpuPercent} +

    )} {detailsResources.memUsage && (

    Memory

    -

    {detailsResources.memUsage}

    +

    + {detailsResources.memUsage} +

    {detailsResources.memPercent && (

    {detailsResources.memPercent} used @@ -2534,20 +2857,30 @@ export function InfrastructureDashboard() { )} {detailsResources.netIO && (

    -

    Network I/O

    -

    {detailsResources.netIO}

    +

    + Network I/O +

    +

    + {detailsResources.netIO} +

    )} {detailsResources.blockIO && (
    -

    Block I/O

    -

    {detailsResources.blockIO}

    +

    + Block I/O +

    +

    + {detailsResources.blockIO} +

    )} {detailsResources.pids && (

    PIDs

    -

    {detailsResources.pids}

    +

    + {detailsResources.pids} +

    )}
    @@ -2557,12 +2890,16 @@ export function InfrastructureDashboard() { {/* Container Config */} {detailsData && (
    -

    Container Configuration

    +

    + Container Configuration +

    Image:

    - {String(detailsData.Config?.Image ?? detailsData.Image ?? "—")} + {String( + detailsData.Config?.Image ?? detailsData.Image ?? "—", + )}

    @@ -2573,12 +2910,16 @@ export function InfrastructureDashboard() { Started:

    {detailsData.State?.StartedAt - ? new Date(detailsData.State.StartedAt).toLocaleString() + ? new Date( + detailsData.State.StartedAt, + ).toLocaleString() : "—"}

    - Restart Count: + + Restart Count: +

    {String(detailsData.RestartCount ?? "—")}

    @@ -2596,12 +2937,15 @@ export function InfrastructureDashboard() { {/* Environment Variables */} {detailsData?.Config?.Env && (
    -

    Environment Variables

    +

    + Environment Variables +

                           {detailsData.Config.Env.map((env: string) => {
                             const [key] = env.split("=");
    -                        const isSensitive = /key|secret|password|token|api/i.test(key ?? "");
    +                        const isSensitive =
    +                          /key|secret|password|token|api/i.test(key ?? "");
                             return isSensitive ? `${key}=****` : env;
                           }).join("\n")}
                         
    @@ -2614,20 +2958,22 @@ export function InfrastructureDashboard() {

    Port Mappings

    - {Object.entries(detailsData.NetworkSettings.Ports).map(([port, bindings]) => ( -
    - {port} - {" → "} - {Array.isArray(bindings) && bindings.length > 0 - ? bindings - .map( - (b: { HostIp?: string; HostPort?: string }) => - `${b.HostIp || "0.0.0.0"}:${b.HostPort}`, - ) - .join(", ") - : "not mapped"} -
    - ))} + {Object.entries(detailsData.NetworkSettings.Ports).map( + ([port, bindings]) => ( +
    + {port} + {" → "} + {Array.isArray(bindings) && bindings.length > 0 + ? bindings + .map( + (b: { HostIp?: string; HostPort?: string }) => + `${b.HostIp || "0.0.0.0"}:${b.HostPort}`, + ) + .join(", ") + : "not mapped"} +
    + ), + )}
    )} @@ -2639,7 +2985,12 @@ export function InfrastructureDashboard() {
    -                    {detailsData && JSON.stringify(maskInspectForDisplay(detailsData), null, 2)}
    +                    {detailsData &&
    +                      JSON.stringify(
    +                        maskInspectForDisplay(detailsData),
    +                        null,
    +                        2,
    +                      )}
                       
    @@ -2664,8 +3015,8 @@ export function InfrastructureDashboard() { Container Audit Results - Ghost containers (running on node but not in DB) and orphan records (in DB but not on - node). + Ghost containers (running on node but not in DB) and orphan + records (in DB but not on node). {auditLoading ? ( @@ -2675,24 +3026,33 @@ export function InfrastructureDashboard() { ) : auditResult ? (

    - Checked {auditResult.nodesChecked} node{auditResult.nodesChecked !== 1 ? "s" : ""}. + Checked {auditResult.nodesChecked} node + {auditResult.nodesChecked !== 1 ? "s" : ""}. {auditResult.message && ` ${auditResult.message}`}

    {auditResult.auditedAt && ( - · {formatRelativeTime(auditResult.auditedAt)} + + · {formatRelativeTime(auditResult.auditedAt)} + )}

    Ghost Containers ( - {auditResult.ghostContainers.reduce((s, n) => s + n.names.length, 0)}) + {auditResult.ghostContainers.reduce( + (s, n) => s + n.names.length, + 0, + )} + ) — running on node but not tracked in DB

    {auditResult.ghostContainers.length === 0 ? ( -

    None found ✓

    +

    + None found ✓ +

    ) : (
    {auditResult.ghostContainers.map((g) => ( @@ -2704,7 +3064,11 @@ export function InfrastructureDashboard() { Node: {g.nodeId} ({g.hostname})

    {g.names.map((name) => ( - + {name} ))} @@ -2723,7 +3087,9 @@ export function InfrastructureDashboard() { {auditResult.orphanRecords.length === 0 ? ( -

    None found ✓

    +

    + None found ✓ +

    ) : (
    {auditResult.orphanRecords.map((r) => ( diff --git a/packages/ui/src/components/admin/redemptions-client.tsx b/packages/ui/src/components/admin/redemptions-client.tsx index 9b3eb9954..df6e43753 100644 --- a/packages/ui/src/components/admin/redemptions-client.tsx +++ b/packages/ui/src/components/admin/redemptions-client.tsx @@ -118,7 +118,8 @@ export function AdminRedemptionsClient() { const [searchQuery, setSearchQuery] = useState(""); // Action dialogs - const [selectedRedemption, setSelectedRedemption] = useState(null); + const [selectedRedemption, setSelectedRedemption] = + useState(null); const [showDetailsDialog, setShowDetailsDialog] = useState(false); const [showApproveDialog, setShowApproveDialog] = useState(false); const [showRejectDialog, setShowRejectDialog] = useState(false); @@ -301,11 +302,15 @@ export function AdminRedemptionsClient() {

    Queue Stats

    -

    {stats?.pending || 0}

    +

    + {stats?.pending || 0} +

    Pending

    -

    {stats?.processing || 0}

    +

    + {stats?.processing || 0} +

    Processing

    @@ -335,7 +340,11 @@ export function AdminRedemptionsClient() { {STATUS_OPTIONS.map((opt) => ( - + {opt.label} ))} @@ -420,13 +429,17 @@ export function AdminRedemptionsClient() {
    -

    {formatCurrency(r.usd_value)}

    +

    + {formatCurrency(r.usd_value)} +

    {parseFloat(r.eliza_amount).toFixed(2)} elizaOS

    - {r.network} + + {r.network} + - + {r.status} @@ -510,7 +527,9 @@ export function AdminRedemptionsClient() {

    ID

    -

    {selectedRedemption.id}

    +

    + {selectedRedemption.id} +

    Status

    @@ -526,7 +545,9 @@ export function AdminRedemptionsClient() {

    Network

    -

    {selectedRedemption.network}

    +

    + {selectedRedemption.network} +

    USD Value

    @@ -548,18 +569,25 @@ export function AdminRedemptionsClient() {

    Created

    -

    {formatDate(selectedRedemption.created_at)}

    +

    + {formatDate(selectedRedemption.created_at)} +

    Payout Address

    -

    {selectedRedemption.payout_address}

    +

    + {selectedRedemption.payout_address} +

    {selectedRedemption.tx_hash && ( )} {selectedRedemption.rejection_reason && (

    Rejection Reason

    -

    {selectedRedemption.rejection_reason}

    +

    + {selectedRedemption.rejection_reason} +

    )}
    @@ -595,16 +627,22 @@ export function AdminRedemptionsClient() { - Approve Redemption? + + Approve Redemption? + This will approve the redemption of{" "} - {selectedRedemption && formatCurrency(selectedRedemption.usd_value)} + {selectedRedemption && + formatCurrency(selectedRedemption.usd_value)} {" "} - ({selectedRedemption && parseFloat(selectedRedemption.eliza_amount).toFixed(2)}{" "} + ( + {selectedRedemption && + parseFloat(selectedRedemption.eliza_amount).toFixed(2)}{" "} elizaOS) to{" "} - {selectedRedemption && truncateAddress(selectedRedemption.payout_address)} + {selectedRedemption && + truncateAddress(selectedRedemption.payout_address)} {" "} on {selectedRedemption?.network}.
    @@ -613,7 +651,9 @@ export function AdminRedemptionsClient() {
    - Cancel + + Cancel + (null); + const [affiliateData, setAffiliateData] = useState( + null, + ); const [loading, setLoading] = useState(true); const [markupPercent, setMarkupPercent] = useState("20.00"); const [isSaving, setIsSaving] = useState(false); const [copied, setCopied] = useState(false); const [referralCopied, setReferralCopied] = useState(false); - const affiliateCopyTimerRef = useRef | null>(null); - const referralCopyTimerRef = useRef | null>(null); + const affiliateCopyTimerRef = useRef | null>( + null, + ); + const referralCopyTimerRef = useRef | null>( + null, + ); const { referralMe, loadingReferral, @@ -97,7 +110,10 @@ export function AffiliatesPageClient() { if (affiliateCopyTimerRef.current) { clearTimeout(affiliateCopyTimerRef.current); } - affiliateCopyTimerRef.current = setTimeout(() => setCopied(false), COPY_FEEDBACK_DURATION_MS); + affiliateCopyTimerRef.current = setTimeout( + () => setCopied(false), + COPY_FEEDBACK_DURATION_MS, + ); } else { toast.error("Could not copy to clipboard"); } @@ -144,7 +160,8 @@ export function AffiliatesPageClient() { ); } - const pageOrigin = typeof window !== "undefined" ? window.location.origin : getAppUrl(); + const pageOrigin = + typeof window !== "undefined" ? window.location.origin : getAppUrl(); return (
    @@ -153,18 +170,25 @@ export function AffiliatesPageClient() {
    -

    Affiliate Program

    +

    + Affiliate Program +

    - Share your customized affiliate link with your users and partners to earn a percentage - of their marked-up top-ups and MCP usage. + Share your customized affiliate link with your users and partners + to earn a percentage of their marked-up top-ups and MCP usage.

    - When a user signs up using your link, you get a direct cut (your markup percentage) of - their activity forever. You can track this revenue in your - + When a user signs up using your link, you get a direct cut (your + markup percentage) of their activity forever. You can track this + revenue in your + Earnings - dashboard, which can be withdrawn to any EVM or Solana wallet as $ELIZA tokens. + dashboard, which can be withdrawn to any EVM or Solana wallet as + $ELIZA tokens.

    @@ -173,14 +197,19 @@ export function AffiliatesPageClient() { {/* Referral invite: uses GET /api/v1/referrals (parallel to affiliate fetch, own loading state). WHY separate from affiliate card: Different URL (?ref= vs ?affiliate=), economics, and copy. WHY cyan accent: Visually distinct from orange affiliate branding so users don’t merge the two mentally. */} - +
    -

    Invite friends

    +

    + Invite friends +

    - Share your invite link—you both earn bonus credits when they sign up, and you earn a - share of their purchases on Eliza Cloud. + Share your invite link—you both earn bonus credits when they sign + up, and you earn a share of their purchases on Eliza Cloud.

    @@ -189,7 +218,9 @@ export function AffiliatesPageClient() { ) : referralFetchFailed || !referralMe ? (
    -

    Could not load your invite link.

    +

    + Could not load your invite link. +

    @@ -327,7 +340,9 @@ export function AgentCard({ )}
    -

    {bioText || "No description"}

    +

    + {bioText || "No description"} +

    {/* Status badges */} @@ -384,11 +399,17 @@ export function AgentCard({ Edit - + Duplicate - + Export JSON @@ -411,7 +432,10 @@ export function AgentCard({ /> {isPublic && ( - + Share @@ -427,7 +451,10 @@ export function AgentCard({ ) : ( <> - + Fork Agent @@ -455,8 +482,8 @@ export function AgentCard({ Delete Agent Are you sure you want to delete{" "} - {agent.name}? This action cannot - be undone. + {agent.name} + ? This action cannot be undone. @@ -482,7 +509,8 @@ export function AgentCard({ className={cn("block h-full", !showDeleteConfirm && "cursor-pointer")} onClick={handleCardClick} onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") handleCardClick(e as unknown as React.MouseEvent); + if (e.key === "Enter" || e.key === " ") + handleCardClick(e as unknown as React.MouseEvent); }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -521,10 +549,19 @@ export function AgentCard({ )} {showDeploymentStatus && isDeployed && ( - + )} {showDeploymentStatus && isStopped && ( - + )}
    @@ -558,11 +595,17 @@ export function AgentCard({ Edit - + Duplicate - + Export JSON @@ -585,7 +628,10 @@ export function AgentCard({ /> {isPublic && ( - + Share @@ -601,7 +647,10 @@ export function AgentCard({ ) : ( <> - + Fork Agent @@ -636,8 +685,8 @@ export function AgentCard({ Delete Agent Are you sure you want to delete{" "} - {agent.name}? This action cannot - be undone. + {agent.name}? + This action cannot be undone. diff --git a/packages/ui/src/components/ai-elements/actions.tsx b/packages/ui/src/components/ai-elements/actions.tsx index 3971d4409..7f6a3ebb0 100644 --- a/packages/ui/src/components/ai-elements/actions.tsx +++ b/packages/ui/src/components/ai-elements/actions.tsx @@ -8,7 +8,12 @@ import type { ComponentProps } from "react"; import { cn } from "@/lib/utils"; import { Button } from "../button"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../tooltip"; export type ActionsProps = ComponentProps<"div">; @@ -34,7 +39,10 @@ export const Action = ({ }: ActionProps) => { const button = ( )} @@ -129,7 +137,10 @@ export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => { export type ContextContentProps = ComponentProps; -export const ContextContent = ({ className, ...props }: ContextContentProps) => ( +export const ContextContent = ({ + className, + ...props +}: ContextContentProps) => ( export type ContextContentHeader = ComponentProps<"div">; -export const ContextContentHeader = ({ children, className, ...props }: ContextContentHeader) => { +export const ContextContentHeader = ({ + children, + className, + ...props +}: ContextContentHeader) => { const { usedTokens, maxTokens } = useContextValue(); const usedPercent = usedTokens / maxTokens; const displayPct = new Intl.NumberFormat("en-US", { @@ -173,7 +188,11 @@ export const ContextContentHeader = ({ children, className, ...props }: ContextC export type ContextContentBody = ComponentProps<"div">; -export const ContextContentBody = ({ children, className, ...props }: ContextContentBody) => ( +export const ContextContentBody = ({ + children, + className, + ...props +}: ContextContentBody) => (
    {children}
    @@ -181,7 +200,11 @@ export const ContextContentBody = ({ children, className, ...props }: ContextCon export type ContextContentFooter = ComponentProps<"div">; -export const ContextContentFooter = ({ children, className, ...props }: ContextContentFooter) => { +export const ContextContentFooter = ({ + children, + className, + ...props +}: ContextContentFooter) => { const { modelId, usage } = useContextValue(); const costUSD = modelId ? estimateCost({ @@ -217,7 +240,11 @@ export const ContextContentFooter = ({ children, className, ...props }: ContextC export type ContextInputUsageProps = ComponentProps<"div">; -export const ContextInputUsage = ({ className, children, ...props }: ContextInputUsageProps) => { +export const ContextInputUsage = ({ + className, + children, + ...props +}: ContextInputUsageProps) => { const { usage, modelId } = useContextValue(); const inputTokens = usage?.inputTokens ?? 0; @@ -241,7 +268,10 @@ export const ContextInputUsage = ({ className, children, ...props }: ContextInpu }).format(inputCost ?? 0); return ( -
    +
    Input
    @@ -250,7 +280,11 @@ export const ContextInputUsage = ({ className, children, ...props }: ContextInpu export type ContextOutputUsageProps = ComponentProps<"div">; -export const ContextOutputUsage = ({ className, children, ...props }: ContextOutputUsageProps) => { +export const ContextOutputUsage = ({ + className, + children, + ...props +}: ContextOutputUsageProps) => { const { usage, modelId } = useContextValue(); const outputTokens = usage?.outputTokens ?? 0; @@ -274,7 +308,10 @@ export const ContextOutputUsage = ({ className, children, ...props }: ContextOut }).format(outputCost ?? 0); return ( -
    +
    Output
    @@ -311,7 +348,10 @@ export const ContextReasoningUsage = ({ }).format(reasoningCost ?? 0); return ( -
    +
    Reasoning
    @@ -320,7 +360,11 @@ export const ContextReasoningUsage = ({ export type ContextCacheUsageProps = ComponentProps<"div">; -export const ContextCacheUsage = ({ className, children, ...props }: ContextCacheUsageProps) => { +export const ContextCacheUsage = ({ + className, + children, + ...props +}: ContextCacheUsageProps) => { const { usage, modelId } = useContextValue(); const cacheTokens = usage?.cachedInputTokens ?? 0; @@ -344,20 +388,31 @@ export const ContextCacheUsage = ({ className, children, ...props }: ContextCach }).format(cacheCost ?? 0); return ( -
    +
    Cache
    ); }; -const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: string }) => ( +const TokensWithCost = ({ + tokens, + costText, +}: { + tokens?: number; + costText?: string; +}) => ( {tokens === undefined ? "—" : new Intl.NumberFormat("en-US", { notation: "compact", }).format(tokens)} - {costText ? • {costText} : null} + {costText ? ( + • {costText} + ) : null} ); diff --git a/packages/ui/src/components/ai-elements/conversation.tsx b/packages/ui/src/components/ai-elements/conversation.tsx index 07415b9be..17f043389 100644 --- a/packages/ui/src/components/ai-elements/conversation.tsx +++ b/packages/ui/src/components/ai-elements/conversation.tsx @@ -24,9 +24,14 @@ export const Conversation = ({ className, ...props }: ConversationProps) => ( /> ); -export type ConversationContentProps = ComponentProps; +export type ConversationContentProps = ComponentProps< + typeof StickToBottom.Content +>; -export const ConversationContent = ({ className, ...props }: ConversationContentProps) => ( +export const ConversationContent = ({ + className, + ...props +}: ConversationContentProps) => ( ); @@ -56,7 +61,9 @@ export const ConversationEmptyState = ({ {icon &&
    {icon}
    }

    {title}

    - {description &&

    {description}

    } + {description && ( +

    {description}

    + )}
    )} @@ -78,7 +85,10 @@ export const ConversationScrollButton = ({ return ( !isAtBottom && (
    @@ -235,7 +270,9 @@ export const WebPreviewConsole = ({ )} key={`${log.timestamp.getTime()}-${index}`} > - {log.timestamp.toLocaleTimeString()}{" "} + + {log.timestamp.toLocaleTimeString()} + {" "} {log.message}
    )) diff --git a/packages/ui/src/components/alert-dialog.stories.tsx b/packages/ui/src/components/alert-dialog.stories.tsx index 10b0e0038..c89328818 100644 --- a/packages/ui/src/components/alert-dialog.stories.tsx +++ b/packages/ui/src/components/alert-dialog.stories.tsx @@ -43,7 +43,9 @@ export const Default: StoryObj = { Cancel - Delete + + Delete + @@ -71,13 +73,15 @@ export const Destructive: StoryObj = { Remove Team Member - Are you sure you want to remove this member from your organization? They will lose - access immediately. + Are you sure you want to remove this member from your organization? + They will lose access immediately. Cancel - Remove + + Remove + diff --git a/packages/ui/src/components/alert-dialog.tsx b/packages/ui/src/components/alert-dialog.tsx index b9485b1d0..a7daeed61 100644 --- a/packages/ui/src/components/alert-dialog.tsx +++ b/packages/ui/src/components/alert-dialog.tsx @@ -9,18 +9,26 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import * as React from "react"; import { cn } from "../lib/utils"; -function AlertDialog({ ...props }: React.ComponentProps) { +function AlertDialog({ + ...props +}: React.ComponentProps) { return ; } function AlertDialogTrigger({ ...props }: React.ComponentProps) { - return ; + return ( + + ); } -function AlertDialogPortal({ ...props }: React.ComponentProps) { - return ; +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); } function AlertDialogOverlay({ @@ -58,7 +66,10 @@ function AlertDialogContent({ ); } -function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { return (
    ) ); } -function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { return (
    ); @@ -108,7 +125,12 @@ function AlertDialogAction({ className, ...props }: React.ComponentProps) { - return ; + return ( + + ); } function AlertDialogCancel({ diff --git a/packages/ui/src/components/alert.stories.tsx b/packages/ui/src/components/alert.stories.tsx index 8bf10cbdb..77fa07e4b 100644 --- a/packages/ui/src/components/alert.stories.tsx +++ b/packages/ui/src/components/alert.stories.tsx @@ -22,7 +22,9 @@ export const Default: Story = { Note - Your API key was successfully created. Store it securely. + + Your API key was successfully created. Store it securely. + ), }; @@ -32,7 +34,9 @@ export const Destructive: Story = { Error - Your session has expired. Please log in again. + + Your session has expired. Please log in again. + ), }; diff --git a/packages/ui/src/components/alert.tsx b/packages/ui/src/components/alert.tsx index d8114216b..07ba3eb6c 100644 --- a/packages/ui/src/components/alert.tsx +++ b/packages/ui/src/components/alert.tsx @@ -43,13 +43,19 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { return (
    ); } -function AlertDescription({ className, ...props }: React.ComponentProps<"div">) { +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { return (
    ; +export type ProjectionsChartProps = ComponentProps< + typeof ProjectionsChartComponent +>; export const ProjectionsChartLazy = (props: ProjectionsChartProps) => { return ; diff --git a/packages/ui/src/components/analytics/analytics-page-client.tsx b/packages/ui/src/components/analytics/analytics-page-client.tsx index 8709fb749..7b96e73ea 100644 --- a/packages/ui/src/components/analytics/analytics-page-client.tsx +++ b/packages/ui/src/components/analytics/analytics-page-client.tsx @@ -20,8 +20,18 @@ import { useSetPageHeader, } from "@elizaos/cloud-ui"; import { format } from "date-fns"; -import { Activity, BarChart3, CalendarRange, Coins, ShieldCheck, TrendingUp } from "lucide-react"; -import type { EnhancedAnalyticsData, ProjectionsData } from "@/lib/actions/analytics-enhanced"; +import { + Activity, + BarChart3, + CalendarRange, + Coins, + ShieldCheck, + TrendingUp, +} from "lucide-react"; +import type { + EnhancedAnalyticsData, + ProjectionsData, +} from "@/lib/actions/analytics-enhanced"; import { CostInsightsCard } from "./cost-insights-card"; import { AnalyticsFilters } from "./filters"; import { ModelBreakdown } from "./model-breakdown"; @@ -34,7 +44,10 @@ interface AnalyticsPageClientProps { projectionsData: ProjectionsData; } -export function AnalyticsPageClient({ data, projectionsData }: AnalyticsPageClientProps) { +export function AnalyticsPageClient({ + data, + projectionsData, +}: AnalyticsPageClientProps) { useSetPageHeader({ title: "Analytics", }); @@ -57,7 +70,8 @@ export function AnalyticsPageClient({ data, projectionsData }: AnalyticsPageClie month: "Monthly", }[data.filters.granularity] || "Custom"; - const totalTokens = data.overallStats.totalInputTokens + data.overallStats.totalOutputTokens; + const totalTokens = + data.overallStats.totalInputTokens + data.overallStats.totalOutputTokens; const averageCostPerRequest = data.overallStats.totalRequests > 0 @@ -65,7 +79,9 @@ export function AnalyticsPageClient({ data, projectionsData }: AnalyticsPageClie : 0; const averageTokensPerRequest = - data.overallStats.totalRequests > 0 ? totalTokens / data.overallStats.totalRequests : 0; + data.overallStats.totalRequests > 0 + ? totalTokens / data.overallStats.totalRequests + : 0; const formatDelta = (value: number | undefined, digits = 1) => { if (value === undefined || Number.isNaN(value)) return undefined; @@ -193,7 +209,10 @@ export function AnalyticsPageClient({ data, projectionsData }: AnalyticsPageClie

    Usage

    - +
    @@ -210,14 +229,20 @@ export function AnalyticsPageClient({ data, projectionsData }: AnalyticsPageClie defaultValue="breakdown" breakpoint="md" > - +
    - + diff --git a/packages/ui/src/components/analytics/cost-alerts.tsx b/packages/ui/src/components/analytics/cost-alerts.tsx index c0c0dbf40..7877b4c83 100644 --- a/packages/ui/src/components/analytics/cost-alerts.tsx +++ b/packages/ui/src/components/analytics/cost-alerts.tsx @@ -28,7 +28,10 @@ export function CostAlerts({ costTrending, creditBalance }: CostAlertsProps) { description: string; }> = []; - if (costTrending.daysUntilBalanceZero !== null && costTrending.daysUntilBalanceZero < 7) { + if ( + costTrending.daysUntilBalanceZero !== null && + costTrending.daysUntilBalanceZero < 7 + ) { alerts.push({ type: "error", title: "Low Balance", @@ -62,8 +65,8 @@ export function CostAlerts({ costTrending, creditBalance }: CostAlertsProps) {

    All good

    - Usage is tracking within healthy thresholds. You're trending below the projected - monthly spend. + Usage is tracking within healthy thresholds. You're trending + below the projected monthly spend.

    @@ -99,7 +102,9 @@ export function CostAlerts({ costTrending, creditBalance }: CostAlertsProps) { {iconMap[alert.type]}

    {alert.title}

    -

    {alert.description}

    +

    + {alert.description} +

    diff --git a/packages/ui/src/components/analytics/cost-insights-card.tsx b/packages/ui/src/components/analytics/cost-insights-card.tsx index 266489601..0f46f8191 100644 --- a/packages/ui/src/components/analytics/cost-insights-card.tsx +++ b/packages/ui/src/components/analytics/cost-insights-card.tsx @@ -23,11 +23,17 @@ const currencyFormatter = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2, }); -export function CostInsightsCard({ costTrending, creditBalance }: CostInsightsCardProps) { +export function CostInsightsCard({ + costTrending, + creditBalance, +}: CostInsightsCardProps) { const numericBalance = Number(creditBalance); const projectedSpendPercent = numericBalance > 0 - ? Math.min(100, (costTrending.projectedMonthlyBurn / numericBalance) * 100) + ? Math.min( + 100, + (costTrending.projectedMonthlyBurn / numericBalance) * 100, + ) : 0; const runwayLabel = @@ -57,7 +63,9 @@ export function CostInsightsCard({ costTrending, creditBalance }: CostInsightsCa
    -

    Daily burn

    +

    + Daily burn +

    {currencyFormatter.format(costTrending.currentDailyBurn)}

    @@ -66,18 +74,24 @@ export function CostInsightsCard({ costTrending, creditBalance }: CostInsightsCa
    Monthly projection - {currencyFormatter.format(costTrending.projectedMonthlyBurn)} + + {currencyFormatter.format(costTrending.projectedMonthlyBurn)} +
    -

    Runway

    +

    + Runway +

    {runwayLabel}

    -

    Balance

    +

    + Balance +

    {currencyFormatter.format(creditBalance)}

    diff --git a/packages/ui/src/components/analytics/export-button.tsx b/packages/ui/src/components/analytics/export-button.tsx index 0ea471901..cbb22bd7f 100644 --- a/packages/ui/src/components/analytics/export-button.tsx +++ b/packages/ui/src/components/analytics/export-button.tsx @@ -117,7 +117,11 @@ export function ExportButton({ } return ( - diff --git a/packages/ui/src/components/analytics/filters.tsx b/packages/ui/src/components/analytics/filters.tsx index 332589c1b..97ad13a9a 100644 --- a/packages/ui/src/components/analytics/filters.tsx +++ b/packages/ui/src/components/analytics/filters.tsx @@ -33,10 +33,13 @@ export function AnalyticsFilters() { return undefined; } - const diffInDays = Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + const diffInDays = Math.round( + (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24), + ); const now = new Date(); - const isAlignedWithNow = Math.abs(end.getTime() - now.getTime()) < 1000 * 60 * 60; + const isAlignedWithNow = + Math.abs(end.getTime() - now.getTime()) < 1000 * 60 * 60; if (diffInDays === 7 && isAlignedWithNow) return "7d"; if (diffInDays === 30 && isAlignedWithNow) return "30d"; @@ -63,7 +66,9 @@ export function AnalyticsFilters() {
    -

    Aggregation

    +

    + Aggregation +