diff --git a/dev/config/src/vite/vite.test.config.ts b/dev/config/src/vite/vite.test.config.ts index 17a3820f29..537e9eee8a 100644 --- a/dev/config/src/vite/vite.test.config.ts +++ b/dev/config/src/vite/vite.test.config.ts @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + import { defineConfig } from "vitest/config"; import VitePluginCloseAndCopy from "./vite-plugin-close-and-copy.js"; import VitePluginSourcemapExclude from "./vite-plugin-sourcemap-exclude.js"; diff --git a/packages/common/src/error.ts b/packages/common/src/error.ts index f26ad8dc6a..03fb6e4084 100644 --- a/packages/common/src/error.ts +++ b/packages/common/src/error.ts @@ -13,7 +13,9 @@ // limitations under the License. import { type TranslationKey, i18n as i18next } from "@prosopo/locale"; +import { ZodError } from "zod"; import { type LogLevel, type Logger, getLoggerDefault } from "./index.js"; +import type { ApiJsonError } from "./types.js"; type BaseErrorOptions = { name?: string; @@ -70,6 +72,9 @@ export abstract class ProsopoBaseError< private logError(logger: Logger, logLevel: LogLevel, errorName?: string) { const errorParams = { error: this.message, context: this.context }; const errorMessage = { errorType: errorName || this.name, errorParams }; + if (logLevel === "debug") { + logger.debug(this.stack); + } logger[logLevel](errorMessage); } } @@ -176,3 +181,42 @@ export class ProsopoApiError extends ProsopoBaseError { this.code = code; } } + +export const unwrapError = (err: ProsopoBaseError | SyntaxError | ZodError) => { + const code = "code" in err ? (err.code as number) : 400; + let message = i18next.t(err.message); + let jsonError: ApiJsonError = { code, message }; + let statusMessage = err.message; + jsonError.message = message; + // unwrap the errors to get the actual error message + while (err instanceof ProsopoBaseError && err.context) { + // base error will not have a translation key + jsonError.key = + err.context.translationKey || err.translationKey || "API.UNKNOWN"; + jsonError.message = err.message; + if (err.context.error) { + err = err.context.error; + } else { + break; + } + } + + if (isZodError(err)) { + message = i18next.t("API.PARSE_ERROR"); + statusMessage = message; + if (typeof err.message === "object") { + jsonError = err.message; + } else { + jsonError.message = JSON.parse(err.message); + } + } + + jsonError.code = jsonError.code || code; + return { code, statusMessage, jsonError }; +}; + +export const isZodError = (err: unknown): err is ZodError => { + return Boolean( + err && (err instanceof ZodError || (err as ZodError).name === "ZodError"), + ); +}; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 3211006c89..81107a1307 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -17,3 +17,4 @@ export * from "./logger.js"; export * from "./array.js"; export * from "./hash.js"; export * from "./string.js"; +export * from "./types.js"; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts new file mode 100644 index 0000000000..f3c025f8a5 --- /dev/null +++ b/packages/common/src/types.ts @@ -0,0 +1,18 @@ +// Copyright 2021-2024 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +export type ApiJsonError = { + message: string; + key?: string; + code: number; +}; diff --git a/packages/locale/src/locales/de.json b/packages/locale/src/locales/de.json index b9e8b101a3..6aa2eda2cb 100644 --- a/packages/locale/src/locales/de.json +++ b/packages/locale/src/locales/de.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Falscher CAPTCHA-Typ", "UNAUTHORIZED_ORIGIN_URL": "Nicht autorisierte Ursprungs-URL", "INVALID_SITE_KEY": "Ungültiger Site-Schlüssel", - "INVALID_IP": "Ungültige IP" + "INVALID_IP": "Ungültige IP", + "INVALID_URL": "Ungültiger URL", + "INVALID_BODY": "Ungültiger Körper", + "PARSE_ERROR": "Fehler beim Parsen der Anfrage" }, "CLI": { "PARAMETER_ERROR": "Ungültiger Parameter" diff --git a/packages/locale/src/locales/en.json b/packages/locale/src/locales/en.json index 975e877d80..92e056e8c4 100644 --- a/packages/locale/src/locales/en.json +++ b/packages/locale/src/locales/en.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Incorrect CAPTCHA type", "INVALID_SITE_KEY": "Invalid site key", "UNAUTHORIZED_ORIGIN_URL": "Unauthorized origin URL", - "INVALID_IP": "Invalid IP" + "INVALID_IP": "Invalid IP", + "INVALID_URL": "Invalid URL", + "INVALID_BODY": "Invalid body", + "PARSE_ERROR": "Error parsing request" }, "CLI": { "PARAMETER_ERROR": "Invalid parameter" diff --git a/packages/locale/src/locales/es.json b/packages/locale/src/locales/es.json index d7145e10c7..02eda4de68 100644 --- a/packages/locale/src/locales/es.json +++ b/packages/locale/src/locales/es.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Tipo di CAPTCHA errato", "UNAUTHORIZED_ORIGIN_URL": "URL de origen no autorizada", "INVALID_SITE_KEY": "Clave de sitio no válida", - "INVALID_IP": "IP no válida" + "INVALID_IP": "IP no válida", + "INVALID_URL": "URL inválida", + "INVALID_BODY": "Cuerpo inválido", + "PARSE_ERROR": "Error al analizar la respuesta" }, "CLI": { "PARAMETER_ERROR": "Parámetro inválido" diff --git a/packages/locale/src/locales/fr.json b/packages/locale/src/locales/fr.json index f50d99a28a..234d0c722b 100644 --- a/packages/locale/src/locales/fr.json +++ b/packages/locale/src/locales/fr.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Type de CAPTCHA incorrect", "UNAUTHORIZED_ORIGIN_URL": "URL d'origine non autorisée", "INVALID_SITE_KEY": "Clé de site non valide", - "INVALID_IP": "IP invalide" + "INVALID_IP": "IP invalide", + "INVALID_URL": "URL inválida", + "INVALID_BODY": "Cuerpo inválido", + "PARSE_ERROR": "Erreur d'analyse de la demande" }, "CLI": { "PARAMETER_ERROR": "Paramètre invalide" diff --git a/packages/locale/src/locales/it.json b/packages/locale/src/locales/it.json index 08aca80628..9609f7dacd 100644 --- a/packages/locale/src/locales/it.json +++ b/packages/locale/src/locales/it.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Tipo di CAPTCHA errato", "UNAUTHORIZED_ORIGIN_URL": "URL di origine non autorizzato", "INVALID_SITE_KEY": "Chiave del sito non valida", - "INVALID_IP": "IP non valido" + "INVALID_IP": "IP non valido", + "INVALID_URL": "URL non valido", + "INVALID_BODY": "Corpo non valido", + "PARSE_ERROR": "Errore nell'analisi della richiesta" }, "CLI": { "PARAMETER_ERROR": "Parametro non valido" diff --git a/packages/locale/src/locales/pt-BR.json b/packages/locale/src/locales/pt-BR.json index 012cfba53c..21b16378d7 100644 --- a/packages/locale/src/locales/pt-BR.json +++ b/packages/locale/src/locales/pt-BR.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Tipo de CAPTCHA incorreto", "UNAUTHORIZED_ORIGIN_URL": "URL de origem não autorizada", "INVALID_SITE_KEY": "Chave de site inválida", - "INVALID_IP": "IP inválido" + "INVALID_IP": "IP inválido", + "INVALID_URL": "URL inválido", + "INVALID_BODY": "Corpo inválido", + "PARSE_ERROR": "Erro ao analisar a resposta" }, "CLI": { "PARAMETER_ERROR": "Parâmetro inválido" diff --git a/packages/locale/src/locales/pt.json b/packages/locale/src/locales/pt.json index a223830a64..042f116f95 100644 --- a/packages/locale/src/locales/pt.json +++ b/packages/locale/src/locales/pt.json @@ -108,7 +108,10 @@ "INCORRECT_CAPTCHA_TYPE": "Tipo de CAPTCHA incorreto", "UNAUTHORIZED_ORIGIN_URL": "URL de origem não autorizada", "INVALID_SITE_KEY": "Chave de site inválida", - "INVALID_IP": "IP inválido" + "INVALID_IP": "IP inválido", + "INVALID_URL": "URL inválido", + "INVALID_BODY": "Corpo inválido", + "PARSE_ERROR": "Erro ao analisar a resposta" }, "CLI": { "PARAMETER_ERROR": "Parâmetro inválido" diff --git a/packages/provider/src/api/captcha.ts b/packages/provider/src/api/captcha.ts index 0cd896f5a7..0355a0251c 100644 --- a/packages/provider/src/api/captcha.ts +++ b/packages/provider/src/api/captcha.ts @@ -26,29 +26,21 @@ import { type CaptchaSolutionResponse, type DappUserSolutionResult, GetFrictionlessCaptchaChallengeRequestBody, - type GetFrictionlessCaptchaResponse, GetPowCaptchaChallengeRequestBody, type GetPowCaptchaChallengeRequestBodyTypeOutput, type GetPowCaptchaResponse, type PowCaptchaSolutionResponse, SubmitPowCaptchaSolutionBody, type SubmitPowCaptchaSolutionBodyTypeOutput, - type TGetImageCaptchaChallengePathAndParams, } from "@prosopo/types"; -import { - FrictionlessToken, - FrictionlessTokenRecord, -} from "@prosopo/types-database"; import type { ProviderEnvironment } from "@prosopo/types-env"; -import { flatten, version } from "@prosopo/util"; +import { flatten } from "@prosopo/util"; import express, { type Router } from "express"; -import { v4 as uuidv4 } from "uuid"; import { getBotScore } from "../tasks/detection/getBotScore.js"; import { Tasks } from "../tasks/tasks.js"; import { getIPAddress } from "../util.js"; import { handleErrors } from "./errorHandler.js"; -const NO_IP_ADDRESS = "NO_IP_ADDRESS" as const; const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5; /** diff --git a/packages/provider/src/api/errorHandler.ts b/packages/provider/src/api/errorHandler.ts index b192942a99..f87ba4d978 100644 --- a/packages/provider/src/api/errorHandler.ts +++ b/packages/provider/src/api/errorHandler.ts @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // We need the unused params to make express recognise this function as an error handler -import { type ProsopoApiError, ProsopoBaseError } from "@prosopo/common"; -import { i18n as i18next } from "@prosopo/locale"; -import type { ApiJsonError } from "@prosopo/types"; +import { type ProsopoApiError, unwrapError } from "@prosopo/common"; import type { NextFunction, Request, Response } from "express"; -import { ZodError } from "zod"; +import type { ZodError } from "zod"; export const handleErrors = ( err: ProsopoApiError | SyntaxError | ZodError, @@ -31,36 +29,3 @@ export const handleErrors = ( response.send({ error: jsonError }); response.end(); }; - -export const unwrapError = (err: ProsopoApiError | SyntaxError | ZodError) => { - const code = "code" in err ? err.code : 400; - let message = err.message; - let jsonError: ApiJsonError = { code, message }; - let statusMessage = err.message; - jsonError.message = message; - // unwrap the errors to get the actual error message - while (err instanceof ProsopoBaseError && err.context) { - // base error will not have a translation key - jsonError.code = - err.context.translationKey || err.translationKey || jsonError.code; - jsonError.message = err.message; - if (err.context.error) { - err = err.context.error; - } else { - break; - } - } - - if (err instanceof ZodError) { - message = i18next.t("CAPTCHA.PARSE_ERROR"); - statusMessage = message; - if (typeof err.message === "object") { - jsonError = err.message; - } else { - jsonError.message = JSON.parse(err.message); - } - } - - jsonError.code = jsonError.code || code; - return { code, statusMessage, jsonError }; -}; diff --git a/packages/provider/src/tests/unit/api/errorHandler.unit.test.ts b/packages/provider/src/tests/unit/api/errorHandler.unit.test.ts index fe23c2c749..9d64596c3a 100644 --- a/packages/provider/src/tests/unit/api/errorHandler.unit.test.ts +++ b/packages/provider/src/tests/unit/api/errorHandler.unit.test.ts @@ -33,7 +33,10 @@ describe("handleErrors", () => { } as unknown as Response; const mockNext = vi.fn() as unknown as NextFunction; - const error = new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT"); + const error = new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT", { + context: { code: 400 }, + }); + console.log(error); handleErrors(error, mockRequest, mockResponse, mockNext); @@ -43,11 +46,12 @@ describe("handleErrors", () => { ); expect(mockResponse.send).toHaveBeenCalledWith({ error: { - code: "CONTRACT.INVALID_DATA_FORMAT", + code: 400, + key: "CONTRACT.INVALID_DATA_FORMAT", message: "Invalid data format", }, }); - expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.status).toHaveBeenCalledWith(400); expect(mockResponse.end).toHaveBeenCalled(); }); @@ -136,7 +140,8 @@ describe("handleErrors", () => { expect(mockResponse.status).toHaveBeenCalledWith(500); expect(mockResponse.send).toHaveBeenCalledWith({ error: { - code: "GENERAL.ENVIRONMENT_NOT_READY", + code: 500, + key: "GENERAL.ENVIRONMENT_NOT_READY", message: "Environment not ready", }, }); diff --git a/packages/types/src/provider/api.ts b/packages/types/src/provider/api.ts index f909c6050a..d72330facb 100644 --- a/packages/types/src/provider/api.ts +++ b/packages/types/src/provider/api.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import type { ApiJsonError } from "@prosopo/common"; import type { Address4, Address6 } from "ip-address"; import { type ZodDefault, @@ -230,11 +231,6 @@ export interface ProviderRegistered { status: "Registered" | "Unregistered"; } -export type ApiJsonError = { - message: string; - code: number; -}; - export interface ApiResponse { [ApiParams.status]: string; [ApiParams.error]?: ApiJsonError;