From a5a0b136d6e4a76af5866d01d01e1d692da1685c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0ren=20SALTALI?= Date: Thu, 5 Dec 2024 17:14:35 +0300 Subject: [PATCH] feat: Add pre process for endpoints with service binding (#85) * feat: add pre-processing and post-processing logic for integration services * feat: add SAGError class for enhanced error handling and API response * feat: integrate SAGError for improved error handling in Auth0 integration * refactor: streamline error handling and response generation in API integration * feat: enhance pre-processing logic and ensure proper request body handling --- src/index.js | 422 +++++++++++++++++++------------------- src/integrations/auth0.js | 26 +-- src/types/error_types.js | 28 ++- 3 files changed, 240 insertions(+), 236 deletions(-) diff --git a/src/index.js b/src/index.js index 217f9d6..7d6e497 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ const responses = await import('./responses'); const { ValueMapper } = await import('./mapping'); const { setCorsHeaders } = await import('./cors'); const { PathOperator } = await import('./path-ops'); -const { AuthError } = await import('./types/error_types'); +const { AuthError, SAGError } = await import('./types/error_types'); const { setPoweredByHeader } = await import('./powered-by'); const { createProxiedRequest } = await import('./requests'); const { IntegrationTypeEnum } = await import('./enums/integration-type'); @@ -15,181 +15,177 @@ const { supabaseEmailOTP, supabasePhoneOTP, supabaseVerifyOTP, supabaseJwtVerify export default { async fetch(request, env, ctx) { const sagContext = new ServerlessAPIGatewayContext(); - sagContext.apiConfig = await this.getApiConfig(env); - sagContext.requestUrl = new URL(request.url); + try { + sagContext.apiConfig = await this.getApiConfig(env); + sagContext.requestUrl = new URL(request.url); - // Handle CORS preflight (OPTIONS) requests directly - if (sagContext.apiConfig.cors && request.method === 'OPTIONS') { - const matchedItem = sagContext.apiConfig.paths.find((item) => { - const matchResult = PathOperator.match(item.path, sagContext.requestUrl.pathname, request.method, item.method); - return item.method === 'OPTIONS' && matchResult.matchedCount > 0 && matchResult.methodMatches; - }); - if (!matchedItem) { - return setPoweredByHeader(setCorsHeaders(request, new Response(null, { status: 204 }), sagContext.apiConfig.cors)); + // Handle CORS preflight (OPTIONS) requests directly + if (sagContext.apiConfig.cors && request.method === 'OPTIONS') { + const matchedItem = sagContext.apiConfig.paths.find((item) => { + const matchResult = PathOperator.match(item.path, sagContext.requestUrl.pathname, request.method, item.method); + return item.method === 'OPTIONS' && matchResult.matchedCount > 0 && matchResult.methodMatches; + }); + if (!matchedItem) { + return setPoweredByHeader(setCorsHeaders(request, new Response(null, { status: 204 }), sagContext.apiConfig.cors)); + } } - } - - // Adjusted filtering based on the updated pathsMatch return value - const matchedPaths = sagContext.apiConfig.paths - .map((config) => ({ config, matchResult: PathOperator.match(config.path, sagContext.requestUrl.pathname, request.method, config.method) })) - .filter((item) => item.matchResult.matchedCount > 0 && item.matchResult.methodMatches); // Only consider matches with the correct method - + // Adjusted filtering based on the updated pathsMatch return value + const matchedPaths = sagContext.apiConfig.paths + .map((config) => ({ config, matchResult: PathOperator.match(config.path, sagContext.requestUrl.pathname, request.method, config.method) })) + .filter((item) => item.matchResult.matchedCount > 0 && item.matchResult.methodMatches); // Only consider matches with the correct method - // Sorting with priority: exact matches > parameterized matches > wildcard matches - const matchedPath = matchedPaths.sort((a, b) => { - // Prioritize exact matches - if (a.matchResult.isExact !== b.matchResult.isExact) { - return a.matchResult.isExact ? -1 : 1; - } - // Among exact or parameterized matches, prioritize those with more matched segments - if (a.matchResult.matchedCount !== b.matchResult.matchedCount) { - return b.matchResult.matchedCount - a.matchResult.matchedCount; - } - // If both are parameterized, prioritize non-wildcard over wildcard - if (a.matchResult.isWildcard !== b.matchResult.isWildcard) { - return a.matchResult.isWildcard ? 1 : -1; - } - // Prioritize exact method matches over "ANY" - if (a.config.method !== b.config.method) { - if (a.config.method === request.method) return -1; - if (b.config.method === request.method) return 1; - } - return 0; // Equal priority - })[0]; + // Sorting with priority: exact matches > parameterized matches > wildcard matches + const matchedPath = matchedPaths.sort((a, b) => { + // Prioritize exact matches + if (a.matchResult.isExact !== b.matchResult.isExact) { + return a.matchResult.isExact ? -1 : 1; + } + // Among exact or parameterized matches, prioritize those with more matched segments + if (a.matchResult.matchedCount !== b.matchResult.matchedCount) { + return b.matchResult.matchedCount - a.matchResult.matchedCount; + } + // If both are parameterized, prioritize non-wildcard over wildcard + if (a.matchResult.isWildcard !== b.matchResult.isWildcard) { + return a.matchResult.isWildcard ? 1 : -1; + } + // Prioritize exact method matches over "ANY" + if (a.config.method !== b.config.method) { + if (a.config.method === request.method) return -1; + if (b.config.method === request.method) return 1; + } + return 0; // Equal priority + })[0]; - sagContext.matchedPath = matchedPath; + sagContext.matchedPath = matchedPath; - if (matchedPath) { - // Check if the matched path requires authorization - if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'jwt') { - try { - sagContext.jwtPayload = await jwtAuth(request, sagContext.apiConfig); - } catch (error) { - console.error('Error validating JWT', error.message); - if (error instanceof AuthError) { - return setPoweredByHeader( - setCorsHeaders( - request, - new Response(safeStringify({ error: error.message, code: error.code }), { - status: error.statusCode, - headers: { 'Content-Type': 'application/json' }, - }), - sagContext.apiConfig.cors - ), - ); - } else { - return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + if (matchedPath) { + // Check if the matched path requires authorization + if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'jwt') { + try { + sagContext.jwtPayload = await jwtAuth(request, sagContext.apiConfig); + } catch (error) { + console.error('Error validating JWT', error.message); + if (error instanceof AuthError) { + return setPoweredByHeader( + setCorsHeaders( + request, + new Response(safeStringify({ error: error.message, code: error.code }), { + status: error.statusCode, + headers: { 'Content-Type': 'application/json' }, + }), + sagContext.apiConfig.cors + ), + ); + } else if (error instanceof GenericError) { + return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors)); + } else { + return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + } } - } - } else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'auth0') { - try { - sagContext.jwtPayload = await validateIdToken(request, null, sagContext.apiConfig.authorizer); - } catch (error) { - if (error instanceof AuthError) { - return setPoweredByHeader( - setCorsHeaders( - request, - new Response(safeStringify({ error: error.message, code: error.code }), { - status: error.statusCode, - headers: { 'Content-Type': 'application/json' }, - }), - sagContext.apiConfig.cors - ), - ); - } else { - return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + } else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'auth0') { + try { + sagContext.jwtPayload = await validateIdToken(request, null, sagContext.apiConfig.authorizer); + } catch (error) { + if (error instanceof AuthError) { + return setPoweredByHeader( + setCorsHeaders( + request, + new Response(safeStringify({ error: error.message, code: error.code }), { + status: error.statusCode, + headers: { 'Content-Type': 'application/json' }, + }), + sagContext.apiConfig.cors + ), + ); + } else if (error instanceof GenericError) { + return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors)); + } else { + return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + } } - } - } else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'supabase') { - try { - sagContext.jwtPayload = await supabaseJwtVerify(request, sagContext.apiConfig.authorizer); - console.log('JWT Payload:', sagContext.jwtPayload); - } catch (error) { - console.error('Error validating JWT', error.message); - if (error instanceof AuthError) { - return setPoweredByHeader( - setCorsHeaders( - request, - new Response(safeStringify({ error: error.message, code: error.code }), { - status: error.statusCode, - headers: { 'Content-Type': 'application/json' }, - }), - sagContext.apiConfig.cors - ), - ); - } else { - return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + } else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'supabase') { + try { + sagContext.jwtPayload = await supabaseJwtVerify(request, sagContext.apiConfig.authorizer); + console.log('JWT Payload:', sagContext.jwtPayload); + } catch (error) { + console.error('Error validating JWT', error.message); + if (error instanceof AuthError) { + return setPoweredByHeader( + setCorsHeaders( + request, + new Response(safeStringify({ error: error.message, code: error.code }), { + status: error.statusCode, + headers: { 'Content-Type': 'application/json' }, + }), + sagContext.apiConfig.cors + ), + ); + } else if (error instanceof GenericError) { + return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors)); + } else { + return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + } } } - } - if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.HTTP_PROXY) { - const server = - sagContext.apiConfig.servers && - sagContext.apiConfig.servers.find((server) => server.alias === matchedPath.config.integration.server); - if (server) { - let modifiedRequest = createProxiedRequest(request, server, matchedPath.config); - if (matchedPath.config.mapping) { - modifiedRequest = await ValueMapper.modify({ - request: modifiedRequest, - mappingConfig: matchedPath.config.mapping, - jwtPayload: sagContext.jwtPayload, - configVariables: matchedPath.config.variables, - globalVariables: sagContext.apiConfig.variables, - }); + // Preprocess logic + if (matchedPath.config.integration && matchedPath.config.pre_process) { + const service = + sagContext.apiConfig.serviceBindings && + sagContext.apiConfig.serviceBindings.find((serviceBinding) => serviceBinding.alias === matchedPath.config.pre_process.binding); + + if (service) { + const [body1, body2] = request.body.tee(); + let response = await env[service.binding][matchedPath.config.pre_process.function](new Request(request, { body: body1 }), safeStringify(env), safeStringify(sagContext)); + if (response !== true) { + return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors)); + } + request = new Request(request, { body: body2 }); } - return fetch(modifiedRequest).then((response) => setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors))); } - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE) { - const service = - sagContext.apiConfig.services && - sagContext.apiConfig.services.find((service) => service.alias === matchedPath.config.integration.binding); - if (service) { - const module = await import(`${service.entrypoint}.js`); - const Service = module.default; - const serviceInstance = new Service(); - const response = await serviceInstance.fetch(request, env, ctx); - try { + console.log('Matched path:', matchedPath.config); + + if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.HTTP_PROXY) { + const server = + sagContext.apiConfig.servers && + sagContext.apiConfig.servers.find((server) => server.alias === matchedPath.config.integration.server); + if (server) { + let modifiedRequest = createProxiedRequest(request, server, matchedPath.config); + if (matchedPath.config.mapping) { + modifiedRequest = await ValueMapper.modify({ + request: modifiedRequest, + mappingConfig: matchedPath.config.mapping, + jwtPayload: sagContext.jwtPayload, + configVariables: matchedPath.config.variables, + globalVariables: sagContext.apiConfig.variables, + }); + } + return fetch(modifiedRequest).then((response) => setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors))); + } + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE) { + const service = + sagContext.apiConfig.services && + sagContext.apiConfig.services.find((service) => service.alias === matchedPath.config.integration.binding); + + if (service) { + const module = await import(`${service.entrypoint}.js`); + const Service = module.default; + const serviceInstance = new Service(); + const response = await serviceInstance.fetch(request, env, ctx); return setPoweredByHeader(setCorsHeaders(request, generateJsonResponse(response), sagContext.apiConfig.cors)); - } catch (error) { - return setPoweredByHeader( - setCorsHeaders( - request, - new Response(safeStringify({ error: error.message, code: error.code }), { - status: error.statusCode || 500, - headers: { 'Content-Type': 'application/json' }, - }), - sagContext.apiConfig.cors - ) - ); } - } - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE_BINDING) { - const service = - sagContext.apiConfig.serviceBindings && - sagContext.apiConfig.serviceBindings.find((serviceBinding) => serviceBinding.alias === matchedPath.config.integration.binding); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE_BINDING) { + const service = + sagContext.apiConfig.serviceBindings && + sagContext.apiConfig.serviceBindings.find((serviceBinding) => serviceBinding.alias === matchedPath.config.integration.binding); - if (service) { - try { + if (service) { const response = await env[service.binding][matchedPath.config.integration.function](request, safeStringify(env), safeStringify(sagContext)); return setPoweredByHeader(setCorsHeaders(request, generateJsonResponse(response), sagContext.apiConfig.cors)); - } catch (error) { - return setPoweredByHeader( - setCorsHeaders( - request, - new Response(safeStringify({ error: error.message, code: error.code }), { - status: error.statusCode || 500, - headers: { 'Content-Type': 'application/json' }, - }), - sagContext.apiConfig.cors - ) - ); } - } - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACK) { - try { + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACK) { const urlParams = new URLSearchParams(sagContext.requestUrl.search); const code = urlParams.get('code'); @@ -217,76 +213,68 @@ export default { }), sagContext.apiConfig.cors )); - } catch (error) { - console.error('Error processing Auth0 callback', error); - if (error instanceof AuthError) { - return setPoweredByHeader(setCorsHeaders( - request, - new Response(safeStringify({ error: error.message, code: error.code }), { - status: error.statusCode, - headers: { 'Content-Type': 'application/json' }, - }), - sagContext.apiConfig.cors - )); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0USERINFO) { + const urlParams = new URLSearchParams(sagContext.requestUrl.search); + const accessToken = urlParams.get('access_token'); + + return getProfile(accessToken, sagContext.apiConfig.authorizer); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACKREDIRECT) { + const urlParams = new URLSearchParams(sagContext.requestUrl.search); + return redirectToLogin({ state: urlParams.get('state') }, sagContext.apiConfig.authorizer); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0REFRESH) { + return this.refreshTokenLogic(request, env, sagContext); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSAUTH) { + const requestBody = await request.json(); + const email = requestBody.email; + const phone = requestBody.phone; + + if (email) { + return await supabaseEmailOTP(env, email); + } else if (phone) { + return await supabasePhoneOTP(env, phone); } else { - return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors)); + return new Response(safeStringify({ error: 'Missing email or phone', code: 'missing_email_or_phone' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); } - } - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0USERINFO) { - const urlParams = new URLSearchParams(sagContext.requestUrl.search); - const accessToken = urlParams.get('access_token'); + } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSVERIFY) { + const requestBody = await request.json(); + const token = requestBody.token; + const email = requestBody.email; + const phone = requestBody.phone; - return getProfile(accessToken, sagContext.apiConfig.authorizer); - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACKREDIRECT) { - const urlParams = new URLSearchParams(sagContext.requestUrl.search); - return redirectToLogin({ state: urlParams.get('state') }, sagContext.apiConfig.authorizer); - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0REFRESH) { - return this.refreshTokenLogic(request, env, sagContext); - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSAUTH) { - const requestBody = await request.json(); - const email = requestBody.email; - const phone = requestBody.phone; + if (!token || (!email && !phone)) { + return new Response(safeStringify({ error: 'Missing token, email, or phone', code: 'missing_token_or_contact' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } - if (email) { - return supabaseEmailOTP(env, email); - } else if (phone) { - return supabasePhoneOTP(env, phone); + const response = await supabaseVerifyOTP(env, email, phone, token); + return setPoweredByHeader(setCorsHeaders(request, + new Response(safeStringify(response), { status: 200, headers: { 'Content-Type': 'application/json' }, }), + sagContext.apiConfig.cors + )); } else { - return new Response(safeStringify({ error: 'Missing email or phone', code: 'missing_email_or_phone' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSVERIFY) { - const requestBody = await request.json(); - const token = requestBody.token; - const email = requestBody.email; - const phone = requestBody.phone; - - if (!token || (!email && !phone)) { - return new Response(safeStringify({ error: 'Missing token, email, or phone', code: 'missing_token_or_contact' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); + return setPoweredByHeader( + setCorsHeaders( + request, + new Response(safeStringify(matchedPath.config.response), { headers: { 'Content-Type': 'application/json' } }), + sagContext.apiConfig.cors + ), + ); } + } - const response = await supabaseVerifyOTP(env, email, phone, token); - return setPoweredByHeader(setCorsHeaders(request, - new Response(safeStringify(response), { status: 200, headers: { 'Content-Type': 'application/json' }, }), - sagContext.apiConfig.cors - )); + return setPoweredByHeader(setCorsHeaders(request, responses.noMatchResponse(), sagContext.apiConfig.cors)); + } catch (error) { + if (error instanceof AuthError || error instanceof SAGError) { + return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext?.apiConfig?.cors)); } else { - return setPoweredByHeader( - setCorsHeaders( - request, - new Response(safeStringify(matchedPath.config.response), { headers: { 'Content-Type': 'application/json' } }), - sagContext.apiConfig.cors - ), - ); + return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext?.apiConfig?.cors)); } } - - return setPoweredByHeader(setCorsHeaders(request, responses.noMatchResponse(), sagContext.apiConfig.cors)); }, async getApiConfig(env) { @@ -348,6 +336,8 @@ export default { sagContext.apiConfig.cors )); } + } else if (error instanceof SAGError) { + return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors)); } else { return setPoweredByHeader( setCorsHeaders( diff --git a/src/integrations/auth0.js b/src/integrations/auth0.js index def0740..410a484 100644 --- a/src/integrations/auth0.js +++ b/src/integrations/auth0.js @@ -1,5 +1,5 @@ import { jwtVerify, createLocalJWKSet, createRemoteJWKSet, errors } from 'jose'; -import { AuthError } from "../types/error_types"; +import { AuthError, SAGError } from "../types/error_types"; async function auth0CallbackHandler(code, authorizer) { const { domain, client_id, client_secret, redirect_uri } = authorizer; @@ -25,13 +25,13 @@ async function auth0CallbackHandler(code, authorizer) { if (!response.ok) { const errorData = await response.json(); - throw new Error(`Failed to fetch token: ${JSON.stringify(errorData)}`); + throw new SAGError(`Failed to fetch token: ${JSON.stringify(errorData)}`, nil, 500, nil); } const jwt = await response.json(); return jwt; } catch (error) { - throw new Error(`Internal Server Error: ${error.message}`); + throw new SAGError('Internal Server Error', nil, 500, error.message); } } @@ -109,13 +109,7 @@ async function getProfile(accessToken, authorizer) { if (!response.ok) { const errorData = await response.json(); - return new Response(JSON.stringify({ - error: 'Failed to fetch token', - details: errorData - }), { - status: response.status, - headers: { 'Content-Type': 'application/json' } - }); + throw new SAGError('Failed to fetch token', response.status, response.status, JSON.stringify(errorData)); } const data = await response.json(); @@ -124,13 +118,7 @@ async function getProfile(accessToken, authorizer) { headers: { 'Content-Type': 'application/json' } }); } catch (error) { - return new Response(JSON.stringify({ - error: 'Internal Server Error', - message: error.message - }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); + throw new SAGError('Internal Server Error', nil, 500, error.message); } } @@ -163,13 +151,13 @@ async function refreshToken(refreshToken, authorizer) { if (!response.ok) { const errorData = await response.json(); - throw new Error(`Failed to fetch token: ${JSON.stringify(errorData)}`); + throw new SAGError(`Failed to fetch token: ${JSON.stringify(errorData)}`, response.status, response.status, nil); } const jwt = await response.json(); return jwt; } catch (error) { - throw new Error(`Internal Server Error: ${error.message}`); + throw new SAGError('Internal Server Error', nil, 500, error.message); } } diff --git a/src/types/error_types.js b/src/types/error_types.js index 36c97b2..85221bb 100644 --- a/src/types/error_types.js +++ b/src/types/error_types.js @@ -5,6 +5,32 @@ class AuthError extends Error { this.code = code; this.statusCode = statusCode; } + + toApiResponse() { + return new Response(JSON.stringify({ error: this.message, code: this.code }), { + status: this.statusCode, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +class SAGError extends Error { + constructor(message, code, statusCode, logMessage) { + super(message); + this.name = 'SAGError'; + this.code = code; + this.statusCode = statusCode; + this.logMessage = logMessage; + Error.captureStackTrace(this, this.constructor); + } + + toApiResponse() { + console.error(this.logMessage || this.message); + return new Response(JSON.stringify({ error: this.message, code: this.code }), { + status: this.statusCode, + headers: { 'Content-Type': 'application/json' }, + }); + } } -export { AuthError }; +export { AuthError, SAGError };