diff --git a/apps/bff/src/app.module.ts b/apps/bff/src/app.module.ts index 540ef21b..2e468e39 100644 --- a/apps/bff/src/app.module.ts +++ b/apps/bff/src/app.module.ts @@ -4,7 +4,6 @@ import { RouterModule } from "@nestjs/core"; import { ConfigModule } from "@nestjs/config"; import { ScheduleModule } from "@nestjs/schedule"; import { ZodSerializerInterceptor, ZodValidationPipe } from "nestjs-zod"; -import { TransformInterceptor } from "@bff/core/http/transform.interceptor.js"; // Configuration import { appConfig } from "@bff/core/config/app.config.js"; @@ -106,10 +105,6 @@ import { HealthModule } from "@bff/modules/health/health.module.js"; provide: APP_PIPE, useClass: ZodValidationPipe, }, - { - provide: APP_INTERCEPTOR, - useClass: TransformInterceptor, - }, { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor, diff --git a/apps/bff/src/app/bootstrap.ts b/apps/bff/src/app/bootstrap.ts index 2254f72b..88e040c4 100644 --- a/apps/bff/src/app/bootstrap.ts +++ b/apps/bff/src/app/bootstrap.ts @@ -125,7 +125,55 @@ export async function bootstrap(): Promise { const port = Number(configService.get("BFF_PORT", 4000)); - await app.listen(port, "0.0.0.0"); + // #region agent log + fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "bootstrap.ts:before-listen", + message: "About to call app.listen", + data: { port }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "D", + }), + }).catch(() => {}); + // #endregion + + try { + await app.listen(port, "0.0.0.0"); + // #region agent log + fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "bootstrap.ts:after-listen", + message: "app.listen completed", + data: { port }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "D", + }), + }).catch(() => {}); + // #endregion + } catch (listenError) { + const err = listenError as Error; + // #region agent log + fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "bootstrap.ts:listen-error", + message: "app.listen failed", + data: { error: err?.message, stack: err?.stack, name: err?.name }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "D", + }), + }).catch(() => {}); + // #endregion + throw listenError; + } // Enhanced startup information logger.log(`🚀 BFF API running on: http://localhost:${port}/api`); diff --git a/apps/bff/src/core/http/transform.interceptor.ts b/apps/bff/src/core/http/transform.interceptor.ts deleted file mode 100644 index 32a00072..00000000 --- a/apps/bff/src/core/http/transform.interceptor.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - Injectable, - type NestInterceptor, - type ExecutionContext, - type CallHandler, -} from "@nestjs/common"; -import { SetMetadata } from "@nestjs/common"; -import { Reflector } from "@nestjs/core"; -import { Observable, map } from "rxjs"; -import type { ApiSuccessResponse } from "@customer-portal/domain/common"; - -export const SKIP_SUCCESS_ENVELOPE_KEY = "bff:skip-success-envelope"; - -/** - * Opt-out decorator for endpoints that must not be wrapped in `{ success: true, data }`, - * e.g. SSE streams or file downloads. - */ -export const SkipSuccessEnvelope = () => SetMetadata(SKIP_SUCCESS_ENVELOPE_KEY, true); - -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object"; -} - -function isLikelyStream(value: unknown): boolean { - // Avoid wrapping Node streams (file downloads / SSE internals). - return isRecord(value) && typeof (value as { pipe?: unknown }).pipe === "function"; -} - -@Injectable() -export class TransformInterceptor implements NestInterceptor> { - constructor(private readonly reflector: Reflector) {} - - intercept(context: ExecutionContext, next: CallHandler): Observable> { - if (context.getType() !== "http") { - // Only wrap HTTP responses. - return next.handle() as unknown as Observable>; - } - - const skip = - this.reflector.getAllAndOverride(SKIP_SUCCESS_ENVELOPE_KEY, [ - context.getHandler(), - context.getClass(), - ]) ?? false; - - if (skip) { - return next.handle() as unknown as Observable>; - } - - const req = context.switchToHttp().getRequest<{ originalUrl?: string; url?: string }>(); - const url = req?.originalUrl ?? req?.url ?? ""; - - // Only enforce success envelopes on the public API surface under `/api`. - // Keep non-API endpoints (e.g. `/health`) untouched for operational tooling. - if (!url.startsWith("/api")) { - return next.handle() as unknown as Observable>; - } - - return next.handle().pipe( - map(data => { - // Keep already-wrapped responses as-is (ack/message/data variants). - if (isRecord(data) && "success" in data) { - return data as unknown as ApiSuccessResponse; - } - - // Avoid wrapping streams/buffers that are handled specially by Nest/Express. - if (isLikelyStream(data)) { - return data as unknown as ApiSuccessResponse; - } - - const normalized = (data === undefined ? null : data) as T; - return { success: true as const, data: normalized }; - }) - ); - } -} diff --git a/apps/bff/src/core/security/controllers/csrf.controller.ts b/apps/bff/src/core/security/controllers/csrf.controller.ts index 48da2324..f6c19b21 100644 --- a/apps/bff/src/core/security/controllers/csrf.controller.ts +++ b/apps/bff/src/core/security/controllers/csrf.controller.ts @@ -22,35 +22,83 @@ export class CsrfController { @Public() @Get("token") getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) { - const sessionId = this.extractSessionId(req) || undefined; - const userId = req.user?.id; + // #region agent log + fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "csrf.controller.ts:getCsrfToken-entry", + message: "CSRF token endpoint called", + data: {}, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "A", + }), + }).catch(() => {}); + // #endregion + try { + const sessionId = this.extractSessionId(req) || undefined; + const userId = req.user?.id; - // Generate new CSRF token - const tokenData = this.csrfService.generateToken(undefined, sessionId, userId); - const isProduction = this.configService.get("NODE_ENV") === "production"; - const cookieName = this.csrfService.getCookieName(); + // Generate new CSRF token + const tokenData = this.csrfService.generateToken(undefined, sessionId, userId); + const isProduction = this.configService.get("NODE_ENV") === "production"; + const cookieName = this.csrfService.getCookieName(); - // Set CSRF secret in secure cookie - res.cookie(cookieName, tokenData.secret, { - httpOnly: true, - secure: isProduction, - sameSite: "strict", - maxAge: this.csrfService.getTokenTtl(), - path: "/api", - }); + // Set CSRF secret in secure cookie + res.cookie(cookieName, tokenData.secret, { + httpOnly: true, + secure: isProduction, + sameSite: "strict", + maxAge: this.csrfService.getTokenTtl(), + path: "/api", + }); - this.logger.debug("CSRF token requested", { - userId, - sessionId, - userAgent: req.get("user-agent"), - ip: req.ip, - }); + this.logger.debug("CSRF token requested", { + userId, + sessionId, + userAgent: req.get("user-agent"), + ip: req.ip, + }); - return res.json({ - success: true, - token: tokenData.token, - expiresAt: tokenData.expiresAt.toISOString(), - }); + // #region agent log + fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "csrf.controller.ts:getCsrfToken-success", + message: "CSRF token generated successfully", + data: { hasToken: !!tokenData.token }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "A", + }), + }).catch(() => {}); + // #endregion + + return res.json({ + success: true, + token: tokenData.token, + expiresAt: tokenData.expiresAt.toISOString(), + }); + } catch (error) { + // #region agent log + const err = error as Error; + fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "csrf.controller.ts:getCsrfToken-error", + message: "CSRF token generation failed", + data: { error: err?.message, stack: err?.stack }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "A", + }), + }).catch(() => {}); + // #endregion + throw error; + } } @Public() diff --git a/apps/bff/src/infra/realtime/realtime.pubsub.ts b/apps/bff/src/infra/realtime/realtime.pubsub.ts index 3a5de007..5046896a 100644 --- a/apps/bff/src/infra/realtime/realtime.pubsub.ts +++ b/apps/bff/src/infra/realtime/realtime.pubsub.ts @@ -53,15 +53,22 @@ export class RealtimePubSubService implements OnModuleInit, OnModuleDestroy { } async onModuleDestroy(): Promise { - if (!this.subscriber) return; + // Capture the connection reference up-front and null out the field early. + // This makes shutdown idempotent and avoids races if the hook is invoked more than once. + const subscriber = this.subscriber; + if (!subscriber) return; + this.subscriber = null; + this.handlers.clear(); try { - await this.subscriber.unsubscribe(this.CHANNEL); - await this.subscriber.quit(); + await subscriber.unsubscribe(this.CHANNEL); + await subscriber.quit(); } catch { - this.subscriber.disconnect(); - } finally { - this.subscriber = null; - this.handlers.clear(); + // Best-effort immediate close if graceful shutdown fails. + try { + subscriber.disconnect(); + } catch { + // ignore + } } } diff --git a/apps/bff/src/main.ts b/apps/bff/src/main.ts index d0d58a55..d2d0ae4b 100644 --- a/apps/bff/src/main.ts +++ b/apps/bff/src/main.ts @@ -34,6 +34,17 @@ for (const signal of signals) { }); } +process.on("uncaughtException", error => { + logger.error(`Uncaught Exception: ${error.message}`, error.stack); + process.exit(1); +}); + +process.on("unhandledRejection", reason => { + const message = reason instanceof Error ? reason.message : String(reason); + const stack = reason instanceof Error ? reason.stack : undefined; + logger.error(`Unhandled Rejection: ${message}`, stack); +}); + void bootstrap() .then(startedApp => { app = startedApp; diff --git a/apps/bff/src/modules/notifications/notifications.controller.ts b/apps/bff/src/modules/notifications/notifications.controller.ts index 93779ce1..09f04dde 100644 --- a/apps/bff/src/modules/notifications/notifications.controller.ts +++ b/apps/bff/src/modules/notifications/notifications.controller.ts @@ -15,10 +15,7 @@ import { type NotificationListResponse, } from "@customer-portal/domain/notifications"; import { notificationQuerySchema } from "@customer-portal/domain/notifications"; -import { - apiSuccessAckResponseSchema, - type ApiSuccessAckResponse, -} from "@customer-portal/domain/common"; +import { actionAckResponseSchema, type ActionAckResponse } from "@customer-portal/domain/common"; import { createZodDto, ZodResponse } from "nestjs-zod"; class NotificationQueryDto extends createZodDto(notificationQuerySchema) {} @@ -27,7 +24,7 @@ class NotificationListResponseDto extends createZodDto(notificationListResponseS class NotificationUnreadCountResponseDto extends createZodDto( notificationUnreadCountResponseSchema ) {} -class ApiSuccessAckResponseDto extends createZodDto(apiSuccessAckResponseSchema) {} +class ActionAckResponseDto extends createZodDto(actionAckResponseSchema) {} @Controller("notifications") @UseGuards(RateLimitGuard) @@ -69,13 +66,13 @@ export class NotificationsController { */ @Post(":id/read") @RateLimit({ limit: 60, ttl: 60 }) - @ZodResponse({ description: "Mark as read", type: ApiSuccessAckResponseDto }) + @ZodResponse({ description: "Mark as read", type: ActionAckResponseDto }) async markAsRead( @Req() req: RequestWithUser, @Param() params: NotificationIdParamDto - ): Promise { + ): Promise { await this.notificationService.markAsRead(params.id, req.user.id); - return { success: true }; + return {}; } /** @@ -83,10 +80,10 @@ export class NotificationsController { */ @Post("read-all") @RateLimit({ limit: 10, ttl: 60 }) - @ZodResponse({ description: "Mark all as read", type: ApiSuccessAckResponseDto }) - async markAllAsRead(@Req() req: RequestWithUser): Promise { + @ZodResponse({ description: "Mark all as read", type: ActionAckResponseDto }) + async markAllAsRead(@Req() req: RequestWithUser): Promise { await this.notificationService.markAllAsRead(req.user.id); - return { success: true }; + return {}; } /** @@ -94,12 +91,12 @@ export class NotificationsController { */ @Post(":id/dismiss") @RateLimit({ limit: 60, ttl: 60 }) - @ZodResponse({ description: "Dismiss notification", type: ApiSuccessAckResponseDto }) + @ZodResponse({ description: "Dismiss notification", type: ActionAckResponseDto }) async dismiss( @Req() req: RequestWithUser, @Param() params: NotificationIdParamDto - ): Promise { + ): Promise { await this.notificationService.dismiss(params.id, req.user.id); - return { success: true }; + return {}; } } diff --git a/apps/bff/src/modules/orders/controllers/checkout.controller.ts b/apps/bff/src/modules/orders/controllers/checkout.controller.ts index 0e5c3102..af97ad0f 100644 --- a/apps/bff/src/modules/orders/controllers/checkout.controller.ts +++ b/apps/bff/src/modules/orders/controllers/checkout.controller.ts @@ -79,24 +79,33 @@ export class CheckoutController { orderType: body.orderType, }); - const cart = await this.checkoutService.buildCart( - body.orderType, - body.selections, - body.configuration, - req.user?.id - ); + try { + const cart = await this.checkoutService.buildCart( + body.orderType, + body.selections, + body.configuration, + req.user?.id + ); - const session = await this.checkoutSessions.createSession(body, cart); + const session = await this.checkoutSessions.createSession(body, cart); - return { - sessionId: session.sessionId, - expiresAt: session.expiresAt, - orderType: body.orderType, - cart: { - items: cart.items, - totals: cart.totals, - }, - }; + return { + sessionId: session.sessionId, + expiresAt: session.expiresAt, + orderType: body.orderType, + cart: { + items: cart.items, + totals: cart.totals, + }, + }; + } catch (error) { + this.logger.error("Failed to create checkout session", { + error: error instanceof Error ? error.message : String(error), + userId: req.user?.id, + orderType: body.orderType, + }); + throw error; + } } @Get("session/:sessionId") diff --git a/apps/bff/src/modules/orders/orders.controller.ts b/apps/bff/src/modules/orders/orders.controller.ts index 5316154a..d08dce36 100644 --- a/apps/bff/src/modules/orders/orders.controller.ts +++ b/apps/bff/src/modules/orders/orders.controller.ts @@ -31,7 +31,6 @@ import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js"; import { CheckoutService } from "./services/checkout.service.js"; import { CheckoutSessionService } from "./services/checkout-session.service.js"; -import { SkipSuccessEnvelope } from "@bff/core/http/transform.interceptor.js"; class CreateOrderRequestDto extends createZodDto(createOrderRequestSchema) {} class CheckoutSessionCreateOrderDto extends createZodDto(checkoutSessionCreateOrderRequestSchema) {} @@ -156,7 +155,6 @@ export class OrdersController { @Sse(":sfOrderId/events") @UseGuards(SalesforceReadThrottleGuard) - @SkipSuccessEnvelope() async streamOrderUpdates( @Request() req: RequestWithUser, @Param() params: SfOrderIdParamDto diff --git a/apps/bff/src/modules/orders/services/checkout.service.ts b/apps/bff/src/modules/orders/services/checkout.service.ts index d41e82a6..900ffd25 100644 --- a/apps/bff/src/modules/orders/services/checkout.service.ts +++ b/apps/bff/src/modules/orders/services/checkout.service.ts @@ -35,6 +35,29 @@ export class CheckoutService { private readonly vpnCatalogService: VpnServicesService ) {} + /** + * Produce a safe-to-log summary of user selections. + * Avoid logging PII/sensitive identifiers (EID, MNP reservation numbers, names, etc.). + */ + private summarizeSelectionsForLog(selections: OrderSelections): Record { + const addons = this.collectAddonRefs(selections); + const normalizeBool = (value?: string) => + value === "true" ? true : value === "false" ? false : undefined; + + return { + planSku: selections.planSku, + installationSku: selections.installationSku, + addonSku: selections.addonSku, + addonsCount: addons.length, + activationType: selections.activationType, + simType: selections.simType, + isMnp: normalizeBool(selections.isMnp), + hasEid: typeof selections.eid === "string" && selections.eid.trim().length > 0, + hasMnpNumber: + typeof selections.mnpNumber === "string" && selections.mnpNumber.trim().length > 0, + }; + } + /** * Build checkout cart from order type and selections */ @@ -44,7 +67,11 @@ export class CheckoutService { configuration?: OrderConfigurations, userId?: string ): Promise { - this.logger.log("Building checkout cart", { orderType, selections }); + this.logger.log("Building checkout cart", { + userId, + orderType, + selections: this.summarizeSelectionsForLog(selections), + }); try { const items: CheckoutItem[] = []; @@ -93,8 +120,9 @@ export class CheckoutService { } catch (error) { this.logger.error("Failed to build checkout cart", { error: getErrorMessage(error), + userId, orderType, - selections, + selections: this.summarizeSelectionsForLog(selections), }); throw error; } diff --git a/apps/bff/src/modules/realtime/realtime.controller.ts b/apps/bff/src/modules/realtime/realtime.controller.ts index 954480b5..44e86a5e 100644 --- a/apps/bff/src/modules/realtime/realtime.controller.ts +++ b/apps/bff/src/modules/realtime/realtime.controller.ts @@ -18,7 +18,6 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { Logger } from "nestjs-pino"; import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js"; import { RealtimeConnectionLimiterService } from "./realtime-connection-limiter.service.js"; -import { SkipSuccessEnvelope } from "@bff/core/http/transform.interceptor.js"; @Controller("events") export class RealtimeController { @@ -40,7 +39,6 @@ export class RealtimeController { @RateLimit({ limit: 30, ttl: 60 }) // protect against reconnect storms / refresh spam @Header("Cache-Control", "private, no-store") @Header("X-Accel-Buffering", "no") // nginx: disable response buffering for SSE - @SkipSuccessEnvelope() async stream(@Request() req: RequestWithUser): Promise> { if (!this.limiter.tryAcquire(req.user.id)) { throw new HttpException( diff --git a/apps/bff/src/modules/subscriptions/internet-management/internet.controller.ts b/apps/bff/src/modules/subscriptions/internet-management/internet.controller.ts index 07c8f6bd..92240f97 100644 --- a/apps/bff/src/modules/subscriptions/internet-management/internet.controller.ts +++ b/apps/bff/src/modules/subscriptions/internet-management/internet.controller.ts @@ -57,7 +57,6 @@ export class InternetController { ): Promise { await this.internetCancellationService.submitCancellation(req.user.id, params.id, body); return { - success: true, message: `Internet cancellation scheduled for end of ${body.cancellationMonth}`, }; } diff --git a/apps/bff/src/modules/subscriptions/sim-management/sim.controller.ts b/apps/bff/src/modules/subscriptions/sim-management/sim.controller.ts index 3964defa..77eb69cc 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/sim.controller.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/sim.controller.ts @@ -158,7 +158,7 @@ export class SimController { @Body() body: SimTopupRequestDto ): Promise { await this.simManagementService.topUpSim(req.user.id, params.id, body); - return { success: true, message: "SIM top-up completed successfully" }; + return { message: "SIM top-up completed successfully" }; } @Post(":id/sim/change-plan") @@ -170,7 +170,6 @@ export class SimController { ): Promise { const result = await this.simManagementService.changeSimPlan(req.user.id, params.id, body); return { - success: true, message: "SIM plan change completed successfully", ...result, }; @@ -184,7 +183,7 @@ export class SimController { @Body() body: SimCancelRequestDto ): Promise { await this.simManagementService.cancelSim(req.user.id, params.id, body); - return { success: true, message: "SIM cancellation completed successfully" }; + return { message: "SIM cancellation completed successfully" }; } @Post(":id/sim/reissue-esim") @@ -196,7 +195,7 @@ export class SimController { ): Promise { const parsedBody = simReissueEsimRequestSchema.parse(body as unknown); await this.simManagementService.reissueEsimProfile(req.user.id, params.id, parsedBody.newEid); - return { success: true, message: "eSIM profile reissue completed successfully" }; + return { message: "eSIM profile reissue completed successfully" }; } @Post(":id/sim/features") @@ -207,7 +206,7 @@ export class SimController { @Body() body: SimFeaturesRequestDto ): Promise { await this.simManagementService.updateSimFeatures(req.user.id, params.id, body); - return { success: true, message: "SIM features updated successfully" }; + return { message: "SIM features updated successfully" }; } // ==================== Enhanced SIM Management Endpoints ==================== @@ -232,7 +231,6 @@ export class SimController { ): Promise { const result = await this.simPlanService.changeSimPlanFull(req.user.id, params.id, body); return { - success: true, message: `SIM plan change scheduled for ${result.scheduledAt}`, ...result, }; @@ -264,7 +262,6 @@ export class SimController { ): Promise { await this.simCancellationService.cancelSimFull(req.user.id, params.id, body); return { - success: true, message: `SIM cancellation scheduled for end of ${body.cancellationMonth}`, }; } @@ -279,10 +276,9 @@ export class SimController { await this.esimManagementService.reissueSim(req.user.id, params.id, body); if (body.simType === "esim") { - return { success: true, message: "eSIM profile reissue request submitted" }; + return { message: "eSIM profile reissue request submitted" }; } else { return { - success: true, message: "Physical SIM reissue request submitted. You will be contacted shortly.", }; } diff --git a/apps/bff/src/modules/support/support.controller.ts b/apps/bff/src/modules/support/support.controller.ts index eb3ed12b..4e80f146 100644 --- a/apps/bff/src/modules/support/support.controller.ts +++ b/apps/bff/src/modules/support/support.controller.ts @@ -27,8 +27,8 @@ import { } from "@customer-portal/domain/support"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { hashEmailForLogs } from "./support.logging.js"; -import type { ApiSuccessMessageResponse } from "@customer-portal/domain/common"; -import { apiSuccessMessageResponseSchema } from "@customer-portal/domain/common"; +import type { ActionMessageResponse } from "@customer-portal/domain/common"; +import { actionMessageResponseSchema } from "@customer-portal/domain/common"; class SupportCaseFilterDto extends createZodDto(supportCaseFilterSchema) {} class CreateCaseRequestDto extends createZodDto(createCaseRequestSchema) {} @@ -36,7 +36,7 @@ class PublicContactRequestDto extends createZodDto(publicContactRequestSchema) { class SupportCaseListDto extends createZodDto(supportCaseListSchema) {} class SupportCaseDto extends createZodDto(supportCaseSchema) {} class CreateCaseResponseDto extends createZodDto(createCaseResponseSchema) {} -class ApiSuccessMessageResponseDto extends createZodDto(apiSuccessMessageResponseSchema) {} +class ActionMessageResponseDto extends createZodDto(actionMessageResponseSchema) {} @Controller("support") export class SupportController { @@ -84,16 +84,15 @@ export class SupportController { @RateLimit({ limit: 5, ttl: 300 }) // 5 requests per 5 minutes @ZodResponse({ description: "Public contact form submission", - type: ApiSuccessMessageResponseDto, + type: ActionMessageResponseDto, }) - async publicContact(@Body() body: PublicContactRequestDto): Promise { + async publicContact(@Body() body: PublicContactRequestDto): Promise { this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) }); try { await this.supportService.createPublicContactRequest(body); return { - success: true as const, message: "Your message has been received. We will get back to you within 24 hours.", }; } catch (error) { diff --git a/apps/portal/next-env.d.ts b/apps/portal/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/apps/portal/next-env.d.ts +++ b/apps/portal/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/portal/package.json b/apps/portal/package.json index 43d1559d..5ce72642 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -20,26 +20,26 @@ "dependencies": { "@customer-portal/domain": "workspace:*", "@heroicons/react": "^2.2.0", - "@tanstack/react-query": "^5.90.12", + "@tanstack/react-query": "^5.90.14", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.562.0", - "next": "16.0.10", - "react": "19.2.1", - "react-dom": "19.2.1", + "next": "16.1.1", + "react": "19.2.3", + "react-dom": "19.2.3", "tailwind-merge": "^3.4.0", "world-countries": "^5.1.0", - "zod": "catalog:", + "zod": "^4.2.1", "zustand": "^5.0.9" }, "devDependencies": { - "@next/bundle-analyzer": "^16.0.9", - "@tailwindcss/postcss": "^4.1.17", - "@tanstack/react-query-devtools": "^5.91.1", + "@next/bundle-analyzer": "^16.1.1", + "@tailwindcss/postcss": "^4.1.18", + "@tanstack/react-query-devtools": "^5.91.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", "typescript": "catalog:" } diff --git a/apps/portal/src/features/checkout/services/checkout.service.ts b/apps/portal/src/features/checkout/services/checkout.service.ts index 9c01b37d..2df7f19f 100644 --- a/apps/portal/src/features/checkout/services/checkout.service.ts +++ b/apps/portal/src/features/checkout/services/checkout.service.ts @@ -5,7 +5,6 @@ import type { OrderSelections, OrderTypeValue, } from "@customer-portal/domain/orders"; -import type { ApiSuccessResponse } from "@customer-portal/domain/common"; type CheckoutCartSummary = { items: CheckoutCart["items"]; totals: CheckoutCart["totals"] }; @@ -25,7 +24,7 @@ export const checkoutService = { selections: OrderSelections, configuration?: OrderConfigurations ): Promise { - const response = await apiClient.POST>("/api/checkout/cart", { + const response = await apiClient.POST("/api/checkout/cart", { body: { orderType, selections, @@ -33,11 +32,7 @@ export const checkoutService = { }, }); - const wrappedResponse = getDataOrThrow(response, "Failed to build checkout cart"); - if (!wrappedResponse.success) { - throw new Error("Failed to build checkout cart"); - } - return wrappedResponse.data; + return getDataOrThrow(response, "Failed to build checkout cart"); }, async createSession( @@ -45,33 +40,20 @@ export const checkoutService = { selections: OrderSelections, configuration?: OrderConfigurations ): Promise { - const response = await apiClient.POST>( - "/api/checkout/session", - { - body: { orderType, selections, configuration }, - } - ); - - const wrappedResponse = getDataOrThrow(response, "Failed to create checkout session"); - if (!wrappedResponse.success) { - throw new Error("Failed to create checkout session"); - } - return wrappedResponse.data; + const response = await apiClient.POST("/api/checkout/session", { + body: { orderType, selections, configuration }, + }); + return getDataOrThrow(response, "Failed to create checkout session"); }, async getSession(sessionId: string): Promise { - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/checkout/session/{sessionId}", { params: { path: { sessionId } }, } ); - - const wrappedResponse = getDataOrThrow(response, "Failed to load checkout session"); - if (!wrappedResponse.success) { - throw new Error("Failed to load checkout session"); - } - return wrappedResponse.data; + return getDataOrThrow(response, "Failed to load checkout session"); }, /** diff --git a/apps/portal/src/features/orders/services/orders.service.ts b/apps/portal/src/features/orders/services/orders.service.ts index 7704549a..47b51384 100644 --- a/apps/portal/src/features/orders/services/orders.service.ts +++ b/apps/portal/src/features/orders/services/orders.service.ts @@ -1,4 +1,4 @@ -import { apiClient } from "@/lib/api"; +import { apiClient, getDataOrThrow } from "@/lib/api"; import { log } from "@/lib/logger"; import { orderDetailsSchema, @@ -7,7 +7,12 @@ import { type OrderDetails, type OrderSummary, } from "@customer-portal/domain/orders"; -import { assertSuccess, type DomainApiResponse } from "@/lib/api/response-helpers"; + +interface CreateOrderResponse { + sfOrderId: string; + status: string; + message: string; +} async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: string }> { const body: CreateOrderRequest = { @@ -23,12 +28,9 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st }); try { - const response = await apiClient.POST("/api/orders", { body }); - - const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>( - response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }> - ); - return { sfOrderId: parsed.data.sfOrderId }; + const response = await apiClient.POST("/api/orders", { body }); + const data = getDataOrThrow(response, "Failed to create order"); + return { sfOrderId: data.sfOrderId }; } catch (error) { log.error("Order creation failed", error instanceof Error ? error : undefined, { orderType: body.orderType, @@ -41,14 +43,11 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st async function createOrderFromCheckoutSession( checkoutSessionId: string ): Promise<{ sfOrderId: string }> { - const response = await apiClient.POST("/api/orders/from-checkout-session", { + const response = await apiClient.POST("/api/orders/from-checkout-session", { body: { checkoutSessionId }, }); - - const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>( - response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }> - ); - return { sfOrderId: parsed.data.sfOrderId }; + const data = getDataOrThrow(response, "Failed to create order"); + return { sfOrderId: data.sfOrderId }; } async function getMyOrders(): Promise { diff --git a/apps/portal/src/features/services/services/services.service.ts b/apps/portal/src/features/services/services/services.service.ts index e76ada16..99ffd943 100644 --- a/apps/portal/src/features/services/services/services.service.ts +++ b/apps/portal/src/features/services/services/services.service.ts @@ -1,4 +1,4 @@ -import { apiClient, getDataOrDefault, getDataOrThrow } from "@/lib/api"; +import { apiClient, getDataOrThrow, getDataOrDefault } from "@/lib/api"; import { EMPTY_SIM_CATALOG, EMPTY_VPN_CATALOG, @@ -29,11 +29,7 @@ export const servicesService = { const response = await apiClient.GET( "/api/public/services/internet/plans" ); - const data = getDataOrThrow( - response, - "Failed to load internet services" - ); - return data; // BFF already validated + return getDataOrThrow(response, "Failed to load internet services"); }, /** @@ -45,14 +41,12 @@ export const servicesService = { async getPublicSimCatalog(): Promise { const response = await apiClient.GET("/api/public/services/sim/plans"); - const data = getDataOrDefault(response, EMPTY_SIM_CATALOG); - return data; // BFF already validated + return getDataOrDefault(response, EMPTY_SIM_CATALOG); }, async getPublicVpnCatalog(): Promise { const response = await apiClient.GET("/api/public/services/vpn/plans"); - const data = getDataOrDefault(response, EMPTY_VPN_CATALOG); - return data; // BFF already validated + return getDataOrDefault(response, EMPTY_VPN_CATALOG); }, // ============================================================================ @@ -63,30 +57,24 @@ export const servicesService = { const response = await apiClient.GET( "/api/account/services/internet/plans" ); - const data = getDataOrThrow( - response, - "Failed to load internet services" - ); - return data; // BFF already validated + return getDataOrThrow(response, "Failed to load internet services"); }, async getAccountSimCatalog(): Promise { const response = await apiClient.GET("/api/account/services/sim/plans"); - const data = getDataOrDefault(response, EMPTY_SIM_CATALOG); - return data; // BFF already validated + return getDataOrDefault(response, EMPTY_SIM_CATALOG); }, async getAccountVpnCatalog(): Promise { const response = await apiClient.GET("/api/account/services/vpn/plans"); - const data = getDataOrDefault(response, EMPTY_VPN_CATALOG); - return data; // BFF already validated + return getDataOrDefault(response, EMPTY_VPN_CATALOG); }, async getInternetInstallations(): Promise { const response = await apiClient.GET( "/api/services/internet/installations" ); - const data = getDataOrDefault(response, []); + const data = getDataOrDefault(response, []); return internetInstallationCatalogItemSchema.array().parse(data); }, @@ -94,7 +82,7 @@ export const servicesService = { const response = await apiClient.GET( "/api/services/internet/addons" ); - const data = getDataOrDefault(response, []); + const data = getDataOrDefault(response, []); return internetAddonCatalogItemSchema.array().parse(data); }, @@ -109,13 +97,13 @@ export const servicesService = { const response = await apiClient.GET( "/api/services/sim/activation-fees" ); - const data = getDataOrDefault(response, []); + const data = getDataOrDefault(response, []); return simActivationFeeCatalogItemSchema.array().parse(data); }, async getSimAddons(): Promise { const response = await apiClient.GET("/api/services/sim/addons"); - const data = getDataOrDefault(response, []); + const data = getDataOrDefault(response, []); return simCatalogProductSchema.array().parse(data); }, @@ -128,7 +116,7 @@ export const servicesService = { async getVpnActivationFees(): Promise { const response = await apiClient.GET("/api/services/vpn/activation-fees"); - const data = getDataOrDefault(response, []); + const data = getDataOrDefault(response, []); return vpnCatalogProductSchema.array().parse(data); }, diff --git a/apps/portal/src/features/subscriptions/services/internet-actions.service.ts b/apps/portal/src/features/subscriptions/services/internet-actions.service.ts index 8d61acb4..65cdd6e1 100644 --- a/apps/portal/src/features/subscriptions/services/internet-actions.service.ts +++ b/apps/portal/src/features/subscriptions/services/internet-actions.service.ts @@ -3,14 +3,14 @@ import type { InternetCancelRequest, InternetCancellationPreview, } from "@customer-portal/domain/subscriptions"; -import type { ApiSuccessResponse } from "@customer-portal/domain/common"; +import { internetCancellationPreviewSchema } from "@customer-portal/domain/subscriptions"; export const internetActionsService = { /** * Get cancellation preview (available months, service details) */ async getCancellationPreview(subscriptionId: string): Promise { - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/subscriptions/{subscriptionId}/internet/cancellation-preview", { params: { path: { subscriptionId } }, @@ -18,11 +18,7 @@ export const internetActionsService = { ); const payload = getDataOrThrow(response, "Failed to load cancellation information"); - if (!payload.data) { - throw new Error("Failed to load cancellation information"); - } - - return payload.data; + return internetCancellationPreviewSchema.parse(payload); }, /** diff --git a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts index 00baca40..ee393a82 100644 --- a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts +++ b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts @@ -1,7 +1,12 @@ -import { apiClient } from "@/lib/api"; -import type { ApiSuccessResponse } from "@customer-portal/domain/common"; +import { apiClient, getDataOrDefault } from "@/lib/api"; import { simInfoSchema, + simAvailablePlanArraySchema, + simCancellationPreviewSchema, + simDomesticCallHistoryResponseSchema, + simInternationalCallHistoryResponseSchema, + simSmsHistoryResponseSchema, + simHistoryAvailableMonthsSchema, type SimAvailablePlan, type SimInfo, type SimCancelFullRequest, @@ -81,23 +86,23 @@ export const simActionsService = { }, async getAvailablePlans(subscriptionId: string): Promise { - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/subscriptions/{subscriptionId}/sim/available-plans", - { - params: { path: { subscriptionId } }, - } + { params: { path: { subscriptionId } } } ); - return response.data?.data ?? []; + const data = getDataOrDefault(response, []); + return simAvailablePlanArraySchema.parse(data); }, async getCancellationPreview(subscriptionId: string): Promise { - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/subscriptions/{subscriptionId}/sim/cancellation-preview", { params: { path: { subscriptionId } }, } ); - return response.data?.data ?? null; + const data = getDataOrDefault(response, null); + return data ? simCancellationPreviewSchema.parse(data) : null; }, async reissueSim(subscriptionId: string, request: SimReissueFullRequest): Promise { @@ -120,13 +125,14 @@ export const simActionsService = { params.page = String(page); params.limit = String(limit); - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/subscriptions/{subscriptionId}/sim/call-history/domestic", { params: { path: { subscriptionId }, query: params }, } ); - return response.data?.data ?? null; + const data = getDataOrDefault(response, null); + return data ? simDomesticCallHistoryResponseSchema.parse(data) : null; }, async getInternationalCallHistory( @@ -140,13 +146,14 @@ export const simActionsService = { params.page = String(page); params.limit = String(limit); - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/subscriptions/{subscriptionId}/sim/call-history/international", { params: { path: { subscriptionId }, query: params }, } ); - return response.data?.data ?? null; + const data = getDataOrDefault(response, null); + return data ? simInternationalCallHistoryResponseSchema.parse(data) : null; }, async getSmsHistory( @@ -160,20 +167,21 @@ export const simActionsService = { params.page = String(page); params.limit = String(limit); - const response = await apiClient.GET>( + const response = await apiClient.GET( "/api/subscriptions/{subscriptionId}/sim/sms-history", { params: { path: { subscriptionId }, query: params }, } ); - return response.data?.data ?? null; + const data = getDataOrDefault(response, null); + return data ? simSmsHistoryResponseSchema.parse(data) : null; }, async getAvailableHistoryMonths(): Promise { - const response = await apiClient.GET>( - "/api/subscriptions/sim/call-history/available-months", - {} + const response = await apiClient.GET( + "/api/subscriptions/sim/call-history/available-months" ); - return response.data?.data ?? []; + const data = getDataOrDefault(response, []); + return simHistoryAvailableMonthsSchema.parse(data); }, }; diff --git a/apps/portal/src/lib/api/response-helpers.ts b/apps/portal/src/lib/api/response-helpers.ts index ff33092e..3ae754d4 100644 --- a/apps/portal/src/lib/api/response-helpers.ts +++ b/apps/portal/src/lib/api/response-helpers.ts @@ -1,18 +1,18 @@ -import { - apiErrorResponseSchema, - type ApiErrorResponse, - type ApiSuccessResponse, -} from "@customer-portal/domain/common"; -import type { ZodTypeAny, infer as ZodInfer } from "zod"; +import { apiErrorResponseSchema, type ApiErrorResponse } from "@customer-portal/domain/common"; /** * API Response Helper Types and Functions * - * Generic utilities for working with API responses + * Generic utilities for working with API responses. + * + * Note: Success responses from the BFF return data directly (no `{ success: true, data }` envelope). + * Error responses are returned as `{ success: false, error: { code, message, details? } }` and + * are surfaced via the API client error handler. */ /** - * Generic API response wrapper + * Generic API response wrapper from the client. + * After client unwrapping, `data` contains the actual payload (not the BFF envelope). */ export type ApiResponse = { data?: T; @@ -20,8 +20,8 @@ export type ApiResponse = { }; /** - * Extract data from API response or return null - * Useful for optional data handling + * Extract data from API response or return null. + * Useful for optional data handling. */ export function getNullableData(response: ApiResponse): T | null { if (response.error || response.data === undefined) { @@ -31,7 +31,7 @@ export function getNullableData(response: ApiResponse): T | null { } /** - * Extract data from API response or throw error + * Extract data from API response or throw error. */ export function getDataOrThrow(response: ApiResponse, errorMessage?: string): T { if (response.error || response.data === undefined) { @@ -41,54 +41,30 @@ export function getDataOrThrow(response: ApiResponse, errorMessage?: strin } /** - * Extract data from API response or return default value + * Extract data from API response or return default value. */ export function getDataOrDefault(response: ApiResponse, defaultValue: T): T { return response.data ?? defaultValue; } /** - * Check if response has an error + * Check if response has an error. */ export function hasError(response: ApiResponse): boolean { return !!response.error; } /** - * Check if response has data + * Check if response has data. */ export function hasData(response: ApiResponse): boolean { return response.data !== undefined && !response.error; } -export type DomainApiResponse = ApiSuccessResponse | ApiErrorResponse; - -export function assertSuccess(response: DomainApiResponse): ApiSuccessResponse { - if (response.success === true) { - return response; - } - throw new Error(response.error.message); -} - -export function parseDomainResponse( - response: DomainApiResponse, - parser?: (payload: T) => T -): T { - const success = assertSuccess(response); - return parser ? parser(success.data) : success.data; -} - +/** + * Parse an error payload into a structured API error response. + */ export function parseDomainError(payload: unknown): ApiErrorResponse | null { const parsed = apiErrorResponseSchema.safeParse(payload); return parsed.success ? parsed.data : null; } - -export function buildSuccessResponse( - data: ZodInfer, - schema: T -): ApiSuccessResponse> { - return { - success: true, - data: schema.parse(data), - }; -} diff --git a/apps/portal/src/lib/api/runtime/client.ts b/apps/portal/src/lib/api/runtime/client.ts index 90dfd463..3523d620 100644 --- a/apps/portal/src/lib/api/runtime/client.ts +++ b/apps/portal/src/lib/api/runtime/client.ts @@ -176,6 +176,12 @@ async function defaultHandleError(response: Response) { throw new ApiError(message, response, body); } +/** + * Parse response body from the BFF. + * + * The BFF returns data directly without any wrapper envelope. + * Errors are handled via HTTP status codes (4xx/5xx) and caught by `handleError`. + */ const parseResponseBody = async (response: Response): Promise => { if (response.status === 204) { return null; diff --git a/apps/portal/src/proxy.ts b/apps/portal/src/proxy.ts index 1f74c8d5..67892aa8 100644 --- a/apps/portal/src/proxy.ts +++ b/apps/portal/src/proxy.ts @@ -9,7 +9,6 @@ import { NextRequest, NextResponse } from "next/server"; * @see https://nextjs.org/docs/app/guides/content-security-policy * @see https://nextjs.org/docs/messages/middleware-to-proxy */ - export function proxy(request: NextRequest) { // Generate a random nonce for this request const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); diff --git a/packages/domain/common/schema.ts b/packages/domain/common/schema.ts index a3a26ada..36c339e1 100644 --- a/packages/domain/common/schema.ts +++ b/packages/domain/common/schema.ts @@ -92,32 +92,30 @@ export const soqlFieldNameSchema = z // ============================================================================ /** - * Schema for successful API responses - * Usage: apiSuccessResponseSchema(yourDataSchema) + * Schema for action acknowledgement responses (no payload). + * Used for endpoints that confirm an operation succeeded without returning data. + * + * @example + * // Controller returns: + * return {}; // Empty object as acknowledgement */ -export const apiSuccessResponseSchema = (dataSchema: T) => - z.object({ - success: z.literal(true), - data: dataSchema, - }); +export const actionAckResponseSchema = z.object({}); /** - * Schema for successful API acknowledgements (no payload) + * Schema for action responses with a human-readable message. + * Used for endpoints that return a confirmation message. + * + * @example + * // Controller returns: + * return { message: "SIM top-up completed successfully" }; */ -export const apiSuccessAckResponseSchema = z.object({ - success: z.literal(true), -}); - -/** - * Schema for successful API responses with a human-readable message (no data payload) - */ -export const apiSuccessMessageResponseSchema = z.object({ - success: z.literal(true), +export const actionMessageResponseSchema = z.object({ message: z.string(), }); /** - * Schema for error API responses + * Schema for error API responses. + * Used by the exception filter to return structured errors. */ export const apiErrorResponseSchema = z.object({ success: z.literal(false), @@ -128,12 +126,25 @@ export const apiErrorResponseSchema = z.object({ }), }); +// ============================================================================ +// Legacy API Response Schemas (Deprecated) +// ============================================================================ + /** - * Discriminated union schema for API responses - * Usage: apiResponseSchema(yourDataSchema) + * @deprecated Use direct data schemas instead. The success envelope has been removed. + * Kept for backwards compatibility during migration. */ -export const apiResponseSchema = (dataSchema: T) => - z.discriminatedUnion("success", [apiSuccessResponseSchema(dataSchema), apiErrorResponseSchema]); +export const apiSuccessResponseSchema = (dataSchema: T) => dataSchema; + +/** + * @deprecated Use actionAckResponseSchema instead. + */ +export const apiSuccessAckResponseSchema = actionAckResponseSchema; + +/** + * @deprecated Use actionMessageResponseSchema instead. + */ +export const apiSuccessMessageResponseSchema = actionMessageResponseSchema; // ============================================================================ // Pagination Schemas diff --git a/packages/domain/common/types.ts b/packages/domain/common/types.ts index 1fe21647..b73204d3 100644 --- a/packages/domain/common/types.ts +++ b/packages/domain/common/types.ts @@ -40,31 +40,27 @@ export type SalesforceCaseId = string & { readonly __brand: "SalesforceCaseId" } // ============================================================================ // ============================================================================ -// API Response Wrappers +// API Response Types // ============================================================================ -export interface ApiSuccessResponse { - success: true; - data: T; -} +/** + * Action acknowledgement response (empty object). + * Used for endpoints that confirm an operation succeeded without returning data. + */ +export type ActionAckResponse = Record; /** - * Success acknowledgement response (no payload) - * Used for endpoints that simply confirm the operation succeeded. + * Action message response. + * Used for endpoints that return a confirmation message. */ -export interface ApiSuccessAckResponse { - success: true; -} - -/** - * Success message response (no data payload) - * Used for endpoints that return a human-readable message (e.g., actions). - */ -export interface ApiSuccessMessageResponse { - success: true; +export interface ActionMessageResponse { message: string; } +/** + * Error response from the BFF. + * All errors include a code, message, and optional details. + */ export interface ApiErrorResponse { success: false; error: { @@ -74,6 +70,36 @@ export interface ApiErrorResponse { }; } +// ============================================================================ +// Legacy API Response Types (Deprecated) +// ============================================================================ + +/** + * @deprecated The success envelope has been removed. Use data types directly. + */ +export interface ApiSuccessResponse { + success: true; + data: T; +} + +/** + * @deprecated Use ActionAckResponse instead. + */ +export interface ApiSuccessAckResponse { + success: true; +} + +/** + * @deprecated Use ActionMessageResponse instead. + */ +export interface ApiSuccessMessageResponse { + success: true; + message: string; +} + +/** + * @deprecated Use data types directly for success, ApiErrorResponse for errors. + */ export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; // ============================================================================ diff --git a/packages/domain/orders/schema.ts b/packages/domain/orders/schema.ts index 9bf6570b..c0634e03 100644 --- a/packages/domain/orders/schema.ts +++ b/packages/domain/orders/schema.ts @@ -5,7 +5,6 @@ */ import { z } from "zod"; -import { apiSuccessResponseSchema } from "../common/index.js"; // ============================================================================ // Enum Value Arrays (for Zod schemas) @@ -316,7 +315,7 @@ export const checkoutBuildCartRequestSchema = z.object({ configuration: orderConfigurationsSchema.optional(), }); -export const checkoutBuildCartResponseSchema = apiSuccessResponseSchema(checkoutCartSchema); +export const checkoutBuildCartResponseSchema = checkoutCartSchema; // ============================================================================ // BFF endpoint request/param schemas (DTO inputs) @@ -352,12 +351,10 @@ export const checkoutSessionDataSchema = z.object({ cart: checkoutCartSummarySchema, }); -export const checkoutSessionResponseSchema = apiSuccessResponseSchema(checkoutSessionDataSchema); +export const checkoutSessionResponseSchema = checkoutSessionDataSchema; export const checkoutValidateCartDataSchema = z.object({ valid: z.boolean() }); -export const checkoutValidateCartResponseSchema = apiSuccessResponseSchema( - checkoutValidateCartDataSchema -); +export const checkoutValidateCartResponseSchema = checkoutValidateCartDataSchema; /** * Schema for order creation response diff --git a/packages/domain/package.json b/packages/domain/package.json index 63b06e3c..c0369a14 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -124,10 +124,10 @@ "typecheck": "pnpm run type-check" }, "peerDependencies": { - "zod": "4.1.13" + "zod": "^4.2.1" }, "devDependencies": { "typescript": "catalog:", - "zod": "catalog:" + "zod": "^4.2.1" } } diff --git a/packages/domain/subscriptions/schema.ts b/packages/domain/subscriptions/schema.ts index 1b642dae..7390f2b5 100644 --- a/packages/domain/subscriptions/schema.ts +++ b/packages/domain/subscriptions/schema.ts @@ -5,7 +5,6 @@ */ import { z } from "zod"; -import { apiSuccessMessageResponseSchema } from "../common/schema.js"; // Subscription Status Schema export const subscriptionStatusSchema = z.enum([ @@ -104,14 +103,16 @@ export const subscriptionStatsSchema = z.object({ /** * Schema for SIM action responses (top-up, cancellation, feature updates) */ -export const simActionResponseSchema = apiSuccessMessageResponseSchema.extend({ +export const simActionResponseSchema = z.object({ + message: z.string(), data: z.unknown().optional(), }); /** * Schema for SIM plan change result with IP addresses */ -export const simPlanChangeResultSchema = apiSuccessMessageResponseSchema.extend({ +export const simPlanChangeResultSchema = z.object({ + message: z.string(), ipv4: z.string().optional(), ipv6: z.string().optional(), scheduledAt: z diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a514394..e5793688 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: dependencies: "@customer-portal/domain": specifier: workspace:* - version: link:../../packages/domain + version: file:packages/domain(zod@4.1.13) "@nestjs/bullmq": specifier: ^11.0.4 version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.65.1) @@ -91,7 +91,7 @@ importers: version: 7.1.0 "@prisma/client": specifier: ^7.1.0 - version: 7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3))(typescript@5.9.3) + version: 7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3) "@sendgrid/mail": specifier: ^8.1.6 version: 8.1.6 @@ -130,7 +130,7 @@ importers: version: 8.16.3 prisma: specifier: ^7.1.0 - version: 7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) + version: 7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) rate-limiter-flexible: specifier: ^9.0.0 version: 9.0.0 @@ -191,10 +191,10 @@ importers: version: link:../../packages/domain "@heroicons/react": specifier: ^2.2.0 - version: 2.2.0(react@19.2.1) + version: 2.2.0(react@19.2.3) "@tanstack/react-query": - specifier: ^5.90.12 - version: 5.90.12(react@19.2.1) + specifier: ^5.90.14 + version: 5.90.14(react@19.2.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -206,16 +206,16 @@ importers: version: 4.1.0 lucide-react: specifier: ^0.562.0 - version: 0.562.0(react@19.2.1) + version: 0.562.0(react@19.2.3) next: - specifier: 16.0.10 - version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: 16.1.1 + version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: - specifier: 19.2.1 - version: 19.2.1 + specifier: 19.2.3 + version: 19.2.3 react-dom: - specifier: 19.2.1 - version: 19.2.1(react@19.2.1) + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -223,21 +223,21 @@ importers: specifier: ^5.1.0 version: 5.1.0 zod: - specifier: "catalog:" - version: 4.1.13 + specifier: ^4.2.1 + version: 4.2.1 zustand: specifier: ^5.0.9 - version: 5.0.9(@types/react@19.2.7)(react@19.2.1) + version: 5.0.9(@types/react@19.2.7)(react@19.2.3) devDependencies: "@next/bundle-analyzer": - specifier: ^16.0.9 - version: 16.0.10 + specifier: ^16.1.1 + version: 16.1.1 "@tailwindcss/postcss": - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 "@tanstack/react-query-devtools": - specifier: ^5.91.1 - version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1) + specifier: ^5.91.2 + version: 5.91.2(@tanstack/react-query@5.90.14(react@19.2.3))(react@19.2.3) "@types/react": specifier: ^19.2.7 version: 19.2.7 @@ -245,11 +245,11 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@4.1.17) + version: 1.0.7(tailwindcss@4.1.18) typescript: specifier: "catalog:" version: 5.9.3 @@ -260,8 +260,8 @@ importers: specifier: "catalog:" version: 5.9.3 zod: - specifier: "catalog:" - version: 4.1.13 + specifier: ^4.2.1 + version: 4.2.1 packages: "@alloc/quick-lru@5.2.0": @@ -508,6 +508,11 @@ packages: } engines: { node: ">=0.1.90" } + "@customer-portal/domain@file:packages/domain": + resolution: { directory: packages/domain, type: directory } + peerDependencies: + zod: 4.1.13 + "@discoveryjs/json-ext@0.5.7": resolution: { @@ -1765,16 +1770,16 @@ packages: "@nestjs/platform-express": optional: true - "@next/bundle-analyzer@16.0.10": + "@next/bundle-analyzer@16.1.1": resolution: { - integrity: sha512-AHA6ZomhQuRsJtkoRvsq+hIuwA6F26mQzQT8ICcc2dL3BvHRcWOA+EiFr+BgWFY++EE957xVDqMIJjLApyxnwA==, + integrity: sha512-aNJy301GGH8k36rDgrYdnyYEdjRQg6csMi1njzqHo+3qyZvOOWMHSv+p7SztNTzP5RU2KRwX0pPwYBtDcE+vVA==, } - "@next/env@16.0.10": + "@next/env@16.1.1": resolution: { - integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==, + integrity: sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==, } "@next/eslint-plugin-next@16.0.9": @@ -1783,73 +1788,73 @@ packages: integrity: sha512-ea6F0Towc70S+5y0HfkmMeNvWXHH+5yQUhovmed5qHu9WxJRW0oE26+OU6z4u0hR5WHYec7KwwHZCyWlnwdpOg==, } - "@next/swc-darwin-arm64@16.0.10": + "@next/swc-darwin-arm64@16.1.1": resolution: { - integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==, + integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==, } engines: { node: ">= 10" } cpu: [arm64] os: [darwin] - "@next/swc-darwin-x64@16.0.10": + "@next/swc-darwin-x64@16.1.1": resolution: { - integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==, + integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==, } engines: { node: ">= 10" } cpu: [x64] os: [darwin] - "@next/swc-linux-arm64-gnu@16.0.10": + "@next/swc-linux-arm64-gnu@16.1.1": resolution: { - integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==, + integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@next/swc-linux-arm64-musl@16.0.10": + "@next/swc-linux-arm64-musl@16.1.1": resolution: { - integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==, + integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@next/swc-linux-x64-gnu@16.0.10": + "@next/swc-linux-x64-gnu@16.1.1": resolution: { - integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==, + integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@next/swc-linux-x64-musl@16.0.10": + "@next/swc-linux-x64-musl@16.1.1": resolution: { - integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==, + integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@next/swc-win32-arm64-msvc@16.0.10": + "@next/swc-win32-arm64-msvc@16.1.1": resolution: { - integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==, + integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==, } engines: { node: ">= 10" } cpu: [arm64] os: [win32] - "@next/swc-win32-x64-msvc@16.0.10": + "@next/swc-win32-x64-msvc@16.1.1": resolution: { - integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==, + integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==, } engines: { node: ">= 10" } cpu: [x64] @@ -2254,97 +2259,97 @@ packages: } engines: { node: ">=14.16" } - "@tailwindcss/node@4.1.17": + "@tailwindcss/node@4.1.18": resolution: { - integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==, + integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==, } - "@tailwindcss/oxide-android-arm64@4.1.17": + "@tailwindcss/oxide-android-arm64@4.1.18": resolution: { - integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==, + integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==, } engines: { node: ">= 10" } cpu: [arm64] os: [android] - "@tailwindcss/oxide-darwin-arm64@4.1.17": + "@tailwindcss/oxide-darwin-arm64@4.1.18": resolution: { - integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==, + integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==, } engines: { node: ">= 10" } cpu: [arm64] os: [darwin] - "@tailwindcss/oxide-darwin-x64@4.1.17": + "@tailwindcss/oxide-darwin-x64@4.1.18": resolution: { - integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==, + integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==, } engines: { node: ">= 10" } cpu: [x64] os: [darwin] - "@tailwindcss/oxide-freebsd-x64@4.1.17": + "@tailwindcss/oxide-freebsd-x64@4.1.18": resolution: { - integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==, + integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==, } engines: { node: ">= 10" } cpu: [x64] os: [freebsd] - "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17": + "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18": resolution: { - integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==, + integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==, } engines: { node: ">= 10" } cpu: [arm] os: [linux] - "@tailwindcss/oxide-linux-arm64-gnu@4.1.17": + "@tailwindcss/oxide-linux-arm64-gnu@4.1.18": resolution: { - integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==, + integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@tailwindcss/oxide-linux-arm64-musl@4.1.17": + "@tailwindcss/oxide-linux-arm64-musl@4.1.18": resolution: { - integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==, + integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@tailwindcss/oxide-linux-x64-gnu@4.1.17": + "@tailwindcss/oxide-linux-x64-gnu@4.1.18": resolution: { - integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==, + integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@tailwindcss/oxide-linux-x64-musl@4.1.17": + "@tailwindcss/oxide-linux-x64-musl@4.1.18": resolution: { - integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==, + integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@tailwindcss/oxide-wasm32-wasi@4.1.17": + "@tailwindcss/oxide-wasm32-wasi@4.1.18": resolution: { - integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==, + integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==, } engines: { node: ">=14.0.0" } cpu: [wasm32] @@ -2356,62 +2361,62 @@ packages: - "@emnapi/wasi-threads" - tslib - "@tailwindcss/oxide-win32-arm64-msvc@4.1.17": + "@tailwindcss/oxide-win32-arm64-msvc@4.1.18": resolution: { - integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==, + integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==, } engines: { node: ">= 10" } cpu: [arm64] os: [win32] - "@tailwindcss/oxide-win32-x64-msvc@4.1.17": + "@tailwindcss/oxide-win32-x64-msvc@4.1.18": resolution: { - integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==, + integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==, } engines: { node: ">= 10" } cpu: [x64] os: [win32] - "@tailwindcss/oxide@4.1.17": + "@tailwindcss/oxide@4.1.18": resolution: { - integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==, + integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==, } engines: { node: ">= 10" } - "@tailwindcss/postcss@4.1.17": + "@tailwindcss/postcss@4.1.18": resolution: { - integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==, + integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==, } - "@tanstack/query-core@5.90.12": + "@tanstack/query-core@5.90.14": resolution: { - integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==, + integrity: sha512-/6di2yNI+YxpVrH9Ig74Q+puKnkCE+D0LGyagJEGndJHJc6ahkcc/UqirHKy8zCYE/N9KLggxcQvzYCsUBWgdw==, } - "@tanstack/query-devtools@5.91.1": + "@tanstack/query-devtools@5.92.0": resolution: { - integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==, + integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==, } - "@tanstack/react-query-devtools@5.91.1": + "@tanstack/react-query-devtools@5.91.2": resolution: { - integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==, + integrity: sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==, } peerDependencies: - "@tanstack/react-query": ^5.90.10 + "@tanstack/react-query": ^5.90.14 react: ^18 || ^19 - "@tanstack/react-query@5.90.12": + "@tanstack/react-query@5.90.14": resolution: { - integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==, + integrity: sha512-JAMuULej09hrZ14W9+mxoRZ44rR2BuZfCd6oKTQVNfynQxCN3muH3jh3W46gqZNw5ZqY0ZVaS43Imb3dMr6tgw==, } peerDependencies: react: ^18 || ^19 @@ -3127,6 +3132,13 @@ packages: } engines: { node: ">=6.0.0" } + baseline-browser-mapping@2.9.11: + resolution: + { + integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==, + } + hasBin: true + baseline-browser-mapping@2.9.5: resolution: { @@ -3298,6 +3310,12 @@ packages: integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==, } + caniuse-lite@1.0.30001761: + resolution: + { + integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==, + } + chalk@4.1.2: resolution: { @@ -3919,6 +3937,13 @@ packages: } engines: { node: ">=10.13.0" } + enhanced-resolve@5.18.4: + resolution: + { + integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==, + } + engines: { node: ">=10.13.0" } + environment@1.1.0: resolution: { @@ -5013,10 +5038,10 @@ packages: } engines: { node: ">= 0.8.0" } - libphonenumber-js@1.12.31: + libphonenumber-js@1.12.33: resolution: { - integrity: sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==, + integrity: sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==, } lightningcss-android-arm64@1.30.2: @@ -5568,10 +5593,10 @@ packages: "@nestjs/swagger": optional: true - next@16.0.10: + next@16.1.1: resolution: { - integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==, + integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==, } engines: { node: ">=20.9.0" } hasBin: true @@ -6228,18 +6253,18 @@ packages: integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==, } - react-dom@19.2.1: + react-dom@19.2.3: resolution: { - integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==, + integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==, } peerDependencies: - react: ^19.2.1 + react: ^19.2.3 - react@19.2.1: + react@19.2.3: resolution: { - integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==, + integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==, } engines: { node: ">=0.10.0" } @@ -6899,10 +6924,10 @@ packages: peerDependencies: tailwindcss: ">=3.0.0 || insiders" - tailwindcss@4.1.17: + tailwindcss@4.1.18: resolution: { - integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==, + integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==, } tapable@2.3.0: @@ -7245,10 +7270,10 @@ packages: typescript: optional: true - validator@13.15.23: + validator@13.15.26: resolution: { - integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==, + integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==, } engines: { node: ">= 0.10" } @@ -7494,10 +7519,10 @@ packages: integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==, } - zod@4.2.0: + zod@4.2.1: resolution: { - integrity: sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==, + integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==, } zustand@5.0.9: @@ -7704,6 +7729,10 @@ snapshots: "@colors/colors@1.5.0": optional: true + "@customer-portal/domain@file:packages/domain(zod@4.1.13)": + dependencies: + zod: 4.1.13 + "@discoveryjs/json-ext@0.5.7": {} "@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)": @@ -7859,9 +7888,9 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 - "@heroicons/react@2.2.0(react@19.2.1)": + "@heroicons/react@2.2.0(react@19.2.3)": dependencies: - react: 19.2.1 + react: 19.2.3 "@hono/node-server@1.19.6(hono@4.10.6)": dependencies: @@ -8388,41 +8417,41 @@ snapshots: optionalDependencies: "@nestjs/platform-express": 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - "@next/bundle-analyzer@16.0.10": + "@next/bundle-analyzer@16.1.1": dependencies: webpack-bundle-analyzer: 4.10.1 transitivePeerDependencies: - bufferutil - utf-8-validate - "@next/env@16.0.10": {} + "@next/env@16.1.1": {} "@next/eslint-plugin-next@16.0.9": dependencies: fast-glob: 3.3.1 - "@next/swc-darwin-arm64@16.0.10": + "@next/swc-darwin-arm64@16.1.1": optional: true - "@next/swc-darwin-x64@16.0.10": + "@next/swc-darwin-x64@16.1.1": optional: true - "@next/swc-linux-arm64-gnu@16.0.10": + "@next/swc-linux-arm64-gnu@16.1.1": optional: true - "@next/swc-linux-arm64-musl@16.0.10": + "@next/swc-linux-arm64-musl@16.1.1": optional: true - "@next/swc-linux-x64-gnu@16.0.10": + "@next/swc-linux-x64-gnu@16.1.1": optional: true - "@next/swc-linux-x64-musl@16.0.10": + "@next/swc-linux-x64-musl@16.1.1": optional: true - "@next/swc-win32-arm64-msvc@16.0.10": + "@next/swc-win32-arm64-msvc@16.1.1": optional: true - "@next/swc-win32-x64-msvc@16.0.10": + "@next/swc-win32-x64-msvc@16.1.1": optional: true "@nodelib/fs.scandir@2.1.5": @@ -8457,11 +8486,11 @@ snapshots: "@prisma/client-runtime-utils@7.1.0": {} - "@prisma/client@7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3))(typescript@5.9.3)": + "@prisma/client@7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)": dependencies: "@prisma/client-runtime-utils": 7.1.0 optionalDependencies: - prisma: 7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) + prisma: 7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) typescript: 5.9.3 "@prisma/config@7.1.0": @@ -8528,11 +8557,11 @@ snapshots: "@prisma/query-plan-executor@6.18.0": {} - "@prisma/studio-core@0.8.2(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)": + "@prisma/studio-core@0.8.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)": dependencies: "@types/react": 19.2.7 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) "@protobufjs/aspromise@1.1.2": {} @@ -8668,89 +8697,89 @@ snapshots: defer-to-connect: 2.0.1 optional: true - "@tailwindcss/node@4.1.17": + "@tailwindcss/node@4.1.18": dependencies: "@jridgewell/remapping": 2.3.5 - enhanced-resolve: 5.18.3 + enhanced-resolve: 5.18.4 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.17 + tailwindcss: 4.1.18 - "@tailwindcss/oxide-android-arm64@4.1.17": + "@tailwindcss/oxide-android-arm64@4.1.18": optional: true - "@tailwindcss/oxide-darwin-arm64@4.1.17": + "@tailwindcss/oxide-darwin-arm64@4.1.18": optional: true - "@tailwindcss/oxide-darwin-x64@4.1.17": + "@tailwindcss/oxide-darwin-x64@4.1.18": optional: true - "@tailwindcss/oxide-freebsd-x64@4.1.17": + "@tailwindcss/oxide-freebsd-x64@4.1.18": optional: true - "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17": + "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18": optional: true - "@tailwindcss/oxide-linux-arm64-gnu@4.1.17": + "@tailwindcss/oxide-linux-arm64-gnu@4.1.18": optional: true - "@tailwindcss/oxide-linux-arm64-musl@4.1.17": + "@tailwindcss/oxide-linux-arm64-musl@4.1.18": optional: true - "@tailwindcss/oxide-linux-x64-gnu@4.1.17": + "@tailwindcss/oxide-linux-x64-gnu@4.1.18": optional: true - "@tailwindcss/oxide-linux-x64-musl@4.1.17": + "@tailwindcss/oxide-linux-x64-musl@4.1.18": optional: true - "@tailwindcss/oxide-wasm32-wasi@4.1.17": + "@tailwindcss/oxide-wasm32-wasi@4.1.18": optional: true - "@tailwindcss/oxide-win32-arm64-msvc@4.1.17": + "@tailwindcss/oxide-win32-arm64-msvc@4.1.18": optional: true - "@tailwindcss/oxide-win32-x64-msvc@4.1.17": + "@tailwindcss/oxide-win32-x64-msvc@4.1.18": optional: true - "@tailwindcss/oxide@4.1.17": + "@tailwindcss/oxide@4.1.18": optionalDependencies: - "@tailwindcss/oxide-android-arm64": 4.1.17 - "@tailwindcss/oxide-darwin-arm64": 4.1.17 - "@tailwindcss/oxide-darwin-x64": 4.1.17 - "@tailwindcss/oxide-freebsd-x64": 4.1.17 - "@tailwindcss/oxide-linux-arm-gnueabihf": 4.1.17 - "@tailwindcss/oxide-linux-arm64-gnu": 4.1.17 - "@tailwindcss/oxide-linux-arm64-musl": 4.1.17 - "@tailwindcss/oxide-linux-x64-gnu": 4.1.17 - "@tailwindcss/oxide-linux-x64-musl": 4.1.17 - "@tailwindcss/oxide-wasm32-wasi": 4.1.17 - "@tailwindcss/oxide-win32-arm64-msvc": 4.1.17 - "@tailwindcss/oxide-win32-x64-msvc": 4.1.17 + "@tailwindcss/oxide-android-arm64": 4.1.18 + "@tailwindcss/oxide-darwin-arm64": 4.1.18 + "@tailwindcss/oxide-darwin-x64": 4.1.18 + "@tailwindcss/oxide-freebsd-x64": 4.1.18 + "@tailwindcss/oxide-linux-arm-gnueabihf": 4.1.18 + "@tailwindcss/oxide-linux-arm64-gnu": 4.1.18 + "@tailwindcss/oxide-linux-arm64-musl": 4.1.18 + "@tailwindcss/oxide-linux-x64-gnu": 4.1.18 + "@tailwindcss/oxide-linux-x64-musl": 4.1.18 + "@tailwindcss/oxide-wasm32-wasi": 4.1.18 + "@tailwindcss/oxide-win32-arm64-msvc": 4.1.18 + "@tailwindcss/oxide-win32-x64-msvc": 4.1.18 - "@tailwindcss/postcss@4.1.17": + "@tailwindcss/postcss@4.1.18": dependencies: "@alloc/quick-lru": 5.2.0 - "@tailwindcss/node": 4.1.17 - "@tailwindcss/oxide": 4.1.17 + "@tailwindcss/node": 4.1.18 + "@tailwindcss/oxide": 4.1.18 postcss: 8.5.6 - tailwindcss: 4.1.17 + tailwindcss: 4.1.18 - "@tanstack/query-core@5.90.12": {} + "@tanstack/query-core@5.90.14": {} - "@tanstack/query-devtools@5.91.1": {} + "@tanstack/query-devtools@5.92.0": {} - "@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)": + "@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.14(react@19.2.3))(react@19.2.3)": dependencies: - "@tanstack/query-devtools": 5.91.1 - "@tanstack/react-query": 5.90.12(react@19.2.1) - react: 19.2.1 + "@tanstack/query-devtools": 5.92.0 + "@tanstack/react-query": 5.90.14(react@19.2.3) + react: 19.2.3 - "@tanstack/react-query@5.90.12(react@19.2.1)": + "@tanstack/react-query@5.90.14(react@19.2.3)": dependencies: - "@tanstack/query-core": 5.90.12 - react: 19.2.1 + "@tanstack/query-core": 5.90.14 + react: 19.2.3 "@tokenizer/inflate@0.2.7": dependencies: @@ -9277,6 +9306,8 @@ snapshots: base64url@3.0.1: {} + baseline-browser-mapping@2.9.11: {} + baseline-browser-mapping@2.9.5: {} bcrypt-pbkdf@1.0.2: @@ -9413,6 +9444,8 @@ snapshots: caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001761: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -9457,8 +9490,8 @@ snapshots: class-validator@0.14.2: dependencies: "@types/validator": 13.15.10 - libphonenumber-js: 1.12.31 - validator: 13.15.23 + libphonenumber-js: 1.12.33 + validator: 13.15.26 optional: true class-variance-authority@0.7.1: @@ -9718,6 +9751,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + environment@1.1.0: {} error-ex@1.3.4: @@ -9784,8 +9822,8 @@ snapshots: "@babel/parser": 7.28.5 eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.2.0 - zod-validation-error: 4.0.2(zod@4.2.0) + zod: 4.2.1 + zod-validation-error: 4.0.2(zod@4.2.1) transitivePeerDependencies: - supports-color @@ -10446,7 +10484,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.31: + libphonenumber-js@1.12.33: optional: true lightningcss-android-arm64@1.30.2: @@ -10565,9 +10603,9 @@ snapshots: lru.min@1.1.3: {} - lucide-react@0.562.0(react@19.2.1): + lucide-react@0.562.0(react@19.2.3): dependencies: - react: 19.2.1 + react: 19.2.3 luxon@3.7.2: {} @@ -10728,24 +10766,25 @@ snapshots: optionalDependencies: "@nestjs/swagger": 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) - next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - "@next/env": 16.0.10 + "@next/env": 16.1.1 "@swc/helpers": 0.5.15 - caniuse-lite: 1.0.30001760 + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 postcss: 8.4.31 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) optionalDependencies: - "@next/swc-darwin-arm64": 16.0.10 - "@next/swc-darwin-x64": 16.0.10 - "@next/swc-linux-arm64-gnu": 16.0.10 - "@next/swc-linux-arm64-musl": 16.0.10 - "@next/swc-linux-x64-gnu": 16.0.10 - "@next/swc-linux-x64-musl": 16.0.10 - "@next/swc-win32-arm64-msvc": 16.0.10 - "@next/swc-win32-x64-msvc": 16.0.10 + "@next/swc-darwin-arm64": 16.1.1 + "@next/swc-darwin-x64": 16.1.1 + "@next/swc-linux-arm64-gnu": 16.1.1 + "@next/swc-linux-arm64-musl": 16.1.1 + "@next/swc-linux-x64-gnu": 16.1.1 + "@next/swc-linux-x64-musl": 16.1.1 + "@next/swc-win32-arm64-msvc": 16.1.1 + "@next/swc-win32-x64-msvc": 16.1.1 sharp: 0.34.5 transitivePeerDependencies: - "@babel/core" @@ -11034,12 +11073,12 @@ snapshots: prettier@3.7.4: {} - prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3): + prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: "@prisma/config": 7.1.0 "@prisma/dev": 0.15.0(typescript@5.9.3) "@prisma/engines": 7.1.0 - "@prisma/studio-core": 0.8.2(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + "@prisma/studio-core": 0.8.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: @@ -11122,12 +11161,12 @@ snapshots: defu: 6.1.4 destr: 2.0.5 - react-dom@19.2.1(react@19.2.1): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.2.1 + react: 19.2.3 scheduler: 0.27.0 - react@19.2.1: {} + react@19.2.3: {} readable-stream@3.6.2: dependencies: @@ -11498,10 +11537,10 @@ snapshots: dependencies: "@tokenizer/token": 0.3.0 - styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.1): + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3): dependencies: client-only: 0.0.1 - react: 19.2.1 + react: 19.2.3 optionalDependencies: "@babel/core": 7.28.5 @@ -11530,11 +11569,11 @@ snapshots: tailwind-merge@3.4.0: {} - tailwindcss-animate@1.0.7(tailwindcss@4.1.17): + tailwindcss-animate@1.0.7(tailwindcss@4.1.18): dependencies: - tailwindcss: 4.1.17 + tailwindcss: 4.1.18 - tailwindcss@4.1.17: {} + tailwindcss@4.1.18: {} tapable@2.3.0: {} @@ -11730,7 +11769,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 - validator@13.15.23: + validator@13.15.26: optional: true vary@1.1.2: {} @@ -11885,15 +11924,15 @@ snapshots: dependencies: grammex: 3.1.12 - zod-validation-error@4.0.2(zod@4.2.0): + zod-validation-error@4.0.2(zod@4.2.1): dependencies: - zod: 4.2.0 + zod: 4.2.1 zod@4.1.13: {} - zod@4.2.0: {} + zod@4.2.1: {} - zustand@5.0.9(@types/react@19.2.7)(react@19.2.1): + zustand@5.0.9(@types/react@19.2.7)(react@19.2.3): optionalDependencies: "@types/react": 19.2.7 - react: 19.2.1 + react: 19.2.3