diff --git a/packages/app/server/package.json b/packages/app/server/package.json index 63df492d1..38fa448f6 100644 --- a/packages/app/server/package.json +++ b/packages/app/server/package.json @@ -42,6 +42,7 @@ "author": "", "license": "ISC", "dependencies": { + "neverthrow": "^7.2.0", "@coinbase/cdp-sdk": "^1.34.0", "@e2b/code-interpreter": "^2.0.1", "@google-cloud/storage": "^7.17.1", diff --git a/packages/app/server/src/errors/results.ts b/packages/app/server/src/errors/results.ts new file mode 100644 index 000000000..8f2de684f --- /dev/null +++ b/packages/app/server/src/errors/results.ts @@ -0,0 +1,33 @@ +/** + * Typed error variants for neverthrow Result types. + * Each error variant carries structured context for downstream handling. + */ + +export type DbError = + | { type: 'DB_NOT_FOUND'; entity: string; id?: string } + | { type: 'DB_VALIDATION_FAILED'; message: string } + | { type: 'DB_TRANSACTION_FAILED'; cause: unknown } + | { type: 'DB_QUERY_FAILED'; cause: unknown }; + +export type AuthError = + | { type: 'AUTH_INVALID_API_KEY' } + | { type: 'AUTH_EXPIRED_JWT' } + | { type: 'AUTH_JWT_VERIFICATION_FAILED'; cause: unknown } + | { type: 'AUTH_MISSING_FIELDS'; fields: string[] } + | { type: 'AUTH_MISSING_CREDENTIALS' }; + +export type SettleError = + | { type: 'SETTLE_SMART_ACCOUNT_FAILED'; cause: unknown } + | { type: 'SETTLE_INVALID_PAYMENT_HEADER'; cause: unknown } + | { type: 'SETTLE_INVALID_PAYLOAD'; cause: unknown } + | { type: 'SETTLE_INSUFFICIENT_PAYMENT'; required: bigint; provided: bigint } + | { type: 'SETTLE_FACILITATOR_FAILED' }; + +export type RefundError = + | { type: 'REFUND_TRANSFER_FAILED'; cause: unknown }; + +export type ResourceError = + | { type: 'RESOURCE_EXECUTION_FAILED'; cause: unknown } + | { type: 'RESOURCE_AUTHENTICATION_FAILED'; cause: unknown } + | { type: 'RESOURCE_PAYMENT_FAILED'; cause: SettleError } + | { type: 'RESOURCE_TRANSACTION_FAILED'; cause: unknown }; diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 131d35d90..002e5a7b0 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -1,7 +1,7 @@ import { TransactionEscrowMiddleware } from 'middleware/transaction-escrow-middleware'; import { modelRequestService } from 'services/ModelRequestService'; import { ApiKeyHandlerInput, X402HandlerInput } from 'types'; -import { calculateRefundAmount } from 'utils'; +import { calculateRefundAmount, buildX402Response } from 'utils'; import { checkBalance } from 'services/BalanceCheckService'; import { prisma } from 'server'; import { makeProxyPassthroughRequest } from 'services/ProxyPassthroughService'; @@ -25,7 +25,7 @@ export async function handleX402Request({ return await makeProxyPassthroughRequest(req, res, provider, headers); } - const settlePromise = settle(req, res, headers, maxCost); + const settlePromise = settle(req, headers, maxCost); const modelResultPromise = modelRequestService .executeModelRequest(req, res, headers, provider, isStream) @@ -37,13 +37,16 @@ export async function handleX402Request({ modelResultPromise, ]); + const settleOk = settleResult.isOk(); + // Case 1: Settle failed and model failed - if (!settleResult && !modelResult.success) { + if (!settleOk && !modelResult.success) { + buildX402Response(req, res, maxCost); return; } // Case 2: Settle failed but model succeeded - if (!settleResult && modelResult.success) { + if (!settleOk && modelResult.success) { const { data } = modelResult; logger.error('Settle failed but model request succeeded', { provider: provider.getType(), @@ -62,16 +65,19 @@ export async function handleX402Request({ return; } - // At this point, settleResult is guaranteed to exist - if (!settleResult) { + // At this point, settleResult is guaranteed to be ok + if (settleResult.isErr()) { + buildX402Response(req, res, maxCost); return; } - const { payload, paymentAmountDecimal } = settleResult; + const { payload, paymentAmountDecimal } = settleResult.value; // Case 3: Settle succeeded but model failed if (!modelResult.success) { - await refund(paymentAmountDecimal, payload); + refund(paymentAmountDecimal, payload).mapErr(refundErr => { + logger.error('Failed to refund', refundErr); + }); return; } diff --git a/packages/app/server/src/handlers/refund.ts b/packages/app/server/src/handlers/refund.ts index 9279303e4..8de46b652 100644 --- a/packages/app/server/src/handlers/refund.ts +++ b/packages/app/server/src/handlers/refund.ts @@ -2,17 +2,18 @@ import { decimalToUsdcBigInt } from 'utils'; import { transfer } from 'transferWithAuth'; import { ExactEvmPayload } from 'services/facilitator/x402-types'; import { Decimal } from '@prisma/client/runtime/library'; -import logger from 'logger'; +import { ResultAsync } from 'neverthrow'; +import type { RefundError } from '../errors/results'; -export async function refund( +export function refund( paymentAmountDecimal: Decimal, payload: ExactEvmPayload -) { - try { - const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); - const authPayload = payload.authorization; - await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); - } catch (error) { - logger.error('Failed to refund', error); - } +): ResultAsync { + const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); + const authPayload = payload.authorization; + + return ResultAsync.fromPromise( + transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt), + (cause): RefundError => ({ type: 'REFUND_TRANSFER_FAILED', cause }) + ).map(() => undefined); } diff --git a/packages/app/server/src/handlers/settle.ts b/packages/app/server/src/handlers/settle.ts index 935797c3a..360433a69 100644 --- a/packages/app/server/src/handlers/settle.ts +++ b/packages/app/server/src/handlers/settle.ts @@ -1,7 +1,6 @@ import { usdcBigIntToDecimal, decimalToUsdcBigInt, - buildX402Response, getSmartAccount, validateXPaymentHeader, } from 'utils'; @@ -10,91 +9,107 @@ import { FacilitatorClient } from 'services/facilitator/facilitatorService'; import { ExactEvmPayload, ExactEvmPayloadSchema, - PaymentPayload, PaymentRequirementsSchema, SettleRequestSchema, Network, } from 'services/facilitator/x402-types'; import { Decimal } from '@prisma/client/runtime/library'; import logger from 'logger'; -import { Request, Response } from 'express'; +import { Request } from 'express'; +import { ResultAsync, fromThrowable, err, ok } from 'neverthrow'; import { env } from '../env'; +import type { SettleError } from '../errors/results'; -export async function settle( +export type SettleSuccess = { + payload: ExactEvmPayload; + paymentAmountDecimal: Decimal; +}; + +const parseXPaymentHeader = fromThrowable( + validateXPaymentHeader, + (cause): SettleError => ({ type: 'SETTLE_INVALID_PAYMENT_HEADER', cause }) +); + +export function settle( req: Request, - res: Response, headers: Record, maxCost: Decimal -): Promise< - { payload: ExactEvmPayload; paymentAmountDecimal: Decimal } | undefined -> { +): ResultAsync { const network = env.NETWORK as Network; - let recipient: string; - try { - recipient = (await getSmartAccount()).smartAccount.address; - } catch (error) { - buildX402Response(req, res, maxCost); - return undefined; - } + return ResultAsync.fromPromise( + getSmartAccount().then(({ smartAccount }) => smartAccount.address), + (cause): SettleError => ({ type: 'SETTLE_SMART_ACCOUNT_FAILED', cause }) + ) + .andThen(recipient => + parseXPaymentHeader(headers, req).map(xPaymentData => ({ + recipient, + xPaymentData, + })) + ) + .andThen(({ recipient, xPaymentData }) => { + const payloadResult = ExactEvmPayloadSchema.safeParse(xPaymentData.payload); + if (!payloadResult.success) { + logger.error('Invalid ExactEvmPayload in settle', { + error: payloadResult.error, + payload: xPaymentData.payload, + }); + return err< + { recipient: string; xPaymentData: typeof xPaymentData; payload: ExactEvmPayload; paymentAmountDecimal: Decimal }, + SettleError + >({ type: 'SETTLE_INVALID_PAYLOAD', cause: payloadResult.error }); + } - let xPaymentData: PaymentPayload; - try { - xPaymentData = validateXPaymentHeader(headers, req); - } catch (error) { - buildX402Response(req, res, maxCost); - return undefined; - } - - const payloadResult = ExactEvmPayloadSchema.safeParse(xPaymentData.payload); - if (!payloadResult.success) { - logger.error('Invalid ExactEvmPayload in settle', { - error: payloadResult.error, - payload: xPaymentData.payload, - }); - buildX402Response(req, res, maxCost); - return undefined; - } - const payload = payloadResult.data; + const payload = payloadResult.data; + const paymentAmount = payload.authorization.value; + const paymentAmountDecimal = usdcBigIntToDecimal(paymentAmount); - const paymentAmount = payload.authorization.value; - const paymentAmountDecimal = usdcBigIntToDecimal(paymentAmount); + // Note(shafu, alvaro): Edge case where client sends the x402-challenge + // but the payment amount is less than what we returned in the first response + if (BigInt(paymentAmount) < decimalToUsdcBigInt(maxCost)) { + return err< + { recipient: string; xPaymentData: typeof xPaymentData; payload: ExactEvmPayload; paymentAmountDecimal: Decimal }, + SettleError + >({ + type: 'SETTLE_INSUFFICIENT_PAYMENT', + required: decimalToUsdcBigInt(maxCost), + provided: BigInt(paymentAmount), + }); + } - // Note(shafu, alvaro): Edge case where client sends the x402-challenge - // but the payment amount is less than what we returned in the first response - if (BigInt(paymentAmount) < decimalToUsdcBigInt(maxCost)) { - buildX402Response(req, res, maxCost); - return undefined; - } + return ok({ recipient, xPaymentData, payload, paymentAmountDecimal }); + }) + .andThen(({ recipient, xPaymentData, payload, paymentAmountDecimal }) => { + const facilitatorClient = new FacilitatorClient(); + const paymentRequirements = PaymentRequirementsSchema.parse({ + scheme: 'exact', + network, + maxAmountRequired: payload.authorization.value, + resource: `${req.protocol}://${req.get('host')}${req.url}`, + description: 'Echo x402', + mimeType: 'application/json', + payTo: recipient, + maxTimeoutSeconds: 60, + asset: USDC_ADDRESS, + extra: { + name: 'USD Coin', + version: '2', + }, + }); - const facilitatorClient = new FacilitatorClient(); - const paymentRequirements = PaymentRequirementsSchema.parse({ - scheme: 'exact', - network, - maxAmountRequired: paymentAmount, - resource: `${req.protocol}://${req.get('host')}${req.url}`, - description: 'Echo x402', - mimeType: 'application/json', - payTo: recipient, - maxTimeoutSeconds: 60, - asset: USDC_ADDRESS, - extra: { - name: 'USD Coin', - version: '2', - }, - }); + const settleRequest = SettleRequestSchema.parse({ + paymentPayload: xPaymentData, + paymentRequirements, + }); - const settleRequest = SettleRequestSchema.parse({ - paymentPayload: xPaymentData, - paymentRequirements, - }); - - const settleResult = await facilitatorClient.settle(settleRequest); - - if (!settleResult.success || !settleResult.transaction) { - buildX402Response(req, res, maxCost); - return undefined; - } - - return { payload, paymentAmountDecimal }; + return ResultAsync.fromPromise( + facilitatorClient.settle(settleRequest), + (): SettleError => ({ type: 'SETTLE_FACILITATOR_FAILED' }) + ).andThen(settleResult => { + if (!settleResult.success || !settleResult.transaction) { + return err({ type: 'SETTLE_FACILITATOR_FAILED' }); + } + return ok({ payload, paymentAmountDecimal }); + }); + }); } diff --git a/packages/app/server/src/resources/handler.ts b/packages/app/server/src/resources/handler.ts index 411f9c6ba..38cc58593 100644 --- a/packages/app/server/src/resources/handler.ts +++ b/packages/app/server/src/resources/handler.ts @@ -9,7 +9,9 @@ import { finalizeResource } from 'handlers/finalize'; import { refund } from 'handlers/refund'; import logger from 'logger'; import { ExactEvmPayload } from 'services/facilitator/x402-types'; -import { HttpError, PaymentRequiredError } from 'errors/http'; +import { HttpError } from 'errors/http'; +import { ResultAsync, ok, err } from 'neverthrow'; +import type { ResourceError } from '../errors/results'; type ResourceHandlerConfig = { inputSchema: ZodSchema; @@ -20,72 +22,83 @@ type ResourceHandlerConfig = { errorMessage: string; }; -async function handleApiRequest( +function handleApiRequest( parsedBody: TInput, headers: Record, config: ResourceHandlerConfig -) { +): ResultAsync { const { executeResource, calculateActualCost, createTransaction } = config; - const { echoControlService } = await authenticateRequest(headers, prisma); - - const output = await executeResource(parsedBody); - - const actualCost = calculateActualCost(parsedBody, output); - const transaction = createTransaction(parsedBody, output, actualCost); - - await echoControlService.createTransaction(transaction); + return ResultAsync.fromPromise( + authenticateRequest(headers, prisma), + (cause): ResourceError => ({ type: 'RESOURCE_AUTHENTICATION_FAILED', cause }) + ).andThen(({ echoControlService }) => + ResultAsync.fromPromise( + executeResource(parsedBody), + (cause): ResourceError => ({ type: 'RESOURCE_EXECUTION_FAILED', cause }) + ).andThen(output => { + const actualCost = calculateActualCost(parsedBody, output); + const transaction = createTransaction(parsedBody, output, actualCost); + return ResultAsync.fromPromise( + echoControlService.createTransaction(transaction), + (cause): ResourceError => ({ type: 'RESOURCE_TRANSACTION_FAILED', cause }) + ).map(() => output); + }) + ); +} - return output; +function executeResourceWithRefund( + parsedBody: TInput, + executeResource: (input: TInput) => Promise, + paymentAmountDecimal: Decimal, + payload: ExactEvmPayload +): ResultAsync { + return ResultAsync.fromPromise( + executeResource(parsedBody), + (cause): ResourceError => ({ type: 'RESOURCE_EXECUTION_FAILED', cause }) + ).mapErr(resourceErr => { + // Attempt refund on execution failure; log but don't block the error propagation + refund(paymentAmountDecimal, payload).mapErr(refundErr => { + logger.error('Failed to refund after resource execution failure', refundErr); + }); + return resourceErr; + }); } -async function handle402Request( +function handle402Request( req: Request, res: Response, parsedBody: TInput, headers: Record, safeMaxCost: Decimal, config: ResourceHandlerConfig -): Promise { +): ResultAsync { const { executeResource, calculateActualCost, createTransaction } = config; - const settleResult = await settle(req, res, headers, safeMaxCost); - if (!settleResult) { - throw new PaymentRequiredError('Payment required, settle failed'); - } - - const { payload, paymentAmountDecimal } = settleResult; - - const output = await executeResourceWithRefund( - parsedBody, - executeResource, - paymentAmountDecimal, - payload - ); - - const actualCost = calculateActualCost(parsedBody, output); - const transaction = createTransaction(parsedBody, output, actualCost); - - finalizeResource(paymentAmountDecimal, transaction, payload).catch(error => { - logger.error('Failed to finalize transaction', error); - }); - - return output; -} - -async function executeResourceWithRefund( - parsedBody: TInput, - executeResource: (input: TInput) => Promise, - paymentAmountDecimal: Decimal, - payload: ExactEvmPayload -): Promise { - try { - const output = await executeResource(parsedBody); - return output; - } catch (error) { - await refund(paymentAmountDecimal, payload); - throw error; - } + return settle(req, headers, safeMaxCost) + .mapErr((cause): ResourceError => ({ type: 'RESOURCE_PAYMENT_FAILED', cause })) + .andThen(({ payload, paymentAmountDecimal }) => + executeResourceWithRefund( + parsedBody, + executeResource, + paymentAmountDecimal, + payload + ).map(output => ({ + output, + payload, + paymentAmountDecimal, + })) + ) + .map(({ output, payload, paymentAmountDecimal }) => { + const actualCost = calculateActualCost(parsedBody, output); + const transaction = createTransaction(parsedBody, output, actualCost); + + finalizeResource(paymentAmountDecimal, transaction, payload).catch(error => { + logger.error('Failed to finalize transaction', error); + }); + + return output; + }); } async function handleResourceRequest( @@ -114,34 +127,27 @@ async function handleResourceRequest( const safeMaxCost = calculateMaxCost(parsedBody); if (isApiRequest(headers)) { - try { - const output = await handleApiRequest(parsedBody, headers, config); - return res.status(200).json(output); - } catch (error) { - logger.error('Failed to handle API request', error); - return res.status(500).json({ error: 'Internal server error' }); - } + return handleApiRequest(parsedBody, headers, config).match( + output => res.status(200).json(output), + error => { + logger.error('Failed to handle API request', error); + return res.status(500).json({ error: 'Internal server error' }); + } + ); } if (isX402Request(headers)) { - try { - const result = await handle402Request( - req, - res, - parsedBody, - headers, - safeMaxCost, - config - ); - return res.status(200).json(result); - } catch (error) { - if (error instanceof PaymentRequiredError) { + return handle402Request(req, res, parsedBody, headers, safeMaxCost, config).match( + result => res.status(200).json(result), + error => { + if (error.type === 'RESOURCE_PAYMENT_FAILED') { + logger.error('Failed to handle 402 request: payment failed', error.cause); + return buildX402Response(req, res, safeMaxCost); + } logger.error('Failed to handle 402 request', error); - return buildX402Response(req, res, safeMaxCost); + return res.status(500).json({ error: 'Internal server error' }); } - logger.error('Failed to handle 402 request', error); - return res.status(500).json({ error: 'Internal server error' }); - } + ); } return buildX402Response(req, res, safeMaxCost); @@ -152,17 +158,17 @@ export async function handleResourceRequestWithErrorHandling( res: Response, config: ResourceHandlerConfig ) { - try { - return await handleResourceRequest(req, res, config); - } catch (error) { - const { errorMessage } = config; - if (error instanceof HttpError) { - logger.error(errorMessage, error); - return res.status(error.statusCode).json({ error: errorMessage }); + return ResultAsync.fromPromise( + handleResourceRequest(req, res, config), + (error): HttpError => + error instanceof HttpError + ? error + : new HttpError(500, config.errorMessage || 'Internal server error') + ).match( + result => result, + (error: HttpError) => { + logger.error(config.errorMessage, error); + return res.status(error.statusCode).json({ error: config.errorMessage }); } - logger.error(errorMessage, error); - return res - .status(500) - .json({ error: errorMessage || 'Internal server error' }); - } + ); } diff --git a/packages/app/server/src/services/DbService.ts b/packages/app/server/src/services/DbService.ts index 93e85592c..c77c62b8d 100644 --- a/packages/app/server/src/services/DbService.ts +++ b/packages/app/server/src/services/DbService.ts @@ -19,6 +19,8 @@ import { import { Decimal } from '@prisma/client/runtime/library'; import logger from '../logger'; import { env } from '../env'; +import { Result, ResultAsync, ok, err } from 'neverthrow'; +import type { DbError, AuthError } from '../errors/results'; /** * Secret key for deterministic API key hashing (should match echo-control) @@ -44,102 +46,111 @@ export class EchoDbService { } /** - * Validate an API key and return user/app information - * Centralized logic previously duplicated in echo-control and echo-server + * Validate an API key and return user/app information. + * Returns a ResultAsync with the validation result or a typed AuthError. + * Centralized logic previously duplicated in echo-control and echo-server. */ - async validateApiKey(apiKey: string): Promise { - try { - // Remove Bearer prefix if present - const cleanApiKey = apiKey.replace('Bearer ', ''); - - const isJWT = cleanApiKey.split('.').length === 3; - - if (isJWT) { - const verifyResult = await jwtVerify(cleanApiKey, this.apiJwtSecret); + validateApiKey(apiKey: string): ResultAsync { + const cleanApiKey = apiKey.replace('Bearer ', ''); + const isJWT = cleanApiKey.split('.').length === 3; + + if (isJWT) { + return ResultAsync.fromPromise( + jwtVerify(cleanApiKey, this.apiJwtSecret), + (cause): AuthError => ({ type: 'AUTH_JWT_VERIFICATION_FAILED', cause }) + ).andThen((verifyResult): Result => { const payload = verifyResult.payload as unknown as EchoAccessJwtPayload; if (!payload) { - return null; + return err({ type: 'AUTH_INVALID_API_KEY' }); } - // Validate required fields exist if (!payload.user_id || !payload.app_id) { logger.error( `JWT missing required fields: user_id=${payload.user_id}, app_id=${payload.app_id}` ); - return null; + return err({ + type: 'AUTH_MISSING_FIELDS', + fields: [ + ...(!payload.user_id ? ['user_id'] : []), + ...(!payload.app_id ? ['app_id'] : []), + ], + }); } if (payload.exp && payload.exp < Date.now() / 1000) { - return null; + return err({ type: 'AUTH_EXPIRED_JWT' }); } - const user = await this.db.user.findUnique({ - where: { - id: payload.user_id, - }, - select: { - id: true, - email: true, - name: true, - createdAt: true, - updatedAt: true, - totalPaid: true, - totalSpent: true, - }, - }); - - const app = await this.db.echoApp.findUnique({ - where: { - id: payload.app_id, - }, - select: { - id: true, - name: true, - description: true, - isArchived: true, - createdAt: true, - updatedAt: true, - }, - }); - - if (!user || !app) { - return null; - } + return ok(payload); + }).andThen((payload: EchoAccessJwtPayload) => + ResultAsync.fromPromise( + Promise.all([ + this.db.user.findUnique({ + where: { id: payload.user_id }, + select: { + id: true, + email: true, + name: true, + createdAt: true, + updatedAt: true, + totalPaid: true, + totalSpent: true, + }, + }), + this.db.echoApp.findUnique({ + where: { id: payload.app_id }, + select: { + id: true, + name: true, + description: true, + isArchived: true, + createdAt: true, + updatedAt: true, + }, + }), + ]), + (cause): AuthError => ({ type: 'AUTH_JWT_VERIFICATION_FAILED', cause }) + ).andThen(([user, app]: [any, any]): Result => { + if (!user || !app) { + return err({ type: 'AUTH_INVALID_API_KEY' }); + } + + return ok({ + userId: payload.user_id, + echoAppId: payload.app_id, + user: { + id: user.id, + email: user.email, + ...(user.name && { name: user.name }), + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }, + echoApp: { + id: app.id, + name: app.name, + ...(app.description && { description: app.description }), + createdAt: app.createdAt.toISOString(), + updatedAt: app.updatedAt.toISOString(), + }, + }); + }) + ); + } - return { - userId: payload.user_id, - echoAppId: payload.app_id, - user: { - id: user.id, - email: user.email, - ...(user.name && { name: user.name }), - createdAt: user.createdAt.toISOString(), - updatedAt: user.updatedAt.toISOString(), - }, - echoApp: { - id: app.id, - name: app.name, - ...(app.description && { description: app.description }), - createdAt: app.createdAt.toISOString(), - updatedAt: app.updatedAt.toISOString(), - }, - }; - } - // Hash the provided API key for direct O(1) lookup - const keyHash = hashApiKey(cleanApiKey); + // Hash the provided API key for direct O(1) lookup + const keyHash = hashApiKey(cleanApiKey); - // Direct lookup by keyHash - O(1) operation! - const apiKeyRecord = await this.db.apiKey.findUnique({ - where: { - keyHash, - }, + return ResultAsync.fromPromise( + this.db.apiKey.findUnique({ + where: { keyHash }, include: { user: true, echoApp: true, }, - }); - + }), + (cause): AuthError => ({ type: 'AUTH_JWT_VERIFICATION_FAILED', cause }) + ).andThen((apiKeyRecord: any): Result => { // Verify the API key is valid and all related entities are active if ( !apiKeyRecord || @@ -147,10 +158,10 @@ export class EchoDbService { apiKeyRecord.user.isArchived || apiKeyRecord.echoApp.isArchived ) { - return null; + return err({ type: 'AUTH_INVALID_API_KEY' }); } - return { + return ok({ userId: apiKeyRecord.userId, echoAppId: apiKeyRecord.echoAppId, user: { @@ -170,11 +181,8 @@ export class EchoDbService { updatedAt: apiKeyRecord.echoApp.updatedAt.toISOString(), }, apiKeyId: apiKeyRecord.id, - }; - } catch (error) { - logger.error(`Error validating API key: ${error}`); - return null; - } + }); + }); } async getReferralCodeForUser( @@ -201,12 +209,13 @@ export class EchoDbService { } /** - * Calculate total balance for a user across all apps - * Uses User.totalPaid and User.totalSpent for consistent balance calculation + * Calculate total balance for a user across all apps. + * Returns a ResultAsync with the Balance or a typed DbError. + * Uses User.totalPaid and User.totalSpent for consistent balance calculation. */ - async getBalance(userId: string): Promise { - try { - const user = await this.db.user.findUnique({ + getBalance(userId: string): ResultAsync { + return ResultAsync.fromPromise( + this.db.user.findUnique({ where: { id: userId }, select: { id: true, @@ -217,34 +226,20 @@ export class EchoDbService { totalPaid: true, totalSpent: true, }, - }); - + }), + (cause): DbError => ({ type: 'DB_QUERY_FAILED', cause }) + ).andThen((user: any): Result => { if (!user) { logger.error(`User not found: ${userId}`); - return { - balance: 0, - totalPaid: 0, - totalSpent: 0, - }; + return err({ type: 'DB_NOT_FOUND', entity: 'User', id: userId }); } const totalPaid = Number(user.totalPaid); const totalSpent = Number(user.totalSpent); const balance = totalPaid - totalSpent; - return { - balance, - totalPaid, - totalSpent, - }; - } catch (error) { - logger.error(`Error fetching balance: ${error}`); - return { - balance: 0, - totalPaid: 0, - totalSpent: 0, - }; - } + return ok({ balance, totalPaid, totalSpent }); + }); } /** @@ -400,28 +395,21 @@ export class EchoDbService { } /** - * Create an LLM transaction record and atomically update user's totalSpent - * Centralized logic for transaction creation with atomic balance updates + * Create an LLM transaction record and atomically update user's totalSpent. + * Returns a ResultAsync with the created Transaction or a typed DbError. + * Centralized logic for transaction creation with atomic balance updates. */ - async createPaidTransaction( + createPaidTransaction( transaction: TransactionRequest - ): Promise { - try { - // Use a database transaction to atomically create the LLM transaction and update user balance - const result = await this.db.$transaction(async tx => { + ): ResultAsync { + return ResultAsync.fromPromise( + this.db.$transaction(async (tx: Prisma.TransactionClient) => { // Create the LLM transaction record - const dbTransaction = await this.createTransactionRecord( - tx, - transaction - ); + const dbTransaction = await this.createTransactionRecord(tx, transaction); if (transaction.userId) { // Update user's total spent amount - await this.updateUserTotalSpent( - tx, - transaction.userId, - transaction.totalCost - ); + await this.updateUserTotalSpent(tx, transaction.userId, transaction.totalCost); } // Update API key's last used timestamp if provided if (transaction.apiKeyId) { @@ -429,34 +417,29 @@ export class EchoDbService { } return dbTransaction; - }); - + }), + (cause): DbError => ({ type: 'DB_TRANSACTION_FAILED', cause }) + ).map((result: Transaction) => { logger.info( `Created transaction for model ${transaction.metadata.model}: $${transaction.totalCost}, updated user totalSpent`, result.id ); return result; - } catch (error) { - logger.error(`Error creating transaction and updating balance: ${error}`); - return null; - } + }); } /** - * Create a free tier transaction and update all related records atomically - * @param userId - The user ID - * @param spendPoolId - The spend pool ID + * Create a free tier transaction and update all related records atomically. + * Returns a ResultAsync with the transaction and usage records or a typed DbError. * @param transactionData - The transaction data to create + * @param spendPoolId - The spend pool ID */ - async createFreeTierTransaction( + createFreeTierTransaction( transactionData: TransactionRequest, spendPoolId: string - ): Promise<{ - transaction: Transaction; - userSpendPoolUsage: UserSpendPoolUsage | null; - }> { - try { - return await this.db.$transaction(async tx => { + ): ResultAsync<{ transaction: Transaction; userSpendPoolUsage: UserSpendPoolUsage | null }, DbError> { + return ResultAsync.fromPromise<{ transaction: Transaction; userSpendPoolUsage: UserSpendPoolUsage | null }, DbError>( + this.db.$transaction(async (tx: Prisma.TransactionClient) => { // 1. Verify the spend pool exists const spendPool = await tx.spendPool.findUnique({ where: { id: spendPoolId }, @@ -476,10 +459,7 @@ export class EchoDbService { ) : null; // 3. Create the transaction record - const transaction = await this.createTransactionRecord( - tx, - transactionData - ); + const transaction = await this.createTransactionRecord(tx, transactionData); // 4. Update API key lastUsed if apiKeyId is provided if (transactionData.apiKeyId) { @@ -487,26 +467,17 @@ export class EchoDbService { } // 5. Update totalSpent on the SpendPool using helper - await this.updateSpendPoolTotalSpent( - tx, - spendPoolId, - transactionData.totalCost - ); + await this.updateSpendPoolTotalSpent(tx, spendPoolId, transactionData.totalCost); logger.info( `Created free tier transaction for model ${transactionData.metadata.model}: $${transactionData.totalCost}`, transaction.id ); - return { - transaction, - userSpendPoolUsage, - }; - }); - } catch (error) { - logger.error(`Error creating free tier transaction: ${error}`); - throw error; - } + return { transaction, userSpendPoolUsage }; + }), + (cause): DbError => ({ type: 'DB_TRANSACTION_FAILED', cause }) + ); } async confirmAccessControl( diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index 861db6a62..db72f5245 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -57,17 +57,17 @@ export class EchoControlService { } /** - * Verify API key against the database and cache the authentication result - * Uses centralized logic from EchoDbService + * Verify API key against the database and cache the authentication result. + * Uses centralized logic from EchoDbService via ResultAsync. */ async verifyApiKey(): Promise { - try { - if (this.apiKey) { - this.authResult = await this.dbService.validateApiKey(this.apiKey); + if (this.apiKey) { + const result = await this.dbService.validateApiKey(this.apiKey); + if (result.isErr()) { + logger.error(`Error verifying API key: ${result.error.type}`); + return null; } - } catch (error) { - logger.error(`Error verifying API key: ${error}`); - return null; + this.authResult = result.value; } const markupData = await this.earningsService.getEarningsData( @@ -152,51 +152,45 @@ export class EchoControlService { } /** - * Get balance for the authenticated user directly from the database - * Uses centralized logic from EchoDbService + * Get balance for the authenticated user directly from the database. + * Uses centralized logic from EchoDbService via ResultAsync. */ async getBalance(): Promise { - try { - if (!this.authResult) { - logger.error('No authentication result available'); - return 0; - } + if (!this.authResult) { + logger.error('No authentication result available'); + return 0; + } - const { userId } = this.authResult; - const balance = await this.dbService.getBalance(userId); + const { userId } = this.authResult; + const result = await this.dbService.getBalance(userId); - return balance.balance; - } catch (error) { - logger.error(`Error fetching balance: ${error}`); + if (result.isErr()) { + logger.error(`Error fetching balance: ${result.error.type}`); return 0; } + + return result.value.balance; } /** - * Create an LLM transaction record directly in the database - * Uses centralized logic from EchoDbService + * Create an LLM transaction record directly in the database. + * Uses centralized logic from EchoDbService via ResultAsync. */ async createTransaction(transaction: Transaction): Promise { - try { - if (!this.authResult) { - logger.error('No authentication result available'); - return; - } + if (!this.authResult) { + logger.error('No authentication result available'); + return; + } - if (!this.markUpAmount) { - logger.error('Error Fetching Markup Amount'); - return; - } + if (!this.markUpAmount) { + logger.error('Error Fetching Markup Amount'); + return; + } - if (this.freeTierSpendPool) { - await this.createFreeTierTransaction(transaction); - return; - } else { - await this.createPaidTransaction(transaction); - return; - } - } catch (error) { - logger.error(`Error creating transaction: ${error}`); + if (this.freeTierSpendPool) { + await this.createFreeTierTransaction(transaction); + } else { + await this.createPaidTransaction(transaction); } } @@ -275,6 +269,7 @@ export class EchoControlService { echoProfit: echoProfitDecimal, }; } + async createFreeTierTransaction(transaction: Transaction): Promise { if (!this.authResult) { logger.error('No authentication result available'); @@ -321,10 +316,17 @@ export class EchoControlService { ...(this.referrerRewardId && { referrerRewardId: this.referrerRewardId }), }; - await this.freeTierService.createFreeTierTransaction( + const result = await this.freeTierService.createFreeTierTransaction( transactionData, this.freeTierSpendPool.id ); + + if (result.isErr()) { + const dbErr = result.error; + logger.error(`Error creating free tier transaction: ${dbErr.type}`); + const cause = 'cause' in dbErr ? dbErr.cause : undefined; + throw cause instanceof Error ? cause : new Error(String(dbErr.type)); + } } async createPaidTransaction(transaction: Transaction): Promise { @@ -361,7 +363,14 @@ export class EchoControlService { ...(this.referrerRewardId && { referrerRewardId: this.referrerRewardId }), }; - await this.dbService.createPaidTransaction(transactionData); + const result = await this.dbService.createPaidTransaction(transactionData); + + if (result.isErr()) { + const dbErr = result.error; + logger.error(`Error creating paid transaction: ${dbErr.type}`); + const cause = 'cause' in dbErr ? dbErr.cause : undefined; + throw cause instanceof Error ? cause : new Error(String(dbErr.type)); + } } async identifyX402Transaction( @@ -402,7 +411,14 @@ export class EchoControlService { transactionType: EnumTransactionType.X402, }; - await this.dbService.createPaidTransaction(transactionData); + const result = await this.dbService.createPaidTransaction(transactionData); + + if (result.isErr()) { + const dbErr = result.error; + logger.error(`Error creating X402 transaction: ${dbErr.type}`); + const cause = 'cause' in dbErr ? dbErr.cause : undefined; + throw cause instanceof Error ? cause : new Error(String(dbErr.type)); + } return transactionCosts; } diff --git a/packages/app/server/src/services/FreeTierService.ts b/packages/app/server/src/services/FreeTierService.ts index d4a8376fe..d239aa3bf 100644 --- a/packages/app/server/src/services/FreeTierService.ts +++ b/packages/app/server/src/services/FreeTierService.ts @@ -96,15 +96,12 @@ class FreeTierService { * Create a free tier transaction and update all related records atomically * Delegates to DbService for shared transaction logic */ - async createFreeTierTransaction( + createFreeTierTransaction( transactionData: TransactionRequest, spendPoolId: string ) { // Delegate to the centralized DbService method - return await this.dbService.createFreeTierTransaction( - transactionData, - spendPoolId - ); + return this.dbService.createFreeTierTransaction(transactionData, spendPoolId); } } diff --git a/packages/sdk/ts/src/supported-models/chat/gemini.ts b/packages/sdk/ts/src/supported-models/chat/gemini.ts index 53197b7f5..92073a22d 100644 --- a/packages/sdk/ts/src/supported-models/chat/gemini.ts +++ b/packages/sdk/ts/src/supported-models/chat/gemini.ts @@ -10,13 +10,11 @@ export type GeminiModel = | 'gemini-2.0-flash-lite-001' | 'gemini-2.0-flash-lite-preview' | 'gemini-2.0-flash-lite-preview-02-05' - | 'gemini-2.0-flash-preview-image-generation' | 'gemini-2.0-flash-thinking-exp' | 'gemini-2.0-flash-thinking-exp-01-21' | 'gemini-2.0-flash-thinking-exp-1219' | 'gemini-2.5-flash' | 'gemini-2.5-flash-image' - | 'gemini-2.5-flash-image-preview' | 'gemini-2.5-flash-lite' | 'gemini-2.5-flash-lite-preview-06-17' | 'gemini-2.5-flash-lite-preview-09-2025' @@ -78,12 +76,6 @@ export const GeminiModels: SupportedModel[] = [ output_cost_per_token: 3e-7, provider: 'Gemini', }, - { - model_id: 'gemini-2.0-flash-preview-image-generation', - input_cost_per_token: 1e-7, - output_cost_per_token: 4e-7, - provider: 'Gemini', - }, { model_id: 'gemini-2.0-flash-thinking-exp', input_cost_per_token: 1e-7, @@ -114,12 +106,6 @@ export const GeminiModels: SupportedModel[] = [ output_cost_per_token: 0.0000025, provider: 'Gemini', }, - { - model_id: 'gemini-2.5-flash-image-preview', - input_cost_per_token: 3e-7, - output_cost_per_token: 0.0000025, - provider: 'Gemini', - }, { model_id: 'gemini-2.5-flash-lite', input_cost_per_token: 1e-7, diff --git a/packages/tests/provider-smoke/gemini-generate-text.test.ts b/packages/tests/provider-smoke/gemini-generate-text.test.ts index da2339df1..8867a7704 100644 --- a/packages/tests/provider-smoke/gemini-generate-text.test.ts +++ b/packages/tests/provider-smoke/gemini-generate-text.test.ts @@ -15,7 +15,6 @@ import { beforeAll(assertEnv); export const BLACKLISTED_MODELS = new Set([ - 'gemini-2.0-flash-preview-image-generation', 'veo-3.0-fast-generate', 'gemini-2.0-flash-exp', 'gemini-2.0-flash-thinking-exp-1219', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06985ade8..560b13bc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: 10.1.5(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)) + version: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-prefer-arrow: specifier: ^1.2.3 version: 1.2.3(eslint@9.35.0(jiti@2.5.1)) @@ -70,7 +70,7 @@ importers: version: 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@coinbase/x402': specifier: ^0.6.4 - version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) '@hookform/resolvers': specifier: ^5.2.1 version: 5.2.1(react-hook-form@7.62.0(react@19.1.1)) @@ -501,6 +501,9 @@ importers: multer: specifier: ^2.0.2 version: 2.0.2 + neverthrow: + specifier: ^7.2.0 + version: 7.2.0 next: specifier: ^15.5.9 version: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -7955,11 +7958,13 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@11.12.0: @@ -10998,6 +11003,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me teeny-request@9.0.0: resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} @@ -11730,6 +11736,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -12688,11 +12695,11 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -17194,6 +17201,10 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -17204,6 +17215,10 @@ snapshots: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -17212,6 +17227,10 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17367,6 +17386,31 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -18777,14 +18821,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.11.2(@types/node@20.19.16)(typescript@5.9.2) - vite: 6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: @@ -18832,7 +18876,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.3)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.19.16)(@vitest/ui@3.2.3)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/utils@3.0.9': dependencies: @@ -21903,8 +21947,8 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.35.0(jiti@2.5.1)) @@ -21962,21 +22006,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 9.35.0(jiti@2.5.1) - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.9.0 - optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)) - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): dependencies: debug: 3.2.7 @@ -21988,17 +22017,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.35.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) - transitivePeerDependencies: - - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -22028,35 +22046,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.35.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.35.0(jiti@2.5.1)): dependencies: aria-query: 5.3.2 @@ -27767,7 +27756,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 @@ -28349,14 +28338,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 diff --git a/templates/next-image/src/app/api/edit-image/google.ts b/templates/next-image/src/app/api/edit-image/google.ts index 527c4879d..616f53a3f 100644 --- a/templates/next-image/src/app/api/edit-image/google.ts +++ b/templates/next-image/src/app/api/edit-image/google.ts @@ -28,7 +28,7 @@ export async function handleGoogleEdit( ]; const result = await generateText({ - model: google('gemini-2.5-flash-image-preview'), + model: google('gemini-2.5-flash-image'), prompt: [ { role: 'user', diff --git a/templates/next-image/src/app/api/generate-image/google.ts b/templates/next-image/src/app/api/generate-image/google.ts index 4fcdffb3a..1a1710be9 100644 --- a/templates/next-image/src/app/api/generate-image/google.ts +++ b/templates/next-image/src/app/api/generate-image/google.ts @@ -12,7 +12,7 @@ import { ERROR_MESSAGES } from '@/lib/constants'; export async function handleGoogleGenerate(prompt: string): Promise { try { const result = await generateText({ - model: google('gemini-2.5-flash-image-preview'), + model: google('gemini-2.5-flash-image'), prompt, });