diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1fdbf63cbe90..77bd6ab8f5a1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -153,8 +153,10 @@ jobs: - name: Apply Prisma Migrations run: | - # pnpm prisma migrate deploy - pnpm db:migrate:dev + # @formbricks/database is already built by the "Build App" step above, + # so run the migration runner directly instead of db:migrate:dev + # (which would rebuild + re-generate the package unnecessarily). + pnpm --filter=@formbricks/database db:migrate:ci - name: Run Rate Limiter Load Tests run: | diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e95b418e8018..aa0f2d99c4f2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -129,7 +129,10 @@ jobs: - name: Apply schema + data migrations to the test database if: steps.harness.outputs.present == 'true' - run: pnpm db:migrate:dev + # @formbricks/database is already built by the "Build workspace package + # dependencies" step above, so run the migration runner directly instead + # of db:migrate:dev (which would rebuild + re-generate the package). + run: pnpm --filter=@formbricks/database db:migrate:ci shell: bash # Shared with apps/web/integration/global-setup.ts (the local harness applies the same file) so diff --git a/apps/web/app/api/v1/auth.test.ts b/apps/web/app/api/v1/auth.test.ts index 338c5dbff6eb..429f3748b636 100644 --- a/apps/web/app/api/v1/auth.test.ts +++ b/apps/web/app/api/v1/auth.test.ts @@ -278,43 +278,48 @@ describe("authenticateRequest", () => { describe("handleErrorResponse", () => { test("returns 401 notAuthenticated for 'NotAuthenticated' message", async () => { - const response = handleErrorResponse(new Error("NotAuthenticated")); + const { response } = handleErrorResponse(new Error("NotAuthenticated")); expect(response.status).toBe(401); const body = await response.json(); expect(body.code).toBe("not_authenticated"); }); test("returns 401 unauthorized for 'Unauthorized' message", async () => { - const response = handleErrorResponse(new Error("Unauthorized")); + const { response } = handleErrorResponse(new Error("Unauthorized")); expect(response.status).toBe(401); const body = await response.json(); expect(body.code).toBe("unauthorized"); }); test("returns 409 conflict for UniqueConstraintError", async () => { - const response = handleErrorResponse(new UniqueConstraintError("Action with name foo already exists")); + const { response } = handleErrorResponse(new UniqueConstraintError("Action with name foo already exists")); expect(response.status).toBe(409); const body = await response.json(); expect(body.code).toBe("conflict"); expect(body.message).toBe("Action with name foo already exists"); }); - test("returns 400 badRequest for DatabaseError", async () => { - const response = handleErrorResponse(new DatabaseError("db boom")); - expect(response.status).toBe(400); + test("returns a generic 500 for DatabaseError, threads the real error, and doesn't leak the message", async () => { + const error = new DatabaseError("db boom"); + const { response, error: reportedError } = handleErrorResponse(error); + // The real error must reach the wrapper's reportApiError (not a synthetic one), so 5xx errors + // from routes still on handleErrorResponse keep their Sentry signal. + expect(reportedError).toBe(error); + expect(response.status).toBe(500); const body = await response.json(); - expect(body.message).toBe("db boom"); + expect(body.message).toBe("Something went wrong. Please try again."); + expect(JSON.stringify(body)).not.toContain("db boom"); }); test("returns 400 badRequest for InvalidInputError", async () => { - const response = handleErrorResponse(new InvalidInputError("bad input")); + const { response } = handleErrorResponse(new InvalidInputError("bad input")); expect(response.status).toBe(400); const body = await response.json(); expect(body.message).toBe("bad input"); }); test("returns 404 notFound for ResourceNotFoundError", async () => { - const response = handleErrorResponse(new ResourceNotFoundError("Survey", "id-1")); + const { response } = handleErrorResponse(new ResourceNotFoundError("Survey", "id-1")); expect(response.status).toBe(404); const body = await response.json(); expect(body).toEqual({ @@ -327,10 +332,10 @@ describe("handleErrorResponse", () => { }); }); - test("returns 500 internalServerError for unknown errors", async () => { - const response = handleErrorResponse(new Error("something else")); + test("returns a generic 500 for unknown errors", async () => { + const { response } = handleErrorResponse(new Error("something else")); expect(response.status).toBe(500); const body = await response.json(); - expect(body.message).toBe("Some error occurred"); + expect(body.message).toBe("Something went wrong. Please try again."); }); }); diff --git a/apps/web/app/api/v1/auth.ts b/apps/web/app/api/v1/auth.ts index d5440adca190..f10ef3b40222 100644 --- a/apps/web/app/api/v1/auth.ts +++ b/apps/web/app/api/v1/auth.ts @@ -1,11 +1,6 @@ import { NextRequest } from "next/server"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; -import { - DatabaseError, - InvalidInputError, - ResourceNotFoundError, - UniqueConstraintError, -} from "@formbricks/types/errors"; +import { type ApiErrorResult, handleApiError } from "@/app/lib/api/handle-api-error"; import { responses } from "@/app/lib/api/response"; import { type AuthenticateApiKeyOptions, @@ -19,22 +14,17 @@ export const authenticateRequest = async ( return await authenticateApiKeyFromHeaders(request.headers, options); }; -export const handleErrorResponse = (error: any): Response => { - switch (error.message) { +export const handleErrorResponse = (error: unknown): ApiErrorResult => { + const message = error instanceof Error ? error.message : undefined; + switch (message) { case "NotAuthenticated": - return responses.notAuthenticatedResponse(); + return { response: responses.notAuthenticatedResponse() }; case "Unauthorized": - return responses.unauthorizedResponse(); + return { response: responses.unauthorizedResponse() }; default: - if (error instanceof UniqueConstraintError) { - return responses.conflictResponse(error.message); - } - if (error instanceof ResourceNotFoundError) { - return responses.notFoundResponse(error.resourceType, error.resourceId); - } - if (error instanceof DatabaseError || error instanceof InvalidInputError) { - return responses.badRequestResponse(error.message); - } - return responses.internalServerErrorResponse("Some error occurred"); + // Delegate to the shared boundary and return its full { response, error } result — not just + // `.response` — so the wrapper's reportApiError receives the real 5xx error (e.g. a + // DatabaseError) instead of a synthetic one. Expected 4xx errors keep their status/message. + return handleApiError(error); } }; diff --git a/apps/web/app/api/v1/client/[workspaceId]/displays/[displayId]/response/route.ts b/apps/web/app/api/v1/client/[workspaceId]/displays/[displayId]/response/route.ts index 24c80740b6bd..ef44c95a2996 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/displays/[displayId]/response/route.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/displays/[displayId]/response/route.ts @@ -1,5 +1,5 @@ -import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { responses } from "@/app/lib/api/response"; import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging"; import { resolveClientApiIds } from "@/lib/utils/resolve-client-id"; @@ -11,7 +11,6 @@ export const OPTIONS = async (): Promise => { export const GET = withV1ApiWrapper({ handler: async ({ - req, props, }: THandlerParams<{ params: Promise<{ workspaceId: string; displayId: string }> }>) => { const params = await props.params; @@ -36,14 +35,7 @@ export const GET = withV1ApiWrapper({ response: responses.notFoundResponse("Display", params.displayId, true), }; } - - logger.error( - { error, url: req.url, workspaceId, displayId: params.displayId }, - "Error in GET /api/v1/client/[workspaceId]/displays/[displayId]/response" - ); - return { - response: responses.internalServerErrorResponse("Something went wrong. Please try again."), - }; + return handleApiError(error, { cors: true }); } }, }); diff --git a/apps/web/app/api/v1/client/[workspaceId]/displays/route.ts b/apps/web/app/api/v1/client/[workspaceId]/displays/route.ts index 31794802132a..486b35ef435b 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/displays/route.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/displays/route.ts @@ -1,6 +1,6 @@ -import { logger } from "@formbricks/logger"; import { ZDisplayCreateInput } from "@formbricks/types/displays"; import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; @@ -89,20 +89,17 @@ export const POST = withV1ApiWrapper({ } catch (error) { if (error instanceof ResourceNotFoundError) { return { - response: responses.notFoundResponse("Survey", inputValidation.data.surveyId), + response: responses.notFoundResponse("Survey", inputValidation.data.surveyId, true), }; - } else if (error instanceof InvalidInputError) { + } + if (error instanceof InvalidInputError) { return { response: responses.forbiddenResponse(error.message, true, { surveyId: inputValidation.data.surveyId, }), }; - } else { - logger.error({ error, url: req.url }, "Error in POST /api/v1/client/[workspaceId]/displays"); - return { - response: responses.internalServerErrorResponse("Something went wrong. Please try again."), - }; } + return handleApiError(error, { cors: true }); } }, }); diff --git a/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.test.ts b/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.test.ts index 258259143286..24b79cb337f6 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.test.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.test.ts @@ -13,7 +13,6 @@ const mocks = vi.hoisted(() => ({ getResponse: vi.fn(), getSurvey: vi.fn(), getValidatedResponseUpdateInput: vi.fn(), - loggerError: vi.fn(), resolveClientApiIds: vi.fn(), sendToPipeline: vi.fn(), updateResponseWithQuotaEvaluation: vi.fn(), @@ -23,12 +22,6 @@ const mocks = vi.hoisted(() => ({ verifyLinkSurveyPinToken: vi.fn(), })); -vi.mock("@formbricks/logger", () => ({ - logger: { - error: mocks.loggerError, - }, -})); - vi.mock("@/app/lib/pipelines", () => ({ sendToPipeline: mocks.sendToPipeline, })); @@ -220,26 +213,20 @@ describe("putResponseHandler", () => { }); }); - test("maps database lookup errors to a reported internal server error", async () => { + test("maps database lookup errors to an internal server error without leaking the message", async () => { const error = new DatabaseError("Lookup failed"); mocks.getResponse.mockRejectedValue(error); const result = await putResponseHandler(createHandlerParams()); + // The real error is threaded back so the wrapper logs/reports it; the client sees a generic message. expect(result.error).toBe(error); expect(result.response.status).toBe(500); await expect(result.response.json()).resolves.toEqual({ code: "internal_server_error", - message: "Lookup failed", + message: "Something went wrong. Please try again.", details: {}, }); - expect(mocks.loggerError).toHaveBeenCalledWith( - { - error, - url: createRequest().url, - }, - "Error in PUT /api/v1/client/[workspaceId]/responses/[responseId]" - ); }); test("maps unknown lookup failures to a generic internal server error", async () => { @@ -252,7 +239,7 @@ describe("putResponseHandler", () => { expect(result.response.status).toBe(500); await expect(result.response.json()).resolves.toEqual({ code: "internal_server_error", - message: "Unknown error occurred", + message: "Something went wrong. Please try again.", details: {}, }); }); @@ -470,7 +457,7 @@ describe("putResponseHandler", () => { }); }); - test("returns a reported internal server error for database update failures", async () => { + test("returns an internal server error for database update failures without leaking the message", async () => { const error = new DatabaseError("Update failed"); mocks.updateResponseWithQuotaEvaluation.mockRejectedValue(error); @@ -480,16 +467,9 @@ describe("putResponseHandler", () => { expect(result.response.status).toBe(500); await expect(result.response.json()).resolves.toEqual({ code: "internal_server_error", - message: "Update failed", + message: "Something went wrong. Please try again.", details: {}, }); - expect(mocks.loggerError).toHaveBeenCalledWith( - { - error, - url: createRequest().url, - }, - "Error in PUT /api/v1/client/[workspaceId]/responses/[responseId]" - ); }); test("returns a generic internal server error for unexpected update failures", async () => { @@ -502,16 +482,9 @@ describe("putResponseHandler", () => { expect(result.response.status).toBe(500); await expect(result.response.json()).resolves.toEqual({ code: "internal_server_error", - message: "Something went wrong", + message: "Something went wrong. Please try again.", details: {}, }); - expect(mocks.loggerError).toHaveBeenCalledWith( - { - error, - url: createRequest().url, - }, - "Error in PUT /api/v1/client/[workspaceId]/responses/[responseId]" - ); }); test("returns a success payload and emits a responseUpdated pipeline event", async () => { diff --git a/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.ts b/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.ts index 2ae43e64e11d..0b39a9e72fec 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/responses/[responseId]/lib/put-response-handler.ts @@ -1,13 +1,8 @@ -import { logger } from "@formbricks/logger"; -import { - DatabaseError, - InvalidInputError, - RESPONSE_ALREADY_FINISHED_ERROR_CODE, - ResourceNotFoundError, -} from "@formbricks/types/errors"; +import { RESPONSE_ALREADY_FINISHED_ERROR_CODE, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse, TResponseUpdateInput } from "@formbricks/types/responses"; import { TSurveyElement } from "@formbricks/types/surveys/elements"; import { TSurvey } from "@formbricks/types/surveys/types"; +import { type ApiErrorResult, handleApiError } from "@/app/lib/api/handle-api-error"; import { responses } from "@/app/lib/api/response"; import { THandlerParams } from "@/app/lib/api/with-api-logging"; import { sendToPipeline } from "@/app/lib/pipelines"; @@ -22,10 +17,7 @@ import { verifyLinkSurveyPinToken } from "@/modules/survey/link/lib/pin-token"; import { updateResponseWithQuotaEvaluation } from "./response"; import { getValidatedResponseUpdateInput } from "./validated-response-update-input"; -type TRouteResult = { - response: Response; - error?: unknown; -}; +type TRouteResult = ApiErrorResult; type TExistingResponseResult = { existingResponse: TResponse } | TRouteResult; type TSurveyResult = { survey: TSurvey } | TRouteResult; @@ -40,30 +32,13 @@ export type TPutRouteParams = { }>; }; -const handleDatabaseError = ( - error: Error, - url: string, - endpoint: string, - responseId: string -): TRouteResult => { +const handleDatabaseError = (error: unknown, responseId: string): TRouteResult => { + // A missing resource keeps its route-specific "Response" framing; everything else (DatabaseError, + // unexpected) is generic-ized here and reported server-side by the wrapper via handleApiError. if (error instanceof ResourceNotFoundError) { return { response: responses.notFoundResponse("Response", responseId, true) }; } - if (error instanceof InvalidInputError) { - return { response: responses.badRequestResponse(error.message, undefined, true) }; - } - if (error instanceof DatabaseError) { - logger.error({ error, url }, `Error in ${endpoint}`); - return { - response: responses.internalServerErrorResponse(error.message, true), - error, - }; - } - - return { - response: responses.internalServerErrorResponse("Unknown error occurred", true), - error, - }; + return handleApiError(error, { cors: true }); }; const validateResponse = ( @@ -94,7 +69,7 @@ const validateResponse = ( } }; -const getExistingResponse = async (req: Request, responseId: string): Promise => { +const getExistingResponse = async (responseId: string): Promise => { try { const existingResponse = await getResponse(responseId); @@ -102,31 +77,17 @@ const getExistingResponse = async (req: Request, responseId: string): Promise => { +const getSurveyForResponse = async (responseId: string, surveyId: string): Promise => { try { const survey = await getSurvey(surveyId); return survey ? { survey } : { response: responses.notFoundResponse("Survey", surveyId, true) }; } catch (error) { - return handleDatabaseError( - error instanceof Error ? error : new Error(String(error)), - req.url, - "PUT /api/v1/client/[workspaceId]/responses/[responseId]", - responseId - ); + return handleDatabaseError(error, responseId); } }; @@ -192,7 +153,6 @@ const validateUpdateRequest = ( }; const getUpdatedResponse = async ( - req: Request, responseId: string, responseUpdateInput: TResponseUpdateInput ): Promise => { @@ -201,36 +161,9 @@ const getUpdatedResponse = async ( return { updatedResponse }; } catch (error) { if (error instanceof ResourceNotFoundError) { - return { - response: responses.notFoundResponse("Response", responseId, true), - }; - } - if (error instanceof InvalidInputError) { - return { - response: responses.badRequestResponse(error.message), - }; + return { response: responses.notFoundResponse("Response", responseId, true) }; } - if (error instanceof DatabaseError) { - logger.error( - { error, url: req.url }, - "Error in PUT /api/v1/client/[workspaceId]/responses/[responseId]" - ); - return { - response: responses.internalServerErrorResponse(error.message), - error, - }; - } - - const unexpectedError = error instanceof Error ? error : new Error(String(error)); - - logger.error( - { error: unexpectedError, url: req.url }, - "Error in PUT /api/v1/client/[workspaceId]/responses/[responseId]" - ); - return { - response: responses.internalServerErrorResponse("Something went wrong"), - error: unexpectedError, - }; + return handleApiError(error, { cors: true }); } }; @@ -261,13 +194,13 @@ export const putResponseHandler = async ({ } const { responseUpdateInput } = validatedUpdateInput; - const existingResponseResult = await getExistingResponse(req, responseId); + const existingResponseResult = await getExistingResponse(responseId); if ("response" in existingResponseResult) { return existingResponseResult; } const { existingResponse } = existingResponseResult; - const surveyResult = await getSurveyForResponse(req, responseId, existingResponse.surveyId); + const surveyResult = await getSurveyForResponse(responseId, existingResponse.surveyId); if ("response" in surveyResult) { return surveyResult; } @@ -295,7 +228,7 @@ export const putResponseHandler = async ({ return validationResult; } - const updatedResponseResult = await getUpdatedResponse(req, responseId, responseUpdateInput); + const updatedResponseResult = await getUpdatedResponse(responseId, responseUpdateInput); if ("response" in updatedResponseResult) { return updatedResponseResult; } diff --git a/apps/web/app/api/v1/client/[workspaceId]/responses/route.ts b/apps/web/app/api/v1/client/[workspaceId]/responses/route.ts index 846d457ee277..2277c943e4af 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/responses/route.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/responses/route.ts @@ -1,11 +1,10 @@ import { headers } from "next/headers"; import { UAParser } from "ua-parser-js"; -import { logger } from "@formbricks/logger"; -import { InvalidInputError, UniqueConstraintError } from "@formbricks/types/errors"; import { TResponseWithQuotaFull } from "@formbricks/types/quota"; import { TResponseInput, ZResponseInput } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; import { validateSingleUseResponseInput } from "@/app/api/client/[workspaceId]/responses/lib/single-use"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; @@ -206,22 +205,7 @@ export const POST = withV1ApiWrapper({ meta, }); } catch (error) { - if (error instanceof InvalidInputError) { - return { - response: responses.badRequestResponse(error.message), - }; - } else if (error instanceof UniqueConstraintError) { - return { - response: responses.conflictResponse(error.message, undefined, true), - }; - } else { - logger.error({ error, url: req.url }, "Error creating response"); - return { - response: responses.internalServerErrorResponse( - error instanceof Error ? error.message : "Unknown error occurred" - ), - }; - } + return handleApiError(error, { cors: true }); } const { quotaFull, ...responseData } = response; diff --git a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts index 051e1c742fed..736ea89ebace 100644 --- a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts +++ b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts @@ -51,9 +51,7 @@ export const GET = withV1ApiWrapper({ response: responses.notFoundResponse("Action Class", params.actionClassId), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, }); @@ -138,9 +136,7 @@ export const PUT = withV1ApiWrapper({ response: responses.internalServerErrorResponse("Some error occurred while updating action"), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "updated", @@ -180,9 +176,7 @@ export const DELETE = withV1ApiWrapper({ response: responses.successResponse(deletedActionClass), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "deleted", diff --git a/apps/web/app/api/v1/management/action-classes/route.ts b/apps/web/app/api/v1/management/action-classes/route.ts index 6afcb1f200a6..5c772aeb8dee 100644 --- a/apps/web/app/api/v1/management/action-classes/route.ts +++ b/apps/web/app/api/v1/management/action-classes/route.ts @@ -1,7 +1,7 @@ import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; -import { DatabaseError, UniqueConstraintError } from "@formbricks/types/errors"; import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; @@ -27,12 +27,7 @@ export const GET = withV1ApiWrapper({ response: responses.successResponse(actionClasses), }; } catch (error) { - if (error instanceof DatabaseError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - throw error; + return handleApiError(error); } }, }); @@ -91,17 +86,7 @@ export const POST = withV1ApiWrapper({ response: responses.successResponse(actionClass), }; } catch (error) { - if (error instanceof UniqueConstraintError) { - return { - response: responses.conflictResponse(error.message), - }; - } - if (error instanceof DatabaseError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - throw error; + return handleApiError(error); } }, action: "created", diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 5e3ef946da5d..0933508d576f 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -62,9 +62,7 @@ export const GET = withV1ApiWrapper({ }), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, }); @@ -95,9 +93,7 @@ export const DELETE = withV1ApiWrapper({ response: responses.successResponse(deletedResponse), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "deleted", @@ -201,9 +197,7 @@ export const PUT = withV1ApiWrapper({ response: responses.successResponse({ ...updated, data: resolveStorageUrlsInObject(updated.data) }), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "updated", diff --git a/apps/web/app/api/v1/management/responses/route.ts b/apps/web/app/api/v1/management/responses/route.ts index 47b044fcc93c..2db18fbd3cb7 100644 --- a/apps/web/app/api/v1/management/responses/route.ts +++ b/apps/web/app/api/v1/management/responses/route.ts @@ -1,7 +1,7 @@ import { logger } from "@formbricks/logger"; -import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses"; import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; @@ -54,12 +54,7 @@ export const GET = withV1ApiWrapper({ ), }; } catch (error) { - if (error instanceof DatabaseError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - throw error; + return handleApiError(error); } }, }); @@ -165,54 +160,33 @@ export const POST = withV1ApiWrapper({ responseInput.updatedAt = responseInput.createdAt; } - try { - const response = await createResponseWithQuotaEvaluation(responseInput); - if (auditLog) { - auditLog.targetId = response.id; - auditLog.newObject = response; - } + const response = await createResponseWithQuotaEvaluation(responseInput); + if (auditLog) { + auditLog.targetId = response.id; + auditLog.newObject = response; + } + + await sendToPipeline({ + event: "responseCreated", + workspaceId: surveyResult.survey.workspaceId, + surveyId: response.surveyId, + response: response, + }); + if (response.finished) { await sendToPipeline({ - event: "responseCreated", + event: "responseFinished", workspaceId: surveyResult.survey.workspaceId, surveyId: response.surveyId, response: response, }); - - if (response.finished) { - await sendToPipeline({ - event: "responseFinished", - workspaceId: surveyResult.survey.workspaceId, - surveyId: response.surveyId, - response: response, - }); - } - - return { - response: responses.successResponse(response, true), - }; - } catch (error) { - logger.error({ error, url: req.url }, "Error in POST /api/v1/management/responses"); - - if (error instanceof InvalidInputError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - - return { - response: responses.internalServerErrorResponse( - error instanceof Error ? error.message : "Unknown error occurred" - ), - }; } + + return { + response: responses.successResponse(response, true), + }; } catch (error) { - if (error instanceof DatabaseError) { - return { - response: responses.badRequestResponse("An unexpected error occurred while creating the response"), - }; - } - throw error; + return handleApiError(error); } }, action: "created", diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts index d7fa933fca7c..911c3e8550ca 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts @@ -75,9 +75,7 @@ export const GET = withV1ApiWrapper({ }; } - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, }); @@ -112,9 +110,7 @@ export const DELETE = withV1ApiWrapper({ response: responses.successResponse(deletedSurvey), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "deleted", @@ -227,14 +223,10 @@ export const PUT = withV1ApiWrapper({ ), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "updated", diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts index 80728693db2d..20ba99d9aa42 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts @@ -77,9 +77,7 @@ export const GET = withV1ApiWrapper({ response: responses.successResponse(surveyLinks), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, }); diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts index b28cf9ab7301..24f245c391eb 100644 --- a/apps/web/app/api/v1/management/surveys/route.ts +++ b/apps/web/app/api/v1/management/surveys/route.ts @@ -1,5 +1,4 @@ import { logger } from "@formbricks/logger"; -import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; import { ZSurveyCreateInputWithWorkspaceId } from "@formbricks/types/surveys/types"; import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver"; import { checkFeaturePermissions } from "@/app/api/v1/management/surveys/lib/utils"; @@ -8,6 +7,7 @@ import { addLegacyProjectOverwritesToList, normaliseProjectOverwritesToWorkspace, } from "@/app/lib/api/api-backwards-compat"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body"; import { responses } from "@/app/lib/api/response"; import { @@ -51,12 +51,7 @@ export const GET = withV1ApiWrapper({ ), }; } catch (error) { - if (error instanceof DatabaseError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - throw error; + return handleApiError(error); } }, }); @@ -157,19 +152,9 @@ export const POST = withV1ApiWrapper({ }; } catch (error) { // Invalid survey media (e.g. an unsupported/unparseable choice imageUrl) surfaces as an - // InvalidInputError — it's bad user input, so return a 400 with the message instead of - // letting it fall through to an unhandled 500 (which would page Sentry). - if (error instanceof InvalidInputError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - if (error instanceof DatabaseError) { - return { - response: responses.internalServerErrorResponse(error.message), - }; - } - throw error; + // InvalidInputError, which handleApiError returns as a 400 with its message instead of a 500 + // that would page Sentry. DatabaseError and unexpected errors become a generic, reported 500. + return handleApiError(error); } }, action: "created", diff --git a/apps/web/app/api/v1/webhooks/route.ts b/apps/web/app/api/v1/webhooks/route.ts index fd22034c2a37..75822c7b3cd6 100644 --- a/apps/web/app/api/v1/webhooks/route.ts +++ b/apps/web/app/api/v1/webhooks/route.ts @@ -1,7 +1,7 @@ -import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver"; import { createWebhook, getWebhooks } from "@/app/api/v1/webhooks/lib/webhook"; import { ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks"; +import { handleApiError } from "@/app/lib/api/handle-api-error"; import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; @@ -23,12 +23,7 @@ export const GET = withV1ApiWrapper({ response: responses.successResponse(webhooks), }; } catch (error) { - if (error instanceof DatabaseError) { - return { - response: responses.internalServerErrorResponse(error.message), - }; - } - throw error; + return handleApiError(error); } }, }); @@ -93,17 +88,7 @@ export const POST = withV1ApiWrapper({ response: responses.successResponse(webhook), }; } catch (error) { - if (error instanceof InvalidInputError) { - return { - response: responses.badRequestResponse(error.message), - }; - } - if (error instanceof DatabaseError) { - return { - response: responses.internalServerErrorResponse(error.message), - }; - } - throw error; + return handleApiError(error); } }, action: "created", diff --git a/apps/web/app/lib/api/handle-api-error.test.ts b/apps/web/app/lib/api/handle-api-error.test.ts new file mode 100644 index 000000000000..75e2730b97d6 --- /dev/null +++ b/apps/web/app/lib/api/handle-api-error.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, test } from "vitest"; +import { + AuthenticationError, + AuthorizationError, + DatabaseError, + InvalidInputError, + OperationNotAllowedError, + QueryExecutionError, + ResourceNotFoundError, + TooManyRequestsError, + UniqueConstraintError, + UnknownError, + ValidationError, +} from "@formbricks/types/errors"; +import { GENERIC_API_ERROR_MESSAGE, handleApiError } from "./handle-api-error"; + +const CORS_HEADER = "Access-Control-Allow-Origin"; + +describe("handleApiError", () => { + describe("unexpected / server errors never leak the underlying message", () => { + const SECRET = "column workspace.secret_column violates constraint fk_secret"; + + test.each([ + ["DatabaseError", new DatabaseError(SECRET)], + ["QueryExecutionError", new QueryExecutionError(SECRET)], // expected-name but statusCode 500 + ["UnknownError", new UnknownError(SECRET)], + ["plain Error", new Error(SECRET)], + ])("%s -> generic 500 with the real error threaded back", async (_label, error) => { + const result = handleApiError(error); + + expect(result.response.status).toBe(500); + // The real error is threaded so the wrapper's reportApiError logs/Sentry-reports it. + expect(result.error).toBe(error); + + const body = await result.response.json(); + expect(body.code).toBe("internal_server_error"); + expect(body.message).toBe(GENERIC_API_ERROR_MESSAGE); + // The raw internal detail must never reach the client. + expect(JSON.stringify(body)).not.toContain(SECRET); + }); + + test("non-Error values still map to a generic 500 and are threaded back", async () => { + const result = handleApiError("boom"); + + expect(result.response.status).toBe(500); + expect(result.error).toBe("boom"); + + const body = await result.response.json(); + expect(body.message).toBe(GENERIC_API_ERROR_MESSAGE); + }); + }); + + describe("expected client (4xx) errors surface their domain message at the right status", () => { + test("InvalidInputError -> 400", async () => { + const result = handleApiError(new InvalidInputError("field 'email' is invalid")); + + expect(result.response.status).toBe(400); + // Expected 4xx errors are not server errors, so nothing is threaded back for reporting. + expect(result.error).toBeUndefined(); + + const body = await result.response.json(); + expect(body.code).toBe("bad_request"); + expect(body.message).toBe("field 'email' is invalid"); + }); + + test("ValidationError -> 400", async () => { + const result = handleApiError(new ValidationError("bad shape")); + expect(result.response.status).toBe(400); + expect((await result.response.json()).message).toBe("bad shape"); + }); + + test("UniqueConstraintError -> 409", async () => { + const result = handleApiError(new UniqueConstraintError("already exists")); + + expect(result.response.status).toBe(409); + expect(result.error).toBeUndefined(); + + const body = await result.response.json(); + expect(body.code).toBe("conflict"); + expect(body.message).toBe("already exists"); + }); + + test("ResourceNotFoundError -> 404 with resource details", async () => { + const result = handleApiError(new ResourceNotFoundError("Survey", "abc123")); + + expect(result.response.status).toBe(404); + + const body = await result.response.json(); + expect(body.code).toBe("not_found"); + expect(body.message).toBe("Survey not found"); + expect(body.details).toEqual({ resource_id: "abc123", resource_type: "Survey" }); + }); + + test.each([ + ["AuthorizationError", new AuthorizationError("no access")], + ["OperationNotAllowedError", new OperationNotAllowedError("no access")], + ])("%s -> 403", async (_label, error) => { + const result = handleApiError(error); + + expect(result.response.status).toBe(403); + + const body = await result.response.json(); + expect(body.code).toBe("forbidden"); + expect(body.message).toBe("no access"); + }); + + test("AuthenticationError -> 401", async () => { + const result = handleApiError(new AuthenticationError("nope")); + + expect(result.response.status).toBe(401); + expect((await result.response.json()).code).toBe("not_authenticated"); + }); + + test("TooManyRequestsError -> 429, message surfaced, not threaded for reporting", async () => { + const result = handleApiError(new TooManyRequestsError("Rate limit exceeded")); + + expect(result.response.status).toBe(429); + // Expected error → must NOT be threaded (no false Sentry report on the 4xx path). + expect(result.error).toBeUndefined(); + const body = await result.response.json(); + expect(body.code).toBe("too_many_requests"); + expect(body.message).toBe("Rate limit exceeded"); + }); + }); + + describe("cors option", () => { + test("omits CORS headers by default", () => { + const result = handleApiError(new DatabaseError("x")); + expect(result.response.headers.get(CORS_HEADER)).toBeNull(); + }); + + test("adds CORS headers on the 500 path when cors is true", () => { + const result = handleApiError(new DatabaseError("x"), { cors: true }); + expect(result.response.headers.get(CORS_HEADER)).toBe("*"); + }); + + test("adds CORS headers on the 4xx path when cors is true", () => { + const result = handleApiError(new InvalidInputError("x"), { cors: true }); + expect(result.response.headers.get(CORS_HEADER)).toBe("*"); + }); + }); +}); diff --git a/apps/web/app/lib/api/handle-api-error.ts b/apps/web/app/lib/api/handle-api-error.ts new file mode 100644 index 000000000000..e81869823435 --- /dev/null +++ b/apps/web/app/lib/api/handle-api-error.ts @@ -0,0 +1,77 @@ +import { ResourceNotFoundError, isExpectedError } from "@formbricks/types/errors"; +import { responses } from "@/app/lib/api/response"; + +/** + * Fixed, client-facing message returned for any unexpected or server-side (5xx) error. + * Intentionally generic so we never leak internal details (e.g. a `DatabaseError` wrapping a raw + * Prisma message with schema/column/constraint info). Matches the string used by the v2 routes. + */ +export const GENERIC_API_ERROR_MESSAGE = "Something went wrong. Please try again."; + +/** The `{ response, error? }` result shape v1 route handlers return to `withV1ApiWrapper`. */ +export interface ApiErrorResult { + response: Response; + /** The real caught error, threaded back so the wrapper's `reportApiError` logs/reports it (5xx path). */ + error?: unknown; +} + +interface HandleApiErrorOptions { + /** Pass `true` for public/client routes so the error response carries CORS headers. */ + cors?: boolean; +} + +/** + * Shared error boundary for v1 API handlers. Maps a caught error to the + * `{ response, error? }` result shape expected by `withV1ApiWrapper`. + * + * - Expected business errors with a **4xx** status surface their (domain-authored, safe) message + * at the correct status. + * - Everything else — `DatabaseError`, `QueryExecutionError`, `UnknownError`, any other 5xx, or a + * non-domain `Error` — returns the fixed {@link GENERIC_API_ERROR_MESSAGE} and threads the real + * `error` back so the wrapper's `reportApiError` logs it (and reports it to Sentry on 5xx). This + * guarantees no raw `error.message` is ever echoed on the unexpected/5xx path. + * + * A `TooManyRequestsError` (429) is mapped defensively: it is normally handled by the wrapper's + * rate limiter before a handler runs, but mapping it keeps this shared boundary from turning a + * stray 429 into a mis-statused, falsely Sentry-reported 500. `RequestBodyTooLargeError` (413) has + * no `statusCode` and is always caught at the `parseJsonBodyWithLimit` boundary, so it isn't mapped. + * + * @param error - The caught error (unknown). + * @param options.cors - Whether the response should include CORS headers (public/client routes). + */ +export const handleApiError = ( + error: unknown, + { cors = false }: HandleApiErrorOptions = {} +): ApiErrorResult => { + if (error instanceof Error && isExpectedError(error)) { + // ResourceNotFoundError is the only expected 404 and needs its resource fields, so handle it + // explicitly before the status switch. + if (error instanceof ResourceNotFoundError) { + return { response: responses.notFoundResponse(error.resourceType, error.resourceId, cors) }; + } + // `isExpectedError` also matches business errors that are technically 5xx (e.g. + // QueryExecutionError), so gate on the status: only 4xx messages are safe to surface. + const statusCode = (error as { statusCode?: number }).statusCode ?? 500; + if (statusCode < 500) { + switch (statusCode) { + case 400: + return { response: responses.badRequestResponse(error.message, undefined, cors) }; + case 401: + return { response: responses.notAuthenticatedResponse(cors) }; + case 403: + return { response: responses.forbiddenResponse(error.message, cors) }; + case 409: + return { response: responses.conflictResponse(error.message, undefined, cors) }; + case 429: + return { response: responses.tooManyRequestsResponse(error.message, cors) }; + } + } + } + + // Unexpected / 5xx: generic message to the client, real error threaded back for server-side + // reporting via the wrapper's reportApiError. + return { + response: responses.internalServerErrorResponse(GENERIC_API_ERROR_MESSAGE, cors), + error, + }; +}; diff --git a/apps/web/modules/ee/contacts/api/v1/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts b/apps/web/modules/ee/contacts/api/v1/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts index 6e56dd770a8b..da1e8d598da9 100644 --- a/apps/web/modules/ee/contacts/api/v1/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts +++ b/apps/web/modules/ee/contacts/api/v1/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts @@ -63,9 +63,7 @@ export const GET = withV1ApiWrapper({ response: responses.forbiddenResponse(error.message), }; } - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, }); @@ -109,9 +107,7 @@ export const DELETE = withV1ApiWrapper({ response: responses.successResponse(deletedContactAttributeKey), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "deleted", @@ -191,9 +187,7 @@ export const PUT = withV1ApiWrapper({ ), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "updated", diff --git a/apps/web/modules/ee/contacts/api/v1/management/contacts/[contactId]/route.ts b/apps/web/modules/ee/contacts/api/v1/management/contacts/[contactId]/route.ts index 2c27aa3900b4..2003c543f8f7 100644 --- a/apps/web/modules/ee/contacts/api/v1/management/contacts/[contactId]/route.ts +++ b/apps/web/modules/ee/contacts/api/v1/management/contacts/[contactId]/route.ts @@ -58,9 +58,7 @@ export const GET = withV1ApiWrapper({ response: responses.successResponse(result.contact), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, }); @@ -109,9 +107,7 @@ export const DELETE = withV1ApiWrapper({ response: responses.successResponse({ success: "Contact deleted successfully" }), }; } catch (error) { - return { - response: handleErrorResponse(error), - }; + return handleErrorResponse(error); } }, action: "deleted", diff --git a/apps/web/package.json b/apps/web/package.json index ed2e620dcdb6..6aaf98ee84f8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,8 +23,8 @@ "i18n:generate": "dotenv -e ../../.env -- npx lingo.dev@latest run && dotenv -e ../../.env -- npx lingo.dev@latest lockfile --force" }, "dependencies": { - "@better-auth/oauth-provider": "1.6.18", - "@better-auth/utils": "0.4.1", + "@better-auth/oauth-provider": "1.6.23", + "@better-auth/utils": "0.4.2", "@boxyhq/saml-jackson": "26.2.0", "@cubejs-client/core": "1.6.6", "@dnd-kit/core": "6.3.1", @@ -98,7 +98,7 @@ "@tanstack/react-table": "8.21.3", "ai": "6.0.177", "bcryptjs": "3.0.3", - "better-auth": "1.6.18", + "better-auth": "1.6.23", "boring-avatars": "2.0.4", "class-variance-authority": "0.7.1", "clsx": "2.1.1", diff --git a/packages/database/README.md b/packages/database/README.md index 52ca47a4fcf1..dd25ec4f72c1 100644 --- a/packages/database/README.md +++ b/packages/database/README.md @@ -54,6 +54,24 @@ packages/database/ - **Schema Migrations**: Continue to be tracked by Prisma in the `_prisma_migrations` table - **Data Migrations**: Are tracked in the new `DataMigration` table to avoid reapplying already executed migrations +### ⚠️ Data migrations must be no-ops on an empty database + +Data migrations exist to **transform pre-existing rows**. On a brand-new +(empty) database — every CI run and every fresh install — the migration runner +takes a fast path: it applies the whole schema in a single `prisma migrate +deploy` and marks all data migrations **applied without running them** +(baselining). This is safe only because there is no data to transform, and it +is necessary because some older data migrations reference columns/tables that +later schema migrations drop or rename, so they can no longer execute against +the final schema. + +Therefore, a data migration must **only** read and modify existing rows and do +nothing when its tables are empty (guard writes behind a `SELECT` of existing +rows, or keep them to `UPDATE`/`DELETE`/`INSERT ... SELECT` that naturally +affect zero rows on an empty table). **Never seed essential/default data from a +data migration** — it would be silently skipped on fresh installs. Seed base +data via the seed script (`src/seed.ts`, `pnpm db:seed`) instead. + ### Directory Structure Example ``` diff --git a/packages/database/migration/20260725070000_add_two_factor_lockout_fields/migration.sql b/packages/database/migration/20260725070000_add_two_factor_lockout_fields/migration.sql new file mode 100644 index 000000000000..3d31af413248 --- /dev/null +++ b/packages/database/migration/20260725070000_add_two_factor_lockout_fields/migration.sql @@ -0,0 +1,7 @@ +-- Better Auth 1.6.23 two-factor plugin brute-force lockout: adds the failedVerificationCount / +-- lockedUntil columns it reads and writes on every 2FA verification (see better-auth's +-- plugins/two-factor/schema.ts). Additive/nullable-or-defaulted, non-breaking. + +-- AlterTable +ALTER TABLE "public"."TwoFactor" ADD COLUMN "failedVerificationCount" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "lockedUntil" TIMESTAMP(3); diff --git a/packages/database/package.json b/packages/database/package.json index 2162261f23f8..3c23c31b1534 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -48,6 +48,7 @@ "dev": "vite build --watch", "db:migrate:deploy": "node ./dist/scripts/apply-migrations.js", "db:migrate:dev": "pnpm build && dotenv -e ../../.env -- sh -c \"pnpm generate && node ./dist/scripts/apply-migrations.js\"", + "db:migrate:ci": "dotenv -e ../../.env -- node ./dist/scripts/apply-migrations.js", "db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" node ./dist/scripts/create-saml-database.js", "db:create-saml-database:dev": "dotenv -e ../../.env -- node ./dist/scripts/create-saml-database.js", "db:push": "prisma db push --accept-data-loss", diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 3eed53724fc4..5c0dcd6ee6a7 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -884,12 +884,16 @@ model VerificationToken { /// User.backupCodes columns (kept until cutover). `secret` and `backupCodes` are encrypted /// at rest by Better Auth using BETTER_AUTH_SECRET. model TwoFactor { - id String @id @default(cuid()) - secret String - backupCodes String - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - verified Boolean @default(true) + id String @id @default(cuid()) + secret String + backupCodes String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + verified Boolean @default(true) + /// Better Auth 1.6.23+ two-factor brute-force lockout: consecutive failed verification attempts. + failedVerificationCount Int @default(0) + /// Better Auth 1.6.23+ two-factor brute-force lockout: set once failedVerificationCount trips the limit. + lockedUntil DateTime? @@unique([userId]) } diff --git a/packages/database/src/scripts/generate-data-migration.ts b/packages/database/src/scripts/generate-data-migration.ts index ee2ba5d27a55..80f65cab83ab 100644 --- a/packages/database/src/scripts/generate-data-migration.ts +++ b/packages/database/src/scripts/generate-data-migration.ts @@ -105,7 +105,13 @@ export const ${migrationName}: MigrationScript = { id: "${migrationId}", name: "${fullMigrationName}", run: async ({ tx }) => { - // Your migration script goes here + // Your migration script goes here. + // + // IMPORTANT: this must be a no-op on an empty database. Data migrations only + // transform pre-existing rows; on a fresh DB they are baselined (marked + // applied) WITHOUT running. Guard any write behind a SELECT of existing rows + // and never seed essential/default data here — seed via the seed script + // (packages/database/src/seed.ts) instead. See the database package README. } }; `; diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts index 9cb84af46141..cb7e7b69ea62 100644 --- a/packages/database/src/scripts/migration-runner.ts +++ b/packages/database/src/scripts/migration-runner.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { logger } from "@formbricks/logger"; -import { PrismaClient } from "../prisma"; +import { Prisma, PrismaClient } from "../prisma"; import { createPrismaPgAdapter } from "../prisma-adapter"; const __filename = fileURLToPath(import.meta.url); @@ -62,125 +62,320 @@ const resolvePrismaBin = async (): Promise => { } }; +// Postgres error codes that mean "this database has never been migrated": the +// database itself doesn't exist yet (`3D000`, when `migrate deploy` will create +// it), or it exists but the `_prisma_migrations` table hasn't been created yet +// (`42P01`). The startup lookups below run BEFORE the first `migrate deploy`, so +// on a truly fresh target they hit one of these — we treat both as "nothing +// applied". Depending on the Prisma driver adapter the code surfaces in +// different places: directly on the error's `code` (raw pg), nested under +// `meta.driverAdapterError.cause` (Prisma wraps it as a generic `P2010`), or — +// as a last resort — in the message. We check all three so a fresh DB is +// reliably recognised. +const PG_UNDEFINED_TABLE = "42P01"; +const PG_INVALID_CATALOG_NAME = "3D000"; +const FRESH_DATABASE_PG_CODES = [PG_UNDEFINED_TABLE, PG_INVALID_CATALOG_NAME]; +const FRESH_DATABASE_ADAPTER_KINDS = ["TableDoesNotExist", "DatabaseDoesNotExist"]; + +const isFreshDatabaseError = (error: unknown): boolean => { + if (typeof error !== "object" || error === null) { + return false; + } + + const err = error as { + code?: unknown; + meta?: { driverAdapterError?: { cause?: { originalCode?: unknown; kind?: unknown } } }; + message?: unknown; + }; + + if (typeof err.code === "string" && FRESH_DATABASE_PG_CODES.includes(err.code)) { + return true; + } + + const cause = err.meta?.driverAdapterError?.cause; + if ( + (typeof cause?.originalCode === "string" && FRESH_DATABASE_PG_CODES.includes(cause.originalCode)) || + (typeof cause?.kind === "string" && FRESH_DATABASE_ADAPTER_KINDS.includes(cause.kind)) + ) { + return true; + } + + const message = err.message; + return typeof message === "string" && FRESH_DATABASE_PG_CODES.some((code) => message.includes(code)); +}; + +// Load the set of schema migrations already recorded as fully applied in +// `_prisma_migrations`. We only ever read this once, before running anything: +// `prisma migrate deploy` is idempotent, so a migration missing from this +// snapshot simply gets (re)deployed, and re-applying an already-applied folder +// is a no-op. The `finished_at IS NOT NULL` predicate excludes failed/partial +// migrations so their batch is redeployed rather than silently skipped. +const loadAppliedSchemaMigrations = async (): Promise> => { + try { + const rows: { migration_name: string }[] = await prisma.$queryRaw` + SELECT migration_name + FROM _prisma_migrations + WHERE finished_at IS NOT NULL + `; + return new Set(rows.map((row) => row.migration_name)); + } catch (error: unknown) { + // Fresh database: the DB doesn't exist yet, or exists without a + // `_prisma_migrations` table. Treat as "nothing applied" and let the first + // `migrate deploy` create the database/table. Any other error is a real + // fault (e.g. connectivity) and must not be swallowed — otherwise we'd + // misread it as an empty DB and needlessly redeploy every migration. + if (isFreshDatabaseError(error)) { + return new Set(); + } + + logger.error(error, "Failed to load applied schema migrations"); + throw error; + } +}; + +// Count data migrations already recorded in the DataMigration table. Returns 0 +// when the database or the table doesn't exist yet (fresh DB — the table is +// created by a schema migration). Used to decide whether the fresh-database +// fast path applies. +const countRecordedDataMigrations = async (): Promise => { + try { + const rows: { count: bigint }[] = + await prisma.$queryRaw`SELECT COUNT(*)::bigint AS count FROM "DataMigration"`; + return Number(rows[0]?.count ?? 0n); + } catch (error: unknown) { + if (isFreshDatabaseError(error)) { + return 0; + } + + logger.error(error, "Failed to count recorded data migrations"); + throw error; + } +}; + +// Record data migrations as applied WITHOUT running them. On a fresh database +// there is no data to transform, so every data migration is a no-op; several +// older ones also reference columns/tables dropped by later schema migrations, +// so they cannot even execute against the final schema. Baselining writes the +// exact end-state a successful `runDataMigration` leaves (status 'applied' + +// finished_at), so a later normal run sees them applied and skips. This is a +// single atomic INSERT: it commits all rows or none, so an interrupted fast +// path is safely resumed by the trigger in `runMigrations`. +const baselineDataMigrations = async (dataMigrations: MigrationScript[]): Promise => { + if (dataMigrations.length === 0) { + return; + } + + const finishedAt = new Date(); + const rows = dataMigrations.map( + (migration) => Prisma.sql`(${migration.id}, ${migration.name}, 'applied', ${finishedAt})` + ); + + await prisma.$executeRaw` + INSERT INTO "DataMigration" ("id", "name", "status", "finished_at") + VALUES ${Prisma.join(rows)} + ON CONFLICT ("id") DO NOTHING + `; + + logger.info( + `Baselined ${dataMigrations.length.toString()} data migration(s) as applied without running them` + ); +}; + const runMigrations = async (migrations: MigrationScript[]): Promise => { logger.info(`Starting migrations: ${migrations.length.toString()} to run`); const startTime = Date.now(); // packages/database/migration is the source of truth (checked in). We copy - // each migration into packages/database/migrations on demand for + // each schema migration into packages/database/migrations on demand for // `prisma migrate deploy`, then wipe between runs so stale or experimental // migrations from a previous local invocation can't influence this one. await fs.rm(PRISMA_MIGRATIONS_DIR, { recursive: true, force: true }); await fs.mkdir(PRISMA_MIGRATIONS_DIR, { recursive: true }); - for (let index = 0; index < migrations.length; index++) { - await runSingleMigration(migrations[index], index); + const appliedSchemaMigrations = await loadAppliedSchemaMigrations(); + + const schemaMigrations = migrations.filter((migration) => migration.type === "schema"); + const dataMigrations = migrations.filter((migration) => migration.type === "data"); + + // Fresh-database fast path. On a brand-new DB there is no data, so every data + // migration is a guaranteed no-op — apply the ENTIRE schema history in a + // single `migrate deploy` and baseline the data migrations without running + // them, instead of interleaving (which needs one deploy per contiguous schema + // segment). It triggers when no data migration has been recorded yet AND + // either the DB is untouched (no schema applied) or the full schema is already + // present. The second case makes the path resume-safe: if a previous fast run + // was interrupted between the schema deploy and the (atomic) baseline INSERT, + // this re-runs it — the deploy is a no-op and the baseline is idempotent — + // rather than falling through to the interleaved path, which would try to run + // data migrations against the final schema (some reference dropped columns and + // would fail). A normal partial upgrade always has recorded data migrations, + // so it never matches. Invariant: data migrations must be no-ops on an empty + // DB (see README) — seed essential data via the seed script, not a migration. + if (dataMigrations.length > 0) { + const recordedDataMigrations = await countRecordedDataMigrations(); + const allSchemaApplied = schemaMigrations.every((migration) => + appliedSchemaMigrations.has(migration.name) + ); + + if (recordedDataMigrations === 0 && (appliedSchemaMigrations.size === 0 || allSchemaApplied)) { + logger.info( + `Fresh database detected: applying ${schemaMigrations.length.toString()} schema migrations in a single batch and baselining ${dataMigrations.length.toString()} data migrations without running them` + ); + + // Applies all pending schema migrations in one `migrate deploy` (also + // creates the DataMigration table the baseline writes to). On resume, + // every schema migration is already applied so this is copy-only. + await runSchemaMigrationBatch(schemaMigrations, appliedSchemaMigrations); + await baselineDataMigrations(dataMigrations); + + const endTime = Date.now(); + logger.info(`All migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); + return; + } + } + + // Data and schema migrations are interleaved by timestamp and must run in + // that exact order. But `prisma migrate deploy` applies every pending + // migration in the migrations directory in one shot, so we can collapse a + // run of *consecutive* schema migrations into a single deploy call instead of + // spawning the Prisma CLI once per migration (which dominated runtime on + // empty DBs). We flush the accumulated schema batch right before each data + // migration — and once more at the end — which preserves ordering exactly: + // consecutive schema migrations are, by definition, not separated by a data + // migration, so batching them cannot reorder anything. + let schemaBatch: MigrationScript[] = []; + + const flushSchemaBatch = async (): Promise => { + if (schemaBatch.length === 0) { + return; + } + + await runSchemaMigrationBatch(schemaBatch, appliedSchemaMigrations); + schemaBatch = []; + }; + + for (const migration of migrations) { + if (migration.type === "schema") { + schemaBatch.push(migration); + } else { + await flushSchemaBatch(); + await runDataMigration(migration); + } } + await flushSchemaBatch(); + const endTime = Date.now(); logger.info(`All migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); }; -const runSingleMigration = async (migration: MigrationScript, index: number): Promise => { - if (migration.type === "data") { - let hasLock = false; - logger.info(`Running data migration: ${migration.name}`); - - try { - await prisma.$transaction( - async (tx) => { - // Check if migration has already been run - const existingMigration: { status: "pending" | "applied" | "failed" }[] | undefined = - await prisma.$queryRaw` +const runDataMigration = async (migration: MigrationScript): Promise => { + let hasLock = false; + logger.info(`Running data migration: ${migration.name}`); + + try { + await prisma.$transaction( + async (tx) => { + // All DataMigration bookkeeping runs on `tx` (not the outer `prisma` + // client) so the status transitions commit atomically with the changes + // migration.run makes through `tx`. If the transaction rolls back, the + // "applied" marker rolls back with it — a migration can never be + // recorded applied while its data changes are discarded. The failure + // path below intentionally stays on `prisma`, since it must persist + // after the transaction has already rolled back. + + // Check if migration has already been run + const existingMigration: { status: "pending" | "applied" | "failed" }[] | undefined = + await tx.$queryRaw` SELECT status FROM "DataMigration" WHERE id = ${migration.id} `; - if (existingMigration?.[0]?.status === "pending") { - logger.info(`Data migration ${migration.name} is pending.`); - logger.info("Either there is another migration which is currently running or this is an error."); - logger.info( - "If you are sure that there is no migration running, you need to manually resolve the issue." - ); - - throw new Error("Migration is pending. Please resolve the issue manually."); - } - - if (existingMigration?.[0]?.status === "applied") { - logger.info(`Data migration ${migration.name} already completed. Skipping...`); - return; - } - - if (existingMigration?.[0]?.status === "failed") { - logger.info(`Data migration ${migration.name} failed previously. Retrying...`); - } else { - // create a new data migration entry with pending status - await prisma.$executeRaw`INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'pending')`; - hasLock = true; - } - - if (migration.run) { - // Run the actual migration - await migration.run({ - prisma, - tx, - }); - - // Mark migration as applied - await prisma.$executeRaw` + if (existingMigration?.[0]?.status === "pending") { + logger.info(`Data migration ${migration.name} is pending.`); + logger.info("Either there is another migration which is currently running or this is an error."); + logger.info( + "If you are sure that there is no migration running, you need to manually resolve the issue." + ); + + throw new Error("Migration is pending. Please resolve the issue manually."); + } + + if (existingMigration?.[0]?.status === "applied") { + logger.info(`Data migration ${migration.name} already completed. Skipping...`); + return; + } + + if (existingMigration?.[0]?.status === "failed") { + logger.info(`Data migration ${migration.name} failed previously. Retrying...`); + } else { + // create a new data migration entry with pending status + await tx.$executeRaw`INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'pending')`; + hasLock = true; + } + + if (migration.run) { + // Run the actual migration + await migration.run({ + prisma, + tx, + }); + + // Mark migration as applied + await tx.$executeRaw` UPDATE "DataMigration" SET status = 'applied', finished_at = ${new Date()} WHERE id = ${migration.id}; `; - } + } - logger.info(`Data migration ${migration.name} completed successfully`); - }, - { timeout: TRANSACTION_TIMEOUT } - ); - } catch (error) { - // Record migration failure - logger.error(error, `Data migration ${migration.name} failed`); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- we need to check if the migration has a lock - if (hasLock) { - // Mark migration as failed - await prisma.$queryRaw` + logger.info(`Data migration ${migration.name} completed successfully`); + }, + { timeout: TRANSACTION_TIMEOUT } + ); + } catch (error) { + // Record migration failure + logger.error(error, `Data migration ${migration.name} failed`); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- we need to check if the migration has a lock + if (hasLock) { + // Mark migration as failed + await prisma.$queryRaw` INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'failed') ON CONFLICT (id) DO UPDATE SET status = 'failed'; `; - } - - throw error; } - } else { - try { - logger.info(`Running schema migration: ${migration.name}`); - - let copyOnly = false; - if (index > 0) { - const isApplied = await isSchemaMigrationApplied(migration.name, prisma); + throw error; + } +}; - if (isApplied) { - // schema migration is already applied, we can just copy the migration to the original migrations directory - copyOnly = true; - } - } +// Applies a contiguous run of schema migrations with a single +// `prisma migrate deploy` call. `prisma migrate deploy` scans the whole +// migrations directory, applies every pending migration in timestamp order and +// skips ones already recorded in `_prisma_migrations`, so one call per batch is +// equivalent to (but far cheaper than) one call per migration. +const runSchemaMigrationBatch = async ( + batch: MigrationScript[], + appliedSchemaMigrations: Set +): Promise => { + if (batch.length === 0) { + return; + } - const originalMigrationsDirExists = await fs - .access(PRISMA_MIGRATIONS_DIR) - .then(() => true) - .catch(() => false); + const batchNames = batch.map((migration) => migration.name); + logger.info(`Running schema migration batch (${batch.length.toString()}): ${batchNames.join(", ")}`); - if (!originalMigrationsDirExists) { - await fs.mkdir(PRISMA_MIGRATIONS_DIR, { recursive: true }); - } + try { + const sourceDirs = await fs.readdir(MIGRATIONS_DIR); - // Copy specific schema migration from temp migrations directory to original migrations directory - const migrationToCopy = await fs - .readdir(MIGRATIONS_DIR) - .then((files) => files.find((dir) => dir.includes(migration.name))); + // Always copy every folder in the batch into the scratch migrations dir — + // even the already-applied ones. A later batch's `migrate deploy` runs + // against this accumulated directory, and Prisma treats a migration recorded + // in `_prisma_migrations` but missing from the directory as a divergence + // error. So the directory must always mirror the applied history. + for (const migration of batch) { + const migrationToCopy = sourceDirs.find((dir) => dir.includes(migration.name)); if (!migrationToCopy) { logger.error(`Schema migration not found: ${migration.name}`); @@ -189,30 +384,34 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr const sourcePath = path.join(MIGRATIONS_DIR, migrationToCopy); const destPath = path.join(PRISMA_MIGRATIONS_DIR, migrationToCopy); - - // Copy migration folder await fs.cp(sourcePath, destPath, { recursive: true }); + } - if (copyOnly) { - logger.info(`Schema migration ${migration.name} copied to migrations directory`); - return; - } + // If every migration in this batch is already applied there is nothing to + // deploy — the copy above is enough to keep the directory consistent for + // later batches. This keeps steady-state runs (e.g. a fully-migrated + // production container restart) from spawning the Prisma CLI at all. + const allApplied = batch.every((migration) => appliedSchemaMigrations.has(migration.name)); - // Run Prisma migrate. Throws when migrate deploy fails. - // We pin DATABASE_URL on the child env to the same URL the in-process - // PrismaClient resolved above. prisma.config.mjs always reads - // env("DATABASE_URL"), so this is how MIGRATE_DATABASE_URL reaches the - // subprocess without leaking into the parent's env. - const prismaBin = await resolvePrismaBin(); - await execFileAsync(prismaBin, ["migrate", "deploy", "--config", PRISMA_CONFIG_PATH], { - cwd: REPO_ROOT_DIR, - env: { ...process.env, DATABASE_URL: migrationDatabaseUrl }, - }); - logger.info(`Successfully applied schema migration: ${migration.name}`); - } catch (err) { - logger.error(err, `Schema migration ${migration.name} failed`); - throw err; + if (allApplied) { + logger.info(`Schema migration batch already applied; copied ${batch.length.toString()} migration(s)`); + return; } + + // Run Prisma migrate. Throws when migrate deploy fails. + // We pin DATABASE_URL on the child env to the same URL the in-process + // PrismaClient resolved above. prisma.config.mjs always reads + // env("DATABASE_URL"), so this is how MIGRATE_DATABASE_URL reaches the + // subprocess without leaking into the parent's env. + const prismaBin = await resolvePrismaBin(); + await execFileAsync(prismaBin, ["migrate", "deploy", "--config", PRISMA_CONFIG_PATH], { + cwd: REPO_ROOT_DIR, + env: { ...process.env, DATABASE_URL: migrationDatabaseUrl }, + }); + logger.info(`Successfully applied schema migration batch (${batch.length.toString()})`); + } catch (err) { + logger.error(err, `Schema migration batch failed: ${batchNames.join(", ")}`); + throw err; } }; @@ -330,19 +529,3 @@ export async function applyMigrations(): Promise { await prisma.$disconnect(); } } - -async function isSchemaMigrationApplied(migrationName: string, prismaClient: PrismaClient): Promise { - try { - const applied: unknown[] = await prismaClient.$queryRaw` - SELECT 1 - FROM _prisma_migrations - WHERE migration_name = ${migrationName} - AND finished_at IS NOT NULL - LIMIT 1; - `; - return applied.length > 0; - } catch (error: unknown) { - logger.error(error, `Failed to check migration status`); - throw new Error(`Could not verify migration status: ${error as string}`); - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 989612f1e2ae..cc058177ad62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ overrides: node-forge: 1.4.0 ajv@6: 6.14.0 minimatch@10: 10.2.5 - postcss: 8.5.14 + postcss: 8.5.18 fast-xml-parser: 5.7.0 body-parser: 2.3.0 typeorm: 0.3.31 @@ -33,7 +33,7 @@ overrides: esbuild: 0.28.1 undici@7: 7.28.0 form-data@4: 4.0.6 - tar: 7.5.19 + tar: 7.5.21 js-yaml@4: 4.3.0 sharp: 0.35.0 engine.io@6: 6.6.7 @@ -43,6 +43,7 @@ overrides: fast-uri@3: 3.1.4 linkify-it: 5.0.2 '@opentelemetry/propagator-jaeger': 2.9.0 + valibot: 1.4.2 importers: @@ -155,11 +156,11 @@ importers: apps/web: dependencies: '@better-auth/oauth-provider': - specifier: 1.6.18 - version: 1.6.18(8fea9c40cddf92368b333fccf3e99f8e) + specifier: 1.6.23 + version: 1.6.23(3b985212d87c84e8e1ad73d025e624a2) '@better-auth/utils': - specifier: 0.4.1 - version: 0.4.1 + specifier: 0.4.2 + version: 0.4.2 '@boxyhq/saml-jackson': specifier: 26.2.0 version: 26.2.0(@types/node@25.4.0)(ioredis@5.8.1)(socks@2.8.7)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@25.4.0)(typescript@5.9.3)) @@ -351,13 +352,13 @@ importers: version: 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@sentry/nextjs': specifier: 10.43.0 - version: 10.43.0(@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)) + version: 10.43.0(@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)) '@stripe/stripe-js': specifier: 5.6.0 version: 5.6.0 '@t3-oss/env-nextjs': specifier: 0.13.11 - version: 0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6) + version: 0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6) '@tailwindcss/forms': specifier: 0.5.11 version: 0.5.11(tailwindcss@4.3.1) @@ -380,8 +381,8 @@ importers: specifier: 3.0.3 version: 3.0.3 better-auth: - specifier: 1.6.18 - version: 1.6.18(@opentelemetry/api@1.9.0)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.8.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))(mysql2@3.20.0(@types/node@25.4.0))(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6) + specifier: 1.6.23 + version: 1.6.23(@opentelemetry/api@1.9.0)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.8.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))(mysql2@3.20.0(@types/node@25.4.0))(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6) boring-avatars: specifier: 2.0.4 version: 2.0.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -574,7 +575,7 @@ importers: version: 10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) '@storybook/react-vite': specifier: 10.3.6 - version: 10.3.6(esbuild@0.28.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)) + version: 10.3.6(esbuild@0.28.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)) '@testing-library/jest-dom': specifier: 6.9.1 version: 6.9.1 @@ -633,8 +634,8 @@ importers: specifier: 1.58.2 version: 1.58.2 postcss: - specifier: 8.5.14 - version: 8.5.14 + specifier: 8.5.18 + version: 8.5.18 resize-observer-polyfill: specifier: 1.5.1 version: 1.5.1 @@ -926,13 +927,13 @@ importers: version: 19.2.14 autoprefixer: specifier: 10.4.27 - version: 10.4.27(postcss@8.5.14) + version: 10.4.27(postcss@8.5.18) clsx: specifier: 2.1.1 version: 2.1.1 postcss: - specifier: 8.5.14 - version: 8.5.14 + specifier: 8.5.18 + version: 8.5.18 tailwind-merge: specifier: 3.5.0 version: 3.5.0 @@ -1280,7 +1281,7 @@ importers: version: 19.2.14 autoprefixer: specifier: 10.4.27 - version: 10.4.27(postcss@8.5.14) + version: 10.4.27(postcss@8.5.18) concurrently: specifier: 9.2.1 version: 9.2.1 @@ -1297,8 +1298,8 @@ importers: specifier: 20.8.9 version: 20.8.9 postcss: - specifier: 8.5.14 - version: 8.5.14 + specifier: 8.5.18 + version: 8.5.18 rollup-plugin-visualizer: specifier: 7.0.1 version: 7.0.1(rolldown@1.0.3)(rollup@4.59.0) @@ -1349,8 +1350,8 @@ importers: specifier: workspace:* version: link:../config-eslint postcss: - specifier: 8.5.14 - version: 8.5.14 + specifier: 8.5.18 + version: 8.5.18 typescript: specifier: 5.9.3 version: 5.9.3 @@ -2018,14 +2019,14 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@better-auth/core@1.6.18': - resolution: {integrity: sha512-mlPZBKHY6gZdJtuY6LiuKjN2r8AWu0LCss7CEnI0xMbE9D7Sw6WofwPPwiMm+0Hi0QBKIaZGYRmR7/WasbOs+Q==} + '@better-auth/core@1.6.23': + resolution: {integrity: sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==} peerDependencies: - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@cloudflare/workers-types': '>=4' '@opentelemetry/api': ^1.9.0 - better-call: 1.3.6 + better-call: 1.3.7 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 @@ -2035,56 +2036,56 @@ packages: '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.6.18': - resolution: {integrity: sha512-VFcW2YMLZt0YAD0hy6BcMkjPh4Rh9khV71jnV2uL3udqz599Z1P4PiuWO6XZLELTvb/bbAUnxS5ZnyXQwFDAPQ==} + '@better-auth/drizzle-adapter@1.6.23': + resolution: {integrity: sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 drizzle-orm: ^0.45.2 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.6.18': - resolution: {integrity: sha512-7Q9+LYWiVD8t3nrxN/fPqZOgUQFcVh+KUo95CDj2+Hfnf4GCb6/ZsQrEnBXBgplwBQbUOYN58Lp/uevb6ugwFQ==} + '@better-auth/kysely-adapter@1.6.23': + resolution: {integrity: sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.6.18': - resolution: {integrity: sha512-jHulNRsA0OGKAACmsCNfAbH2z0NEoLQCLdZTU6Pvqn/Cd+xh7aFz0oys69KyKZp2r30QUjuEG9FrrqS8ASWDiw==} + '@better-auth/memory-adapter@1.6.23': + resolution: {integrity: sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.18': - resolution: {integrity: sha512-A6aD3sKsptQl5sBq2wFoLGckEIM5Pa93T//R1DR+erAZOMejZGyaqYaPXWwRk7ZejFOyrftMm98OP0IJsz52gg==} + '@better-auth/mongo-adapter@1.6.23': + resolution: {integrity: sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/oauth-provider@1.6.18': - resolution: {integrity: sha512-Ae6d9btZQoqXAkXqYpxagcf6u4Q3oT5OkBy/RTw1Uox5s+pqodl5EQYWXfHjiuLfO/NwfjGDxeD3j5cTIkCfCg==} + '@better-auth/oauth-provider@1.6.23': + resolution: {integrity: sha512-1sDN+N4Sztmpk8ziCU3MXicxOTfvYoHvHvhJMQ7PSfr+pLXnYN+dJFI9S3zBRwstmTeJx/OhRIZWrwFJ0TgBnA==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 - better-auth: ^1.6.18 - better-call: 1.3.6 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + better-auth: ^1.6.23 + better-call: 1.3.7 - '@better-auth/prisma-adapter@1.6.18': - resolution: {integrity: sha512-9hJrDYDqGpb53o8BUuB8uunkkeY+nACzYpZr06R8+22zTFS/NL/jLwgUlK3Xf+1VjhBoXAoEBKJ3ghVUGu6jFw==} + '@better-auth/prisma-adapter@1.6.23': + resolution: {integrity: sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: @@ -2093,19 +2094,25 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.6.18': - resolution: {integrity: sha512-C/sWZzDAFrtb8bFs+yLHLYYfk1Yffw2GnKXerxTGO3rHI7qV2Ktf359nAf3IdZr+cC/404fFk4hkDCnQiGCkZA==} + '@better-auth/telemetry@1.6.23': + resolution: {integrity: sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==} peerDependencies: - '@better-auth/core': ^1.6.18 - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 + '@better-auth/core': ^1.6.23 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@better-auth/utils@0.4.1': resolution: {integrity: sha512-SZBPRPF3z0nBvE5ygOkxae35wnnXPRShmqFo78S+qslLeFoPu/pMgnXAuNKFMMybac3tiLaVg1e3MQW5MC+1iA==} + '@better-auth/utils@0.4.2': + resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} + '@better-fetch/fetch@1.3.0': resolution: {integrity: sha512-Lgkl18IrUURFqa1nE38GNDWXf7XGzfxXQDCE/alCQV0yZ98YrPGtlmW61ch1T4YRt3lxrSAGA+Ft73FzuWWb3A==} + '@better-fetch/fetch@1.3.1': + resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@boxyhq/error-code-mnemonic@0.1.1': resolution: {integrity: sha512-NmO111OG8GQDE8W/+uSREb67YSqnY2N/tHykGeFoIZc9Leher+lW+jN4U1OXzlc66hwB8yO7WRu2cbYsAKsi9g==} @@ -5702,7 +5709,7 @@ packages: peerDependencies: arktype: ^2.1.0 typescript: '>=5.0.0' - valibot: ^1.0.0-beta.7 || ^1.0.0 + valibot: 1.4.2 zod: ^3.24.0 || ^4.0.0 peerDependenciesMeta: arktype: @@ -5719,7 +5726,7 @@ packages: peerDependencies: arktype: ^2.1.0 typescript: '>=5.0.0' - valibot: ^1.0.0-beta.7 || ^1.0.0 + valibot: 1.4.2 zod: ^3.24.0 || ^4.0.0 peerDependenciesMeta: arktype: @@ -7009,7 +7016,7 @@ packages: engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: - postcss: 8.5.14 + postcss: 8.5.18 available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} @@ -7071,8 +7078,8 @@ packages: peerDependencies: ajv: 6.14.0 - better-auth@1.6.18: - resolution: {integrity: sha512-6jONqD0gNjE0S6ehhSZhZ7We0rBwjAzbqnQMJQg/zY0387M+agak7VsOAuE78r7Z0Qv7C/KBRKhHSuyJDQ4iLw==} + better-auth@1.6.23: + resolution: {integrity: sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -7133,8 +7140,8 @@ packages: vue: optional: true - better-call@1.3.6: - resolution: {integrity: sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA==} + better-call@1.3.7: + resolution: {integrity: sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -9851,8 +9858,8 @@ packages: react: '*' react-dom: '*' - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -10361,19 +10368,19 @@ packages: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: - postcss: 8.5.14 + postcss: 8.5.18 postcss-js@4.1.0: resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: - postcss: 8.5.14 + postcss: 8.5.18 postcss-load-config@4.0.2: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} peerDependencies: - postcss: 8.5.14 + postcss: 8.5.18 ts-node: '>=9.0.0' peerDependenciesMeta: postcss: @@ -10385,7 +10392,7 @@ packages: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: - postcss: 8.5.14 + postcss: 8.5.18 postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} @@ -10398,8 +10405,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.14: - resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + postcss@8.5.18: + resolution: {integrity: sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -11659,8 +11666,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@7.5.19: - resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} + tar@7.5.21: + resolution: {integrity: sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==} engines: {node: '>=18'} tarn@3.0.2: @@ -12137,8 +12144,8 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -14091,13 +14098,13 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0)': + '@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0)': dependencies: - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@opentelemetry/semantic-conventions': 1.40.0 '@standard-schema/spec': 1.1.0 - better-call: 1.3.6(zod@4.3.6) + better-call: 1.3.7(zod@4.3.6) jose: 6.2.2 kysely: 0.29.2 nanostores: 1.3.0 @@ -14105,60 +14112,66 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 - '@better-auth/drizzle-adapter@1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)': + '@better-auth/drizzle-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 - '@better-auth/kysely-adapter@1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(kysely@0.29.2)': + '@better-auth/kysely-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2)': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.29.2 - '@better-auth/memory-adapter@1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)': + '@better-auth/memory-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))': + '@better-auth/mongo-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 optionalDependencies: mongodb: 7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7) - '@better-auth/oauth-provider@1.6.18(8fea9c40cddf92368b333fccf3e99f8e)': + '@better-auth/oauth-provider@1.6.23(3b985212d87c84e8e1ad73d025e624a2)': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 - better-auth: 1.6.18(@opentelemetry/api@1.9.0)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.8.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))(mysql2@3.20.0(@types/node@25.4.0))(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6) - better-call: 1.3.6(zod@4.3.6) + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + better-auth: 1.6.23(@opentelemetry/api@1.9.0)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.8.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))(mysql2@3.20.0(@types/node@25.4.0))(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6) + better-call: 1.3.7(zod@4.3.6) jose: 6.2.2 zod: 4.3.6 - '@better-auth/prisma-adapter@1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))': + '@better-auth/prisma-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 optionalDependencies: '@prisma/client': 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3) prisma: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - '@better-auth/telemetry@1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)': + '@better-auth/telemetry@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@better-auth/utils@0.4.1': dependencies: '@noble/hashes': 2.0.1 + '@better-auth/utils@0.4.2': + dependencies: + '@noble/hashes': 2.0.1 + '@better-fetch/fetch@1.3.0': {} + '@better-fetch/fetch@1.3.1': {} + '@boxyhq/error-code-mnemonic@0.1.1': {} '@boxyhq/metrics@0.2.12': @@ -16402,7 +16415,7 @@ snapshots: proper-lockfile: 4.1.2 remeda: 2.33.4 std-env: 3.10.0 - valibot: 1.2.0(typescript@5.9.3) + valibot: 1.4.2(typescript@5.9.3) zeptomatch: 2.1.0 transitivePeerDependencies: - typescript @@ -17559,7 +17572,7 @@ snapshots: '@sentry/core@10.43.0': {} - '@sentry/nextjs@10.43.0(@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14))': + '@sentry/nextjs@10.43.0(@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 @@ -17571,7 +17584,7 @@ snapshots: '@sentry/opentelemetry': 10.43.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) '@sentry/react': 10.43.0(react@19.2.6) '@sentry/vercel-edge': 10.43.0 - '@sentry/webpack-plugin': 5.1.1(encoding@0.1.13)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)) + '@sentry/webpack-plugin': 5.1.1(encoding@0.1.13)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)) next: 16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) rollup: 4.59.0 stacktrace-parser: 0.1.11 @@ -17668,11 +17681,11 @@ snapshots: '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@sentry/core': 10.43.0 - '@sentry/webpack-plugin@5.1.1(encoding@0.1.13)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14))': + '@sentry/webpack-plugin@5.1.1(encoding@0.1.13)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18))': dependencies: '@sentry/bundler-plugin-core': 5.1.1(encoding@0.1.13) uuid: 11.1.1 - webpack: 5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14) + webpack: 5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18) transitivePeerDependencies: - encoding - supports-color @@ -18297,9 +18310,9 @@ snapshots: dependencies: storybook: 10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@storybook/builder-vite@10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14))': + '@storybook/builder-vite@10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18))': dependencies: - '@storybook/csf-plugin': 10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)) + '@storybook/csf-plugin': 10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)) storybook: 10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) ts-dedent: 2.2.0 vite: 7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) @@ -18319,7 +18332,7 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14))': + '@storybook/csf-plugin@10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18))': dependencies: storybook: 10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) unplugin: 2.3.11 @@ -18327,7 +18340,7 @@ snapshots: esbuild: 0.28.1 rollup: 4.59.0 vite: 7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) - webpack: 5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14) + webpack: 5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18) '@storybook/csf-plugin@10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0))': dependencies: @@ -18352,11 +18365,11 @@ snapshots: react-dom: 19.2.6(react@19.2.6) storybook: 10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@storybook/react-vite@10.3.6(esbuild@0.28.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14))': + '@storybook/react-vite@10.3.6(esbuild@0.28.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)) + '@storybook/builder-vite': 10.3.6(esbuild@0.28.1)(rollup@4.59.0)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@7.3.5(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)) '@storybook/react': 10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@8.20.1)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -18418,20 +18431,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-core@0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6)': optionalDependencies: arktype: 2.1.29 typescript: 5.9.3 - valibot: 1.2.0(typescript@5.9.3) + valibot: 1.4.2(typescript@5.9.3) zod: 4.3.6 - '@t3-oss/env-nextjs@0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-nextjs@0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6)': dependencies: - '@t3-oss/env-core': 0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6) + '@t3-oss/env-core': 0.13.11(arktype@2.1.29)(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6) optionalDependencies: arktype: 2.1.29 typescript: 5.9.3 - valibot: 1.2.0(typescript@5.9.3) + valibot: 1.4.2(typescript@5.9.3) zod: 4.3.6 '@tabby_ai/hijri-converter@1.0.5': {} @@ -18568,7 +18581,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 - postcss: 8.5.14 + postcss: 8.5.18 tailwindcss: 4.2.4 '@tailwindcss/postcss@4.3.1': @@ -18576,7 +18589,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.3.1 '@tailwindcss/oxide': 4.3.1 - postcss: 8.5.14 + postcss: 8.5.18 tailwindcss: 4.3.1 '@tailwindcss/typography@0.5.19(tailwindcss@4.3.1)': @@ -19841,13 +19854,13 @@ snapshots: stubborn-fs: 2.0.0 when-exit: 2.1.5 - autoprefixer@10.4.27(postcss@8.5.14): + autoprefixer@10.4.27(postcss@8.5.18): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001776 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.14 + postcss: 8.5.18 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -19899,20 +19912,20 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - better-auth@1.6.18(@opentelemetry/api@1.9.0)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.8.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))(mysql2@3.20.0(@types/node@25.4.0))(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6): - dependencies: - '@better-auth/core': 1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) - '@better-auth/kysely-adapter': 1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(kysely@0.29.2) - '@better-auth/memory-adapter': 1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) - '@better-auth/mongo-adapter': 1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7)) - '@better-auth/prisma-adapter': 1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)) - '@better-auth/telemetry': 1.6.18(@better-auth/core@1.6.18(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.3.0) - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.3.0 + better-auth@1.6.23(@opentelemetry/api@1.9.0)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.8.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7))(mysql2@3.20.0(@types/node@25.4.0))(next@16.2.11(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6): + dependencies: + '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/drizzle-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/kysely-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2) + '@better-auth/memory-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(mongodb@7.1.0(@aws-sdk/credential-providers@3.1013.0)(socks@2.8.7)) + '@better-auth/prisma-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3))(typescript@5.9.3))(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(magicast@0.5.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)) + '@better-auth/telemetry': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@noble/ciphers': 2.2.0 '@noble/hashes': 2.0.1 - better-call: 1.3.6(zod@4.3.6) + better-call: 1.3.7(zod@4.3.6) defu: 6.1.7 jose: 6.2.2 kysely: 0.29.2 @@ -19933,7 +19946,7 @@ snapshots: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.3.6(zod@4.3.6): + better-call@1.3.7(zod@4.3.6): dependencies: '@better-auth/utils': 0.4.1 '@better-fetch/fetch': 1.3.0 @@ -20101,7 +20114,7 @@ snapshots: promise-inflight: 1.0.1 rimraf: 3.0.2 ssri: 8.0.1 - tar: 7.5.19 + tar: 7.5.21 unique-filename: 1.1.1 transitivePeerDependencies: - bluebird @@ -22910,7 +22923,7 @@ snapshots: stacktrace-js: 2.0.2 stylis: 4.3.6 - nanoid@3.3.11: {} + nanoid@3.3.16: {} nanostores@1.3.0: {} @@ -22944,7 +22957,7 @@ snapshots: '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.31 caniuse-lite: 1.0.30001793 - postcss: 8.5.14 + postcss: 8.5.18 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) styled-jsx: 5.1.6(react@19.2.6) @@ -23008,7 +23021,7 @@ snapshots: npmlog: 6.0.2 rimraf: 3.0.2 semver: 7.8.5 - tar: 7.5.19 + tar: 7.5.21 which: 2.0.2 transitivePeerDependencies: - bluebird @@ -23472,29 +23485,29 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.14): + postcss-import@15.1.0(postcss@8.5.18): dependencies: - postcss: 8.5.14 + postcss: 8.5.18 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.11 - postcss-js@4.1.0(postcss@8.5.14): + postcss-js@4.1.0(postcss@8.5.18): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.14 + postcss: 8.5.18 - postcss-load-config@4.0.2(postcss@8.5.14)(ts-node@10.9.2(@types/node@25.4.0)(typescript@5.9.3)): + postcss-load-config@4.0.2(postcss@8.5.18)(ts-node@10.9.2(@types/node@25.4.0)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 yaml: 2.9.0 optionalDependencies: - postcss: 8.5.14 + postcss: 8.5.18 ts-node: 10.9.2(@types/node@25.4.0)(typescript@5.9.3) - postcss-nested@6.2.0(postcss@8.5.14): + postcss-nested@6.2.0(postcss@8.5.18): dependencies: - postcss: 8.5.14 + postcss: 8.5.18 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.0.10: @@ -23509,9 +23522,9 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.14: + postcss@8.5.18: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -24333,7 +24346,7 @@ snapshots: is-plain-object: 5.0.0 launder: 1.7.1 parse-srcset: 1.0.2 - postcss: 8.5.14 + postcss: 8.5.18 sax@1.4.3: {} @@ -24689,7 +24702,7 @@ snapshots: bindings: 1.5.0 node-addon-api: 7.1.1 prebuild-install: 7.1.3 - tar: 7.5.19 + tar: 7.5.21 optionalDependencies: node-gyp: 8.4.1 transitivePeerDependencies: @@ -24973,11 +24986,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.14 - postcss-import: 15.1.0(postcss@8.5.14) - postcss-js: 4.1.0(postcss@8.5.14) - postcss-load-config: 4.0.2(postcss@8.5.14)(ts-node@10.9.2(@types/node@25.4.0)(typescript@5.9.3)) - postcss-nested: 6.2.0(postcss@8.5.14) + postcss: 8.5.18 + postcss-import: 15.1.0(postcss@8.5.18) + postcss-js: 4.1.0(postcss@8.5.18) + postcss-load-config: 4.0.2(postcss@8.5.18)(ts-node@10.9.2(@types/node@25.4.0)(typescript@5.9.3)) + postcss-nested: 6.2.0(postcss@8.5.18) postcss-selector-parser: 6.1.2 resolve: 1.22.11 sucrase: 3.35.1 @@ -25007,7 +25020,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar@7.5.19: + tar@7.5.21: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -25033,17 +25046,17 @@ snapshots: transitivePeerDependencies: - supports-color - terser-webpack-plugin@5.6.0(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)): + terser-webpack-plugin@5.6.0(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.47.1 - webpack: 5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14) + webpack: 5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18) optionalDependencies: esbuild: 0.28.1 lightningcss: 1.32.0 - postcss: 8.5.14 + postcss: 8.5.18 terser-webpack-plugin@5.6.0(esbuild@0.28.1)(lightningcss@1.32.0)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)): dependencies: @@ -25448,7 +25461,7 @@ snapshots: v8-compile-cache-lib@3.0.1: optional: true - valibot@1.2.0(typescript@5.9.3): + valibot@1.4.2(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -25520,7 +25533,7 @@ snapshots: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.14 + postcss: 8.5.18 rollup: 4.59.0 tinyglobby: 0.2.16 optionalDependencies: @@ -25536,7 +25549,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.14 + postcss: 8.5.18 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -25552,7 +25565,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.14 + postcss: 8.5.18 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -25738,7 +25751,7 @@ snapshots: - uglify-js optional: true - webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14): + webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -25762,7 +25775,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.14)) + terser-webpack-plugin: 5.6.0(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)(webpack@5.105.4(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.18)) watchpack: 2.5.1 webpack-sources: 3.4.1 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fd3093fa36de..cfd0051a9296 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,7 +54,10 @@ overrides: # Webpack / linting / schema validation chains. ajv@6: 6.14.0 minimatch@10: 10.2.5 - postcss: 8.5.14 + # GHSA-r28c-9q8g-f849: postcss <=8.5.17 auto-loads a previous sourceMappingURL + # without validating it stays within the project root, allowing arbitrary .map file + # disclosure via path traversal; patched in 8.5.18. + postcss: 8.5.18 # SDK and runtime transitive security pins. fast-xml-parser: 5.7.0 # load-bearing: <5.7.0 vulnerable (GHSA-gh4j-gqv2-49f6 + GHSA-jp2q-39xq-3w4g) # ENG-1942 / GHSA-v422-hmwv-36x6 (CVE-2026-12590): body-parser 2.0.0–2.2.x silently skips the request @@ -92,7 +95,10 @@ overrides: # ENG-1947 / GHSA-23hp-3jrh-7fpw (CVE-2026-59873): node-tar <=7.5.18 has no hard # upper bound on total decompressed bytes / entry count, so a small gzip bomb can # exhaust disk and CPU (DoS). Patched in 7.5.19. - tar: 7.5.19 + # GHSA-r292-9mhp-454m: node-tar <=7.5.20 has uncontrolled recursion in + # mapHas/filesFilter, allowing an uncatchable stack-overflow DoS via a crafted + # long-path tar entry during member selection; patched in 7.5.21. + tar: 7.5.21 # ENG-1527-batch / GHSA-mh29-5h37-fv8m + GHSA-h67p-54hq-rp68: js-yaml <4.1.2 quadratic-complexity # DoS via merge keys / repeated aliases (dev-only, via @redocly/cli). No 4.1.2 published; 4.2.0 is the # lowest patched and stays <5. Scoped to the 4.x line so any 3.x consumer is untouched. @@ -112,6 +118,12 @@ overrides: # the tree; each is pinned to its lowest patched release: 1.x→1.1.16, 2.x→2.1.2, 3.x–5.x→5.0.7. brace-expansion@1: 1.1.16 brace-expansion@2: 2.1.2 + # GHSA-mh99-v99m-4gvg: brace-expansion@5 <=5.0.7 DoS via unbounded expansion length causing + # an out-of-memory crash; patched in 5.0.8 (published 2026-07-23T11:39 UTC). NOT yet bumped: 5.0.8 is + # still inside the minimumReleaseAge (4320min/3-day) cooldown, which clears 2026-07-26T11:39 UTC. Only + # transitive dev-tooling path affected (@redocly/cli openapi tooling via minimatch), not a runtime web + # dependency. Bump this pin to 5.0.8 once the cooldown clears — do not add a minimumReleaseAgeExclude + # entry for this without going through the dependency-cooldown exception process first. brace-expansion@5: 5.0.7 # ENG-1994 / GHSA-4c8g-83qw-93j6 (CVE-2026-13676) + ENG-1988 / GHSA-v2hh-gcrm-f6hx (CVE-2026-16221): # fast-uri 3.x host confusion via failed IDN canonicalization and via literal-backslash authority @@ -124,6 +136,10 @@ overrides: # unhandled decodeURIComponent() error on a malformed header; patched in 2.9.0. Coerces the transitive # 1.26.0 line up to 2.9.0 (core is already pinned to 2.x above). "@opentelemetry/propagator-jaeger": 2.9.0 + # GHSA-5qjj-4xww-7phc: valibot <=1.4.1 record() issue paths can make flatten() throw for + # inherited Object property names (e.g. "constructor"). Pulled in transitively via prisma's dev-only + # tooling (@prisma/dev); patched in 1.4.2. + valibot: 1.4.2 autoInstallPeers: true linkWorkspacePackages: true @@ -154,21 +170,3 @@ minimumReleaseAgeExclude: # a fresh hub bump isn't blocked. Version-pinned scoped entries (e.g. '@formbricks/hub@0.9.0') are not # reliably matched by the exclude, so match by name. - '@formbricks/hub' - # Urgent CVE fix, approved via the dependency-cooldown exception process (see the - # "Dependency Cooldown Security Exceptions" page in the engineering handbook / Notion): - # next@16.2.11 fixes GHSA-6gpp-xcg3-4w24, GHSA-m99w-x7hq-7vfj, GHSA-89xv-2m56-2m9x, - # GHSA-p9j2-gv94-2wf4 (high) and GHSA-68g3-v927-f742, GHSA-4633-3j49-mh5q, - # GHSA-4c39-4ccg-62r3, GHSA-q8wf-6r8g-63ch, GHSA-955p-x3mx-jcvp (moderate). Published - # 2026-07-21, cooldown clears 2026-07-24T16:00 UTC — remove this entry once past that date. - # `next`'s platform-specific companion packages publish under the same version and need their - # own entries; pnpm's release-age check applies per npm package, not per dependency graph. - - next@16.2.11 - - '@next/env@16.2.11' - - '@next/swc-darwin-arm64@16.2.11' - - '@next/swc-darwin-x64@16.2.11' - - '@next/swc-linux-arm64-gnu@16.2.11' - - '@next/swc-linux-arm64-musl@16.2.11' - - '@next/swc-linux-x64-gnu@16.2.11' - - '@next/swc-linux-x64-musl@16.2.11' - - '@next/swc-win32-arm64-msvc@16.2.11' - - '@next/swc-win32-x64-msvc@16.2.11'