diff --git a/apps/api/src/app/workflows-v2/usecases/preview/preview.usecase.ts b/apps/api/src/app/workflows-v2/usecases/preview/preview.usecase.ts deleted file mode 100644 index c1c1f892759..00000000000 --- a/apps/api/src/app/workflows-v2/usecases/preview/preview.usecase.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - BuildStepDataUsecase, - ControlValueSanitizerService, - CreateVariablesObject, - CreateVariablesObjectCommand, - GeneratePreviewResponseDto, - GetWorkflowByIdsCommand, - GetWorkflowByIdsUseCase, - Instrument, - InstrumentUsecase, - isStepResolverActive, - PinoLogger, - PreviewCommand, - PreviewErrorHandler, - PreviewPayloadDto, - PreviewPayloadProcessorService, - PreviewStep, - PreviewStepCommand, - StepResponseDto, -} from '@novu/application-generic'; -import { ContextResolved } from '@novu/framework/internal'; -import { ChannelTypeEnum, ResourceOriginEnum, StepTypeEnum } from '@novu/shared'; -import { PayloadMergerService } from './services/payload-merger.service'; - -@Injectable() -export class PreviewUsecase { - constructor( - private previewStepUsecase: PreviewStep, - private buildStepDataUsecase: BuildStepDataUsecase, - private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, - private createVariablesObject: CreateVariablesObject, - private readonly controlValueSanitizer: ControlValueSanitizerService, - private readonly payloadMerger: PayloadMergerService, - private readonly payloadProcessor: PreviewPayloadProcessorService, - private readonly errorHandler: PreviewErrorHandler, - private readonly logger: PinoLogger - ) {} - - @InstrumentUsecase() - async execute(command: PreviewCommand): Promise { - try { - const context = await this.initializePreviewContext(command); - const stepResolverHash = - typeof context.stepData.stepResolverHash === 'string' ? context.stepData.stepResolverHash : undefined; - const isStepResolver = isStepResolverActive(stepResolverHash); - const isStepResolverEmail = isStepResolver && context.stepData.type === StepTypeEnum.EMAIL; - - const sanitizedControls = isStepResolver - ? context.controlValues - : this.controlValueSanitizer.sanitizeControlsForPreview( - context.controlValues, - context.stepData.type, - context.workflow.origin || ResourceOriginEnum.NOVU_CLOUD - ); - - const { previewTemplateData } = this.controlValueSanitizer.processControlValues( - sanitizedControls, - context.variableSchema, - context.variablesObject - ); - - let payloadExample = await this.payloadMerger.mergePayloadExample({ - workflow: context.workflow, - stepIdOrInternalId: command.stepIdOrInternalId, - payloadExample: previewTemplateData.payloadExample, - userPayloadExample: command.generatePreviewRequestDto.previewPayload, - user: command.user, - }); - - payloadExample = this.payloadProcessor.enhanceEventCountValue(payloadExample); - - const cleanedPayloadExample = this.payloadProcessor.cleanPreviewExamplePayload(payloadExample); - - try { - const executeOutput = await this.executePreviewUsecase( - command, - context.stepData, - payloadExample, - previewTemplateData.controlValues, - stepResolverHash - ); - - return { - result: { - preview: executeOutput.outputs as Record, - type: context.stepData.type as unknown as ChannelTypeEnum, - }, - previewPayloadExample: cleanedPayloadExample, - schema: context.variableSchema, - }; - } catch (error) { - /* - * If preview execution fails, still return valid schema and payload example - * but with an empty preview result. - * For step resolver email steps, since its a runtime error, surface the error - * as HTML rendered in the preview panel. - * For all other resolver step types, log the error so it's visible in server logs. - */ - if (isStepResolver) { - this.logger.error({ error, stepType: context.stepData.type }, 'Step resolver preview execution failed'); - } - - const previewResult = isStepResolverEmail - ? { subject: '', body: this.errorHandler.buildPreviewErrorHtml(error) } - : {}; - - return { - result: { - preview: previewResult, - type: context.stepData.type as unknown as ChannelTypeEnum, - }, - previewPayloadExample: cleanedPayloadExample, - schema: context.variableSchema, - }; - } - } catch { - // Return default response for non-existent workflows/steps or other critical errors - return this.errorHandler.createErrorResponse(); - } - } - - private async initializePreviewContext(command: PreviewCommand) { - // get step with control values, variables, issues etc. - const stepData = await this.getStepData(command); - const controlValues = command.generatePreviewRequestDto.controlValues || stepData.controls.values || {}; - const workflow = await this.findWorkflow(command); - - // extract all variables from the control values and build the variables object - const variablesObject = await this.createVariablesObject.execute( - CreateVariablesObjectCommand.create({ - environmentId: command.user.environmentId, - organizationId: command.user.organizationId, - controlValues: Object.values(controlValues), - variableSchema: stepData.variables, - payloadSchema: workflow.payloadSchema, - }) - ); - - return { stepData, controlValues, variableSchema: stepData.variables, variablesObject, workflow }; - } - - @Instrument() - private async findWorkflow(command: PreviewCommand) { - return await this.getWorkflowByIdsUseCase.execute( - GetWorkflowByIdsCommand.create({ - workflowIdOrInternalId: command.workflowIdOrInternalId, - environmentId: command.user.environmentId, - organizationId: command.user.organizationId, - }) - ); - } - - @Instrument() - private async getStepData(command: PreviewCommand) { - return await this.buildStepDataUsecase.execute({ - workflowIdOrInternalId: command.workflowIdOrInternalId, - stepIdOrInternalId: command.stepIdOrInternalId, - user: command.user, - previewPayload: command.generatePreviewRequestDto.previewPayload, - }); - } - - @Instrument() - private async executePreviewUsecase( - command: PreviewCommand, - stepData: StepResponseDto, - previewPayloadExample: PreviewPayloadDto, - controlValues: Record, - stepResolverHash: string | undefined - ) { - const state = this.payloadProcessor.buildState(previewPayloadExample.steps); - - return await this.previewStepUsecase.execute( - PreviewStepCommand.create({ - payload: previewPayloadExample.payload || {}, - subscriber: previewPayloadExample.subscriber, - controls: controlValues || {}, - context: previewPayloadExample.context as ContextResolved, - environmentId: command.user.environmentId, - organizationId: command.user.organizationId, - stepId: stepData.stepId, - userId: command.user._id, - workflowId: stepData.workflowId, - workflowOrigin: stepData.origin, - state, - skipLayoutRendering: command.skipLayoutRendering, - stepResolverHash, - }) - ); - } -} diff --git a/apps/api/src/app/workflows-v2/usecases/preview/services/payload-merger.service.ts b/apps/api/src/app/workflows-v2/usecases/preview/services/payload-merger.service.ts deleted file mode 100644 index 36a1b2c58e5..00000000000 --- a/apps/api/src/app/workflows-v2/usecases/preview/services/payload-merger.service.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - BuildStepDataUsecase, - JsonSchemaMock, - MockDataGeneratorService, - mergeCommonObjectKeys, - PreviewPayloadDto, - StepResponseDto, -} from '@novu/application-generic'; -import { NotificationTemplateEntity } from '@novu/dal'; -import { ContextResolved } from '@novu/framework/internal'; -import { ContextPayload, createMockObjectFromSchema, ResourceOriginEnum, UserSessionData } from '@novu/shared'; -import { isPlainObject, pick } from 'es-toolkit'; -import { keys, merge, mergeWith } from 'es-toolkit/compat'; - -@Injectable() -export class PayloadMergerService { - constructor( - private readonly mockDataGenerator: MockDataGeneratorService, - private readonly buildStepDataUsecase: BuildStepDataUsecase - ) {} - - /** - * Merges workflow payload schema with user-provided payload, handling feature flags - * for schema-based generation vs legacy merging strategies. - */ - async mergePayloadExample({ - workflow, - payloadExample, - userPayloadExample, - stepIdOrInternalId, - user, - }: { - workflow?: NotificationTemplateEntity; - payloadExample: Record; - userPayloadExample: PreviewPayloadDto | undefined; - stepIdOrInternalId?: string; - user: UserSessionData; - }): Promise> { - const shouldUsePayloadSchema = - workflow?.origin === ResourceOriginEnum.EXTERNAL || workflow?.origin === ResourceOriginEnum.NOVU_CLOUD; - - if (shouldUsePayloadSchema && workflow?.payloadSchema) { - return this.mergeWithPayloadSchema({ - workflow, - payloadExample, - userPayloadExample, - stepIdOrInternalId, - user, - }); - } - - return this.mergeWithoutPayloadSchema({ - payloadExample, - userPayloadExample, - workflow, - stepIdOrInternalId, - user, - }); - } - - private async mergeWithPayloadSchema({ - workflow, - payloadExample, - userPayloadExample, - stepIdOrInternalId, - user, - }: { - workflow: NotificationTemplateEntity; - payloadExample: Record; - userPayloadExample: PreviewPayloadDto | undefined; - stepIdOrInternalId?: string; - user: UserSessionData; - }): Promise> { - let schemaBasedPayloadExample: Record; - - try { - const schema = { - type: 'object' as const, - properties: { payload: workflow.payloadSchema }, - additionalProperties: false, - }; - - const mockData = JsonSchemaMock.generate(schema) as Record; - schemaBasedPayloadExample = mockData; - } catch (error) { - schemaBasedPayloadExample = createMockObjectFromSchema({ - type: 'object', - properties: { payload: workflow.payloadSchema }, - }); - } - - let mergedPayload = merge({}, schemaBasedPayloadExample); - - if (userPayloadExample && Object.keys(userPayloadExample).length > 0) { - // Filter userPayloadExample to only include keys that exist in schemaBasedPayloadExample - const filteredUserPayload = this.filterPayloadBySchema( - userPayloadExample as Record, - schemaBasedPayloadExample - ); - - mergedPayload = mergeWith(mergedPayload, filteredUserPayload, (objValue, srcValue) => { - if (Array.isArray(srcValue)) { - return srcValue; - } - - return undefined; - }); - } - - const fullSubscriberSchema = this.mockDataGenerator.createFullSubscriberObject(); - // Preserve user-provided subscriber data even if it was filtered out earlier - const userSubscriberData = (userPayloadExample?.subscriber as Record) || {}; - - mergedPayload.subscriber = merge({}, fullSubscriberSchema, userSubscriberData); - - mergedPayload.context = this.resolveContext(userPayloadExample?.context); - - if (workflow && stepIdOrInternalId) { - /* - * Preserve steps from payloadExample (which contains correctly generated digest events) - * and merge with user-provided steps and mock data for missing steps - */ - const stepsFromPayloadExample = (payloadExample.steps as Record) || {}; - const generatedStepsObject = await this.createFullStepsObject({ - workflow, - stepIdOrInternalId, - user, - userPayloadExample, - }); - - /* - * Merge with priority: user steps > payloadExample steps > generated mock steps - * Use mergeWith to ensure user-provided data (including empty objects) takes precedence - */ - mergedPayload.steps = mergeWith( - {}, - generatedStepsObject, - stepsFromPayloadExample, - (userPayloadExample?.steps as Record) || {}, - (objValue, srcValue) => { - // If source value is provided by user, always use it (even if it's an empty object) - if (srcValue !== undefined) { - return srcValue; - } - - return undefined; // Let lodash handle the merge - } - ); - } - - return mergedPayload; - } - - /** - * Convert ContextPayload to ContextResolved without upserting actual db entities - * just for the preview purposes - */ - private resolveContext(contextPayload?: ContextPayload): ContextResolved | undefined { - if (!contextPayload) return undefined; - - const resolved: ContextResolved = {}; - - for (const [contextType, contextValue] of Object.entries(contextPayload)) { - if (!contextValue) continue; - - resolved[contextType] = - typeof contextValue === 'string' - ? { id: contextValue, data: {} } - : { id: contextValue.id, data: contextValue.data || {} }; - } - - return Object.keys(resolved).length > 0 ? resolved : undefined; - } - - private async mergeWithoutPayloadSchema({ - payloadExample, - userPayloadExample, - workflow, - stepIdOrInternalId, - user, - }: { - payloadExample: Record; - userPayloadExample: PreviewPayloadDto | undefined; - workflow?: NotificationTemplateEntity; - user: UserSessionData; - stepIdOrInternalId?: string; - }): Promise> { - let finalPayload: Record; - - if (userPayloadExample && Object.keys(userPayloadExample).length > 0) { - finalPayload = mergeCommonObjectKeys( - userPayloadExample as Record, - payloadExample as Record - ); - } else { - finalPayload = payloadExample; - } - - const fullSubscriberSchema = this.mockDataGenerator.createFullSubscriberObject(); - // Preserve user-provided subscriber data even if it was filtered out earlier - const userSubscriberData = (userPayloadExample?.subscriber as Record) || {}; - - finalPayload.subscriber = merge({}, fullSubscriberSchema, userSubscriberData); - - finalPayload.context = this.resolveContext(userPayloadExample?.context); - - if (workflow && stepIdOrInternalId) { - /* - * Preserve steps from payloadExample (which contains correctly generated digest events) - * and merge with user-provided steps and mock data for missing steps - */ - - const stepsFromPayloadExample = (payloadExample.steps as Record) || {}; - const generatedStepsObject = await this.createFullStepsObject({ - workflow, - stepIdOrInternalId, - user, - userPayloadExample, - }); - /* - * Merge with priority: user steps > payloadExample steps > generated mock steps - * Use mergeWith to ensure user-provided data (including empty objects) takes precedence - */ - finalPayload.steps = mergeWith( - {}, - generatedStepsObject, - stepsFromPayloadExample, - (userPayloadExample?.steps as Record) || {}, - (objValue, srcValue) => { - // If source value is provided by user, always use it (even if it's an empty object) - if (srcValue !== undefined) { - return srcValue; - } - - return undefined; // Let lodash handle the merge - } - ); - } - - return finalPayload; - } - - /** - * Generates mock step results for all workflow steps preceding the current step, - * enabling preview of step-dependent data in templates. - */ - private async createFullStepsObject({ - workflow, - stepIdOrInternalId, - user, - userPayloadExample, - }: { - workflow: NotificationTemplateEntity; - stepIdOrInternalId: string; - user: UserSessionData; - userPayloadExample?: PreviewPayloadDto; - }): Promise> { - const stepsObject: Record = {}; - const currentStepData = await this.getStepData({ - workflowIdOrInternalId: workflow._id, - stepIdOrInternalId, - user, - }); - const currentStepId = currentStepData._id; - - const currentStepIndex = workflow.steps.findIndex( - (step) => step._id === currentStepId || step.stepId === currentStepData.stepId - ); - - if (currentStepIndex === -1) { - return stepsObject; - } - - const previousSteps = workflow.steps.slice(0, currentStepIndex); - const userStepsData = (userPayloadExample?.steps as Record) || {}; - - for (const step of previousSteps) { - const stepId = step.stepId || step._id; - - if (stepId) { - if (userStepsData[stepId]) { - stepsObject[stepId] = userStepsData[stepId]; - } else { - // Fall back to generating mock data - const mockResult = this.mockDataGenerator.generateMockStepResult({ - stepType: step.template?.type || '', - workflow, - }); - - stepsObject[stepId] = mockResult; - } - } - } - - return stepsObject; - } - - private async getStepData({ - workflowIdOrInternalId, - stepIdOrInternalId, - user, - }: { - workflowIdOrInternalId: string; - stepIdOrInternalId: string; - user: UserSessionData; - }): Promise { - return await this.buildStepDataUsecase.execute({ - workflowIdOrInternalId, - stepIdOrInternalId, - user, - }); - } - - /** - * Recursively filters the user payload to only include keys that exist in the schema-based payload - */ - private filterPayloadBySchema( - userPayload: Record, - schemaPayload: Record - ): Record { - // Use lodash pick to only include keys that exist in the schema - const filtered = pick(userPayload, keys(schemaPayload)); - - // Recursively filter nested objects and arrays - for (const [key, value] of Object.entries(filtered)) { - if (isPlainObject(value) && isPlainObject(schemaPayload[key])) { - filtered[key] = this.filterPayloadBySchema( - value as Record, - schemaPayload[key] as Record - ); - } else if (Array.isArray(value) && Array.isArray(schemaPayload[key])) { - // Handle arrays by filtering each element - filtered[key] = value.map((item) => { - if (isPlainObject(item) && schemaPayload[key] && Array.isArray(schemaPayload[key])) { - const schemaArray = schemaPayload[key] as unknown[]; - // Use the first element of the schema array as the template for filtering - const schemaTemplate = - schemaArray.length > 0 && isPlainObject(schemaArray[0]) - ? (schemaArray[0] as Record) - : {}; - - return this.filterPayloadBySchema(item as Record, schemaTemplate); - } - - return item; - }); - } - } - - return filtered; - } -} diff --git a/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx b/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx index 9a86cbf6285..ff48199d26c 100644 --- a/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx +++ b/apps/dashboard/src/components/conditions-editor/combinator-selector.tsx @@ -1,6 +1,6 @@ import { type CombinatorSelectorProps } from 'react-querybuilder'; -import { toSelectOptions } from '@/components/conditions-editor/select-option-utils'; +import { fromSafeValue, toSafeValue, toSelectOptions } from '@/components/conditions-editor/select-option-utils'; import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/primitives/select'; import { cn } from '@/utils/ui'; @@ -8,11 +8,11 @@ export const CombinatorSelector = ({ disabled, value, options, handleOnChange, c return ( { - handleOnChange(e); + handleOnChange(fromSafeValue(e)); context?.saveForm(); }} disabled={disabled} - value={value} + value={toSafeValue(value)} > { if (isOptionGroupArray(arr)) { return arr.map((group) => ( {group.label} {group.options.map((option) => ( - + {capitalizeLabel ? capitalize(option.label.toLocaleLowerCase()) : option.label} @@ -20,7 +34,7 @@ export const toSelectOptions = (arr: OptionList, capitalizeLabel: boolean = true } return (arr as BaseOption[]).map((option) => ( - + {capitalizeLabel ? capitalize(option.label.toLocaleLowerCase()) : option.label} diff --git a/apps/dashboard/src/components/workflow-editor/nodes.tsx b/apps/dashboard/src/components/workflow-editor/nodes.tsx index 8e2be0c335f..3146a10e8fe 100644 --- a/apps/dashboard/src/components/workflow-editor/nodes.tsx +++ b/apps/dashboard/src/components/workflow-editor/nodes.tsx @@ -591,9 +591,8 @@ export const AddNode = (props: NodeProps) => { Drop here - {!isIntersecting && ( + {!isIntersecting && !isReadOnly && ( addNode(data.index, selection)} diff --git a/apps/dashboard/src/components/workflow-editor/steps/preview/step-preview-factory.tsx b/apps/dashboard/src/components/workflow-editor/steps/preview/step-preview-factory.tsx index 9903f827939..28052fe8584 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/preview/step-preview-factory.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/preview/step-preview-factory.tsx @@ -1,4 +1,4 @@ -import { ResourceOriginEnum, StepTypeEnum } from '@novu/shared'; +import { type PreviewError, ResourceOriginEnum, StepTypeEnum } from '@novu/shared'; import { memo } from 'react'; import { InlineToast } from '@/components/primitives/inline-toast'; import { ChatPreview } from '@/components/workflow-editor/steps/chat/chat-preview'; @@ -9,6 +9,7 @@ import { PushPreview } from '@/components/workflow-editor/steps/push/push-previe import { SmsPreview } from '@/components/workflow-editor/steps/sms/sms-preview'; import { STEP_TYPE_LABELS } from '@/utils/constants'; import { EmailCorePreview } from './previews/email-preview-wrapper'; +import { StepResolverPreviewError } from './step-resolver-preview-error'; const NoPreviewAvailable = memo(({ stepType }: { stepType: StepTypeEnum }) => { return ( @@ -37,6 +38,14 @@ export function StepPreviewFactory() { const isStepResolver = typeof step.stepResolverHash === 'string'; + const resolverError = isStepResolver + ? (previewData?.result as { error?: PreviewError } | undefined)?.error + : undefined; + + if (resolverError) { + return ; + } + const mobilePreviewDescription = 'This preview shows how your message will appear on mobile. Actual rendering may vary by device.'; diff --git a/apps/dashboard/src/components/workflow-editor/steps/preview/step-resolver-preview-error.tsx b/apps/dashboard/src/components/workflow-editor/steps/preview/step-resolver-preview-error.tsx new file mode 100644 index 00000000000..871064c4d24 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/preview/step-resolver-preview-error.tsx @@ -0,0 +1,23 @@ +import { type PreviewError } from '@novu/shared'; +import { RiErrorWarningLine } from 'react-icons/ri'; + +export function StepResolverPreviewError({ error }: { error: PreviewError }) { + return ( +
+
+
+
+ +
+ {error.title} +
+
+
+            {error.message}
+          
+

{error.hint}

+
+
+
+ ); +} diff --git a/apps/worker/src/app/workflow/services/subscriber-process.worker.ts b/apps/worker/src/app/workflow/services/subscriber-process.worker.ts index 24f83679584..7c83ffa6906 100644 --- a/apps/worker/src/app/workflow/services/subscriber-process.worker.ts +++ b/apps/worker/src/app/workflow/services/subscriber-process.worker.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { BullMqService, FeatureFlagsService, @@ -19,6 +19,11 @@ import { SubscriberJobBound } from '../usecases/subscriber-job-bound/subscriber- const nr = require('newrelic'); const LOG_CONTEXT = 'SubscriberProcessWorker'; +const SUBSCRIBER_ID_VALIDATION_PREFIX = 'subscriberId under property to'; + +function isSubscriberIdValidationError(e: unknown): boolean { + return e instanceof BadRequestException && typeof e.message === 'string' && e.message.startsWith(SUBSCRIBER_ID_VALIDATION_PREFIX); +} @Injectable() export class SubscriberProcessWorker extends SubscriberProcessWorkerService { @@ -69,8 +74,12 @@ export class SubscriberProcessWorker extends SubscriberProcessWorkerService { .execute(data) .then(resolve) .catch((e) => { - Logger.error(e, 'unexpected error', 'SubscriberProcessWorkerService - getWorkerProcessor'); - nr.noticeError(e); + if (isSubscriberIdValidationError(e)) { + Logger.debug(e, e.message, 'SubscriberProcessWorkerService - getWorkerProcessor'); + } else { + Logger.error(e, 'unexpected error', 'SubscriberProcessWorkerService - getWorkerProcessor'); + nr.noticeError(e); + } reject(e); }) diff --git a/libs/application-generic/src/commands/base.command.ts b/libs/application-generic/src/commands/base.command.ts index 511d383790f..b526f5f654d 100644 --- a/libs/application-generic/src/commands/base.command.ts +++ b/libs/application-generic/src/commands/base.command.ts @@ -87,6 +87,15 @@ export class CommandValidationException extends BadRequestException { public className: string, public constraintsViolated: Record ) { - super({ message: 'Validation failed', className, constraintsViolated }); + const message = formatValidationMessage(className, constraintsViolated); + super({ message, className, constraintsViolated }); } } + +function formatValidationMessage(className: string, constraints: Record): string { + const details = Object.entries(constraints) + .map(([field, constraint]) => `${field}: ${constraint.messages.join(', ')}`) + .join('; '); + + return `Validation failed for ${className}: ${details}`; +} diff --git a/libs/application-generic/src/dtos/workflow/generate-preview-response.dto.ts b/libs/application-generic/src/dtos/workflow/generate-preview-response.dto.ts index 454cc380a78..737ca888251 100644 --- a/libs/application-generic/src/dtos/workflow/generate-preview-response.dto.ts +++ b/libs/application-generic/src/dtos/workflow/generate-preview-response.dto.ts @@ -21,6 +21,20 @@ export enum RedirectTargetEnum { UNFENCED_TOP = '_unfencedTop', } +export class PreviewErrorDto { + @ApiProperty({ description: 'Short error title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'Detailed error message' }) + @IsString() + message: string; + + @ApiProperty({ description: 'Actionable hint for the user' }) + @IsString() + hint: string; +} + export class RenderOutput {} export class RedirectDto { @@ -210,7 +224,8 @@ export class InAppRenderOutput extends RenderOutput { ChatRenderOutput, DigestRegularOutput, DigestTimedOutput, - DelayRenderOutput + DelayRenderOutput, + PreviewErrorDto ) export class GeneratePreviewResponseDto { @ApiProperty({ @@ -250,36 +265,42 @@ export class GeneratePreviewResponseDto { properties: { type: { enum: [ChannelTypeEnum.EMAIL] }, preview: { $ref: getSchemaPath(EmailRenderOutput) }, + error: { $ref: getSchemaPath(PreviewErrorDto) }, }, }, { properties: { type: { enum: [ChannelTypeEnum.EMAIL] }, preview: { $ref: getSchemaPath(EmailRenderOutput) }, + error: { $ref: getSchemaPath(PreviewErrorDto) }, }, }, { properties: { type: { enum: [ChannelTypeEnum.IN_APP] }, preview: { $ref: getSchemaPath(InAppRenderOutput) }, + error: { $ref: getSchemaPath(PreviewErrorDto) }, }, }, { properties: { type: { enum: [ChannelTypeEnum.SMS] }, preview: { $ref: getSchemaPath(SmsRenderOutput) }, + error: { $ref: getSchemaPath(PreviewErrorDto) }, }, }, { properties: { type: { enum: [ChannelTypeEnum.PUSH] }, preview: { $ref: getSchemaPath(PushRenderOutput) }, + error: { $ref: getSchemaPath(PreviewErrorDto) }, }, }, { properties: { type: { enum: [ChannelTypeEnum.CHAT] }, preview: { $ref: getSchemaPath(ChatRenderOutput) }, + error: { $ref: getSchemaPath(PreviewErrorDto) }, }, }, { @@ -300,22 +321,27 @@ export class GeneratePreviewResponseDto { | { type: ChannelTypeEnum.EMAIL; preview: EmailRenderOutput; + error?: PreviewErrorDto; } | { type: ChannelTypeEnum.IN_APP; preview: InAppRenderOutput; + error?: PreviewErrorDto; } | { type: ChannelTypeEnum.SMS; preview: SmsRenderOutput; + error?: PreviewErrorDto; } | { type: ChannelTypeEnum.PUSH; preview: PushRenderOutput; + error?: PreviewErrorDto; } | { type: ChannelTypeEnum.CHAT; preview: ChatRenderOutput; + error?: PreviewErrorDto; } | { type: ActionTypeEnum.DELAY; @@ -334,7 +360,8 @@ export class GeneratePreviewResponseDto { | ChannelTypeEnum.CHAT | ActionTypeEnum.DELAY | ActionTypeEnum.DIGEST; - preview: Record; // Allow empty object + preview: Record; + error?: PreviewErrorDto; }; } diff --git a/libs/application-generic/src/factories/push/handlers/fcm.handler.ts b/libs/application-generic/src/factories/push/handlers/fcm.handler.ts index ec5c81afb85..dbc53888dbe 100644 --- a/libs/application-generic/src/factories/push/handlers/fcm.handler.ts +++ b/libs/application-generic/src/factories/push/handlers/fcm.handler.ts @@ -21,11 +21,19 @@ export class FCMHandler extends BasePushHandler { throw new Error('Config is not valid for fcm'); } - const config = JSON.parse(updatedCredentials); + let config: Record; + try { + config = JSON.parse(updatedCredentials); + } catch { + throw new Error( + 'FCM credentials must be a valid JSON service account configuration. Received a non-JSON string instead.' + ); + } + this.provider = new FcmPushProvider({ - projectId: config.project_id, - email: config.client_email, - secretKey: config.private_key, + projectId: config.project_id as string, + email: config.client_email as string, + secretKey: config.private_key as string, }); } diff --git a/libs/application-generic/src/usecases/preview/preview.usecase.ts b/libs/application-generic/src/usecases/preview/preview.usecase.ts index 1aa935af35c..6dc9104b1d9 100644 --- a/libs/application-generic/src/usecases/preview/preview.usecase.ts +++ b/libs/application-generic/src/usecases/preview/preview.usecase.ts @@ -96,19 +96,29 @@ export class PreviewUsecase { /* * If preview execution fails, still return valid schema and payload example * but with an empty preview result. - * For step resolver steps, surface the error as HTML rendered in the preview panel. + * For step resolver steps, surface a structured error so the dashboard can + * render a channel-agnostic error UI regardless of step type. */ - const previewResult = isStepResolver - ? { subject: '', body: this.errorHandler.buildPreviewErrorHtml(error) } - : {}; - const novuSignature = isHttpRequestStep ? await this.buildNovuSignatureSample(command.user.environmentId) : undefined; + if (isStepResolver) { + return { + result: { + preview: {}, + type: context.stepData.type as unknown as ChannelTypeEnum, + error: this.errorHandler.extractErrorContent(error), + }, + previewPayloadExample: cleanedPayloadExample, + schema: context.variableSchema, + novuSignature, + }; + } + return { result: { - preview: previewResult, + preview: {}, type: context.stepData.type as unknown as ChannelTypeEnum, }, previewPayloadExample: cleanedPayloadExample, diff --git a/libs/application-generic/src/usecases/preview/utils/preview-error-handler.ts b/libs/application-generic/src/usecases/preview/utils/preview-error-handler.ts index f0610221bca..59858798321 100644 --- a/libs/application-generic/src/usecases/preview/utils/preview-error-handler.ts +++ b/libs/application-generic/src/usecases/preview/utils/preview-error-handler.ts @@ -121,29 +121,7 @@ export class PreviewErrorHandler { } } - buildPreviewErrorHtml(error: unknown): string { - const { title, message, hint } = this.extractErrorContent(error); - - return `
-
-
-
- - - - -
- ${title} -
-
-
${this.escapeHtml(message)}
-

${hint}

-
-
-
`; - } - - private extractErrorContent(error: unknown): { title: string; message: string; hint: string } { + extractErrorContent(error: unknown): { title: string; message: string; hint: string } { if (error instanceof HttpException) { const response = error.getResponse() as Record; const code = typeof response?.code === 'string' ? response.code : ''; @@ -165,13 +143,4 @@ export class PreviewErrorHandler { hint: 'Please try again. If the issue persists, contact support.', }; } - - private escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } } diff --git a/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts b/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts index b3685a87ccd..a02368806a8 100644 --- a/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts +++ b/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts @@ -180,16 +180,19 @@ export class TriggerMulticast extends TriggerBase { } ); - this.logger.error( - { - transactionId: command.transactionId, - organization: command.organizationId, - triggerIdentifier: command.identifier, - userId: command.userId, - error: e, - }, - 'Unexpected error has occurred when processing multicast' - ); + const logData = { + transactionId: command.transactionId, + organization: command.organizationId, + triggerIdentifier: command.identifier, + userId: command.userId, + error: e, + }; + + if (isSubscriberIdValidationError(e)) { + this.logger.debug(logData, error.message); + } else { + this.logger.error(logData, 'Unexpected error has occurred when processing multicast'); + } throw e; } @@ -327,6 +330,12 @@ export const buildSubscriberDefine = (recipient: TriggerRecipientSubscriber): IS } }; +const SUBSCRIBER_ID_VALIDATION_PREFIX = 'subscriberId under property to'; + +function isSubscriberIdValidationError(e: unknown): boolean { + return e instanceof BadRequestException && typeof e.message === 'string' && e.message.startsWith(SUBSCRIBER_ID_VALIDATION_PREFIX); +} + export const validateSubscriberDefine = (recipient: ISubscribersDefine) => { if (!recipient) { throw new BadRequestException( diff --git a/libs/dal/src/repositories/message/message.schema.ts b/libs/dal/src/repositories/message/message.schema.ts index 341cdfb1e5a..ffd8fd6e16d 100644 --- a/libs/dal/src/repositories/message/message.schema.ts +++ b/libs/dal/src/repositories/message/message.schema.ts @@ -158,6 +158,18 @@ const messageSchema = new Schema( schemaOptions ); +messageSchema.pre('init', function sanitizeCorruptCta(doc: Record) { + if (doc.cta !== undefined && doc.cta !== null && typeof doc.cta !== 'object') { + doc.cta = {}; + } + if (doc.cta && typeof doc.cta === 'object') { + const cta = doc.cta as Record; + if (cta.action !== undefined && cta.action !== null && typeof cta.action !== 'object') { + cta.action = {}; + } + } +}); + /** * todo: all the pre hooks should be removed after all the soft deletes are removed task nv-5688 */ diff --git a/libs/notifications/src/workflows/usage-report/email.tsx b/libs/notifications/src/workflows/usage-report/email.tsx index c6a850d87d0..5e967a4c738 100644 --- a/libs/notifications/src/workflows/usage-report/email.tsx +++ b/libs/notifications/src/workflows/usage-report/email.tsx @@ -892,10 +892,8 @@ function FooterCta({ dashboardUrl }: { dashboardUrl: string }) { - View dashboard - + diff --git a/package.json b/package.json index 11ac41f6dc1..51303c29660 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "minimatch@>=7.0.0 <7.4.8": "^7.4.8", "minimatch@>=8.0.0 <8.0.6": "^8.0.6", "minimatch@>=9.0.0 <9.0.7": "^9.0.7", - "nanoid@>=3.0.0 <3.1.31": "^3.1.31", + "nanoid@>=3.0.0 <3.3.8": "^3.3.8", "nth-check": "^2.1.1", "postcss@<8.4.31": "^8.4.31", "proxy-agent": "^6.3.0", @@ -199,7 +199,7 @@ "node-forge@<1.3.2": "^1.3.2", "ws@>=8.0.0 <8.17.1": "^8.17.1", "next@>=13.0.0 <14.2.32": "^14.2.35", - "next@>=15.0.0 <15.4.7": "^15.4.10", + "next@>=15.0.0 <15.4.11": "^15.4.11", "fast-xml-parser@>=4.1.3 <4.5.4": "4.5.4", "fast-xml-parser@>=5.0.0 <5.3.8": "5.3.8", "basic-ftp@<5.2.0": "5.2.0", @@ -214,12 +214,17 @@ "jws@<3.2.3": "^3.2.3", "svgo@>=3.0.0 <3.3.3": "^3.3.3", "flatted@<3.4.0": "^3.4.0", + "serialize-javascript@<7.0.3": "^7.0.3", + "ejs@<3.1.10": "^3.1.10", "rollup@>=3.0.0 <3.30.0": "^3.30.0", "rollup@<2.80.0": "^2.80.0", "glob@>=10.2.0 <10.5.0": "^10.5.0", "undici@>=7.0.0 <7.24.0": "^7.24.0", "undici@>=6.0.0 <6.24.0": "^6.24.0", - "jose@>=3.0.0 <4.15.5": "^4.15.5" + "jose@>=3.0.0 <4.15.5": "^4.15.5", + "qs@<6.14.2": "^6.14.2", + "js-yaml@<3.14.2": "^3.14.2", + "js-yaml@>=4.0.0 <4.1.1": "^4.1.1" }, "onlyBuiltDependencies": [ "@clerk/shared", diff --git a/packages/shared/src/dto/workflows/preview-step-response.dto.ts b/packages/shared/src/dto/workflows/preview-step-response.dto.ts index 9cb389dc94b..e3a4dab254b 100644 --- a/packages/shared/src/dto/workflows/preview-step-response.dto.ts +++ b/packages/shared/src/dto/workflows/preview-step-response.dto.ts @@ -122,6 +122,12 @@ export class InAppRenderOutput extends RenderOutput { }; } +export type PreviewError = { + title: string; + message: string; + hint: string; +}; + export class PreviewPayload { subscriber?: Partial; payload?: Record; @@ -137,22 +143,27 @@ export class GeneratePreviewResponseDto { | { type: ChannelTypeEnum.EMAIL; preview: EmailRenderOutput; + error?: PreviewError; } | { type: ChannelTypeEnum.IN_APP; preview: InAppRenderOutput; + error?: PreviewError; } | { type: ChannelTypeEnum.SMS; preview: SmsRenderOutput; + error?: PreviewError; } | { type: ChannelTypeEnum.PUSH; preview: PushRenderOutput; + error?: PreviewError; } | { type: ChannelTypeEnum.CHAT; preview: ChatRenderOutput; + error?: PreviewError; } | { type: ActionTypeEnum.DELAY; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b548e137ded..555c9816c46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ overrides: minimatch@>=7.0.0 <7.4.8: ^7.4.8 minimatch@>=8.0.0 <8.0.6: ^8.0.6 minimatch@>=9.0.0 <9.0.7: ^9.0.7 - nanoid@>=3.0.0 <3.1.31: ^3.1.31 + nanoid@>=3.0.0 <3.3.8: ^3.3.8 nth-check: ^2.1.1 postcss@<8.4.31: ^8.4.31 proxy-agent: ^6.3.0 @@ -45,7 +45,7 @@ overrides: node-forge@<1.3.2: ^1.3.2 ws@>=8.0.0 <8.17.1: ^8.17.1 next@>=13.0.0 <14.2.32: ^14.2.35 - next@>=15.0.0 <15.4.7: ^15.4.10 + next@>=15.0.0 <15.4.11: ^15.4.11 fast-xml-parser@>=4.1.3 <4.5.4: 4.5.4 fast-xml-parser@>=5.0.0 <5.3.8: 5.3.8 basic-ftp@<5.2.0: 5.2.0 @@ -60,12 +60,17 @@ overrides: jws@<3.2.3: ^3.2.3 svgo@>=3.0.0 <3.3.3: ^3.3.3 flatted@<3.4.0: ^3.4.0 + serialize-javascript@<7.0.3: ^7.0.3 + ejs@<3.1.10: ^3.1.10 rollup@>=3.0.0 <3.30.0: ^3.30.0 rollup@<2.80.0: ^2.80.0 glob@>=10.2.0 <10.5.0: ^10.5.0 undici@>=7.0.0 <7.24.0: ^7.24.0 undici@>=6.0.0 <6.24.0: ^6.24.0 jose@>=3.0.0 <4.15.5: ^4.15.5 + qs@<6.14.2: ^6.14.2 + js-yaml@<3.14.2: ^3.14.2 + js-yaml@>=4.0.0 <4.1.1: ^4.1.1 importers: @@ -430,7 +435,7 @@ importers: specifier: ^11.2.4 version: 11.2.4 nanoid: - specifier: ^3.1.31 + specifier: ^3.3.8 version: 3.3.8 nest-raven: specifier: 10.1.0 @@ -2490,7 +2495,7 @@ importers: specifier: ^0.17.0 version: 0.17.0 nanoid: - specifier: ^3.1.31 + specifier: ^3.3.8 version: 3.3.8 nestjs-otel: specifier: 6.2.0 @@ -3698,7 +3703,7 @@ importers: specifier: ^4.0.1 version: 4.0.1 nanoid: - specifier: ^3.1.31 + specifier: ^3.3.8 version: 3.3.8 node-fetch: specifier: ^3.2.10 @@ -3722,8 +3727,8 @@ importers: specifier: 1.0.0 version: 1.0.0 qs: - specifier: ^6.11.0 - version: 6.14.0 + specifier: ^6.14.2 + version: 6.15.0 resend: specifier: ^6.0.3 version: 6.0.3(@react-email/render@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) @@ -4111,8 +4116,8 @@ importers: specifier: ^5.1.6 version: 5.1.6 next: - specifier: 15.4.10 - version: 15.4.10(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + specifier: ^15.4.11 + version: 15.5.12(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) react: specifier: ^18.3.1 version: 18.3.1 @@ -8645,8 +8650,8 @@ packages: '@next/env@14.2.35': resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@next/env@15.4.10': - resolution: {integrity: sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg==} + '@next/env@15.5.12': + resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} '@next/swc-darwin-arm64@14.2.33': resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} @@ -8654,8 +8659,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@15.4.8': - resolution: {integrity: sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==} + '@next/swc-darwin-arm64@15.5.12': + resolution: {integrity: sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -8666,8 +8671,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@15.4.8': - resolution: {integrity: sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw==} + '@next/swc-darwin-x64@15.5.12': + resolution: {integrity: sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -8679,8 +8684,8 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-arm64-gnu@15.4.8': - resolution: {integrity: sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw==} + '@next/swc-linux-arm64-gnu@15.5.12': + resolution: {integrity: sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -8693,8 +8698,8 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-arm64-musl@15.4.8': - resolution: {integrity: sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==} + '@next/swc-linux-arm64-musl@15.5.12': + resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -8707,8 +8712,8 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-x64-gnu@15.4.8': - resolution: {integrity: sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==} + '@next/swc-linux-x64-gnu@15.5.12': + resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -8721,8 +8726,8 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-x64-musl@15.4.8': - resolution: {integrity: sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==} + '@next/swc-linux-x64-musl@15.5.12': + resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -8734,8 +8739,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@15.4.8': - resolution: {integrity: sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==} + '@next/swc-win32-arm64-msvc@15.5.12': + resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -8752,8 +8757,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.8': - resolution: {integrity: sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg==} + '@next/swc-win32-x64-msvc@15.5.12': + resolution: {integrity: sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -17780,11 +17785,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} - engines: {node: '>=0.10.0'} - hasBin: true - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -20735,16 +20735,8 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.0.0: - resolution: {integrity: sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true js-yaml@4.1.1: @@ -22401,11 +22393,6 @@ packages: nanoid@2.1.11: resolution: {integrity: sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==} - nanoid@3.3.3: - resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -22526,8 +22513,8 @@ packages: sass: optional: true - next@15.4.10: - resolution: {integrity: sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ==} + next@15.5.12: + resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -24326,22 +24313,6 @@ packages: pushpad@1.0.0: resolution: {integrity: sha512-hOaqVpS/b6pZXsYTf2avGoBl1Z4pVjtCyKta57Pdfm189NdMJ/6ZdLL0vFs+/GKuOA5g60/GLRRSqO5zMqr4Eg==} - qs@6.10.4: - resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} - engines: {node: '>=0.6'} - - qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} - - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -24395,9 +24366,6 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -25314,14 +25282,9 @@ packages: sentence-case@2.1.1: resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} - serialize-javascript@5.0.1: - resolution: {integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==} - - serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.4: + resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==} + engines: {node: '>=20.0.0'} seroval-plugins@1.2.1: resolution: {integrity: sha512-H5vs53+39+x4Udwp4J5rNZfgFuA+Lt+uU+09w1gYBVWomtAl98B+E9w7yC05Xc81/HgLvJdlyqJbU0fJCKCmdw==} @@ -28335,7 +28298,7 @@ snapshots: dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 - js-yaml: 4.1.0 + js-yaml: 4.1.1 '@apimatic/schema@0.6.0': dependencies: @@ -32710,7 +32673,7 @@ snapshots: json-stringify-safe: 5.0.1 mime-types: 2.1.35 performance-now: 2.1.0 - qs: 6.10.4 + qs: 6.15.0 safe-buffer: 5.2.1 tough-cookie: 4.1.3 tunnel-agent: 0.6.0 @@ -33885,7 +33848,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.1 + js-yaml: 3.14.2 resolve-from: 5.0.0 '@istanbuljs/nyc-config-typescript@1.0.2(nyc@15.1.0)': @@ -34908,7 +34871,7 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.4.3 - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.0 optional: true @@ -35115,7 +35078,7 @@ snapshots: '@nestjs/common': 10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.18(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.18)(@nestjs/websockets@10.4.18)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 path-to-regexp: 3.2.0 reflect-metadata: 0.2.2 @@ -35213,48 +35176,48 @@ snapshots: '@next/env@14.2.35': {} - '@next/env@15.4.10': {} + '@next/env@15.5.12': {} '@next/swc-darwin-arm64@14.2.33': optional: true - '@next/swc-darwin-arm64@15.4.8': + '@next/swc-darwin-arm64@15.5.12': optional: true '@next/swc-darwin-x64@14.2.33': optional: true - '@next/swc-darwin-x64@15.4.8': + '@next/swc-darwin-x64@15.5.12': optional: true '@next/swc-linux-arm64-gnu@14.2.33': optional: true - '@next/swc-linux-arm64-gnu@15.4.8': + '@next/swc-linux-arm64-gnu@15.5.12': optional: true '@next/swc-linux-arm64-musl@14.2.33': optional: true - '@next/swc-linux-arm64-musl@15.4.8': + '@next/swc-linux-arm64-musl@15.5.12': optional: true '@next/swc-linux-x64-gnu@14.2.33': optional: true - '@next/swc-linux-x64-gnu@15.4.8': + '@next/swc-linux-x64-gnu@15.5.12': optional: true '@next/swc-linux-x64-musl@14.2.33': optional: true - '@next/swc-linux-x64-musl@15.4.8': + '@next/swc-linux-x64-musl@15.5.12': optional: true '@next/swc-win32-arm64-msvc@14.2.33': optional: true - '@next/swc-win32-arm64-msvc@15.4.8': + '@next/swc-win32-arm64-msvc@15.5.12': optional: true '@next/swc-win32-ia32-msvc@14.2.33': @@ -35263,7 +35226,7 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.33': optional: true - '@next/swc-win32-x64-msvc@15.4.8': + '@next/swc-win32-x64-msvc@15.5.12': optional: true '@nextjournal/lang-clojure@1.0.0': @@ -44164,7 +44127,7 @@ snapshots: '@verdaccio/core': 7.0.0-next-7.15 '@verdaccio/utils': 7.0.0-next-7.15 debug: 4.3.4(supports-color@8.1.1) - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 minimatch: 7.4.9 yup: 0.32.11 @@ -44726,7 +44689,7 @@ snapshots: '@yarnpkg/parsers@3.0.2': dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 tslib: 2.8.1 '@zag-js/core@1.21.3': @@ -45377,7 +45340,7 @@ snapshots: dependencies: aws-sdk: 2.1354.0 commander: 3.0.2 - js-yaml: 3.14.1 + js-yaml: 3.14.2 watchpack: 2.4.0 aws-sdk@2.1354.0: @@ -45814,7 +45777,7 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.13.0 + qs: 6.15.0 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 @@ -46522,7 +46485,7 @@ snapshots: dependencies: argv: 0.0.2 ignore-walk: 3.0.4 - js-yaml: 3.14.1 + js-yaml: 3.14.2 teeny-request: 7.1.1(encoding@0.1.13) urlgrey: 1.0.0 transitivePeerDependencies: @@ -46683,7 +46646,7 @@ snapshots: compression-webpack-plugin@10.0.0(webpack@5.94.0): dependencies: schema-utils: 4.0.0 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.4 webpack: 5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.15))(esbuild@0.27.3)(webpack-cli@5.1.4) compression@1.7.4: @@ -47431,6 +47394,7 @@ snapshots: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 + optional: true debug@4.3.1(supports-color@8.1.1): dependencies: @@ -47862,10 +47826,6 @@ snapshots: dependencies: jake: 10.8.5 - ejs@3.1.9: - dependencies: - jake: 10.8.5 - electron-to-chromium@1.5.267: {} elegant-spinner@1.0.1: {} @@ -48673,7 +48633,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.11.0 + qs: 6.15.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.18.0 @@ -48709,7 +48669,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.11.0 + qs: 6.15.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.18.0 @@ -48745,7 +48705,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.15.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -48781,7 +48741,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.15.0 range-parser: 1.2.1 router: 2.1.0 safe-buffer: 5.2.1 @@ -49335,7 +49295,7 @@ snapshots: front-matter@4.0.2: dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 fs-constants@1.0.0: {} @@ -50412,7 +50372,7 @@ snapshots: change-case: 3.1.0 debug: 4.3.4(supports-color@8.1.1) degit: 2.8.4 - ejs: 3.1.9 + ejs: 3.1.10 enquirer: 2.3.6 execa: 5.1.1 front-matter: 4.0.2 @@ -52116,19 +52076,11 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.0.0: - dependencies: - argparse: 2.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -52273,7 +52225,7 @@ snapshots: json-schema-ref-parser@6.1.0: dependencies: call-me-maybe: 1.0.2 - js-yaml: 3.14.1 + js-yaml: 3.14.2 ono: 4.0.11 json-schema-to-ts@1.6.4: @@ -52463,7 +52415,7 @@ snapshots: fast-glob: 3.3.2 file-entry-cache: 8.0.0 jiti: 1.21.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimist: 1.2.6 picocolors: 1.0.0 picomatch: 4.0.2 @@ -52675,7 +52627,7 @@ snapshots: lightningcss@1.29.2: dependencies: - detect-libc: 2.0.3 + detect-libc: 2.1.2 optionalDependencies: lightningcss-darwin-arm64: 1.29.2 lightningcss-darwin-x64: 1.29.2 @@ -53108,7 +53060,7 @@ snapshots: dependencies: gaxios: 6.1.1(encoding@0.1.13) isomorphic-unfetch: 4.0.2 - qs: 6.14.0 + qs: 6.15.0 transitivePeerDependencies: - encoding - supports-color @@ -53202,7 +53154,7 @@ snapshots: get-stdin: 9.0.0 glob: 8.0.3 ignore: 5.2.4 - js-yaml: 4.1.0 + js-yaml: 4.1.1 jsonc-parser: 3.2.0 markdownlint: 0.27.0 minimatch: 5.1.9 @@ -54001,12 +53953,12 @@ snapshots: find-up: 5.0.0 glob: 7.2.0 he: 1.2.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 log-symbols: 4.1.0 minimatch: 5.1.9 ms: 2.1.3 - nanoid: 3.3.3 - serialize-javascript: 6.0.0 + nanoid: 3.3.8 + serialize-javascript: 7.0.4 strip-json-comments: 3.1.1 supports-color: 8.1.1 workerpool: 6.2.1 @@ -54027,12 +53979,12 @@ snapshots: glob: 7.1.6 growl: 1.10.5 he: 1.2.0 - js-yaml: 4.0.0 + js-yaml: 4.1.1 log-symbols: 4.0.0 minimatch: 3.1.5 ms: 2.1.3 nanoid: 3.3.8 - serialize-javascript: 5.0.1 + serialize-javascript: 7.0.4 strip-json-comments: 3.1.1 supports-color: 8.1.1 which: 2.0.2 @@ -54250,8 +54202,6 @@ snapshots: nanoid@2.1.11: {} - nanoid@3.3.3: {} - nanoid@3.3.8: {} nanoid@5.1.6: {} @@ -54294,7 +54244,7 @@ snapshots: needle@2.4.0: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) iconv-lite: 0.4.24 sax: 1.4.1 transitivePeerDependencies: @@ -54302,7 +54252,7 @@ snapshots: needle@3.2.0: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) iconv-lite: 0.6.3 sax: 1.5.0 transitivePeerDependencies: @@ -54450,24 +54400,24 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.4.10(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8): + next@15.5.12(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8): dependencies: - '@next/env': 15.4.10 + '@next/env': 15.5.12 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001734 + caniuse-lite: 1.0.30001764 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.6(@babel/core@7.28.0)(babel-plugin-macros@3.1.0)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.8 - '@next/swc-darwin-x64': 15.4.8 - '@next/swc-linux-arm64-gnu': 15.4.8 - '@next/swc-linux-arm64-musl': 15.4.8 - '@next/swc-linux-x64-gnu': 15.4.8 - '@next/swc-linux-x64-musl': 15.4.8 - '@next/swc-win32-arm64-msvc': 15.4.8 - '@next/swc-win32-x64-msvc': 15.4.8 + '@next/swc-darwin-arm64': 15.5.12 + '@next/swc-darwin-x64': 15.5.12 + '@next/swc-linux-arm64-gnu': 15.5.12 + '@next/swc-linux-arm64-musl': 15.5.12 + '@next/swc-linux-x64-gnu': 15.5.12 + '@next/swc-linux-x64-musl': 15.5.12 + '@next/swc-win32-arm64-msvc': 15.5.12 + '@next/swc-win32-x64-msvc': 15.5.12 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.58.2 sass: 1.77.8 @@ -55706,7 +55656,7 @@ snapshots: enquirer: 2.3.6 eventemitter2: 5.0.1 fclone: 1.0.11 - js-yaml: 4.1.0 + js-yaml: 4.1.1 mkdirp: 1.0.4 needle: 2.4.0 pidusage: 3.0.2 @@ -55740,7 +55690,7 @@ snapshots: portfinder@1.0.32: dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) mkdirp: 0.5.6 transitivePeerDependencies: - supports-color @@ -56559,22 +56509,6 @@ snapshots: transitivePeerDependencies: - supports-color - qs@6.10.4: - dependencies: - side-channel: 1.1.0 - - qs@6.11.0: - dependencies: - side-channel: 1.1.0 - - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -56616,10 +56550,6 @@ snapshots: radix3@1.1.2: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - range-parser@1.2.1: {} raw-body@2.5.2: @@ -57793,17 +57723,7 @@ snapshots: no-case: 2.3.2 upper-case-first: 1.1.2 - serialize-javascript@5.0.1: - dependencies: - randombytes: 2.1.0 - - serialize-javascript@6.0.0: - dependencies: - randombytes: 2.1.0 - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 + serialize-javascript@7.0.4: {} seroval-plugins@1.2.1(seroval@1.5.1): dependencies: @@ -58633,7 +58553,7 @@ snapshots: stripe@11.18.0: dependencies: '@types/node': 22.15.13 - qs: 6.14.0 + qs: 6.15.0 strnum@1.0.5: {} @@ -59110,7 +59030,7 @@ snapshots: telnyx@1.23.0: dependencies: lodash.isplainobject: 4.0.6 - qs: 6.14.0 + qs: 6.15.0 safe-buffer: 5.2.1 tweetnacl: 1.0.3 uuid: 3.4.0 @@ -59135,7 +59055,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.4 terser: 5.31.6 webpack: 5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.15))(esbuild@0.27.3) optionalDependencies: @@ -59147,7 +59067,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.4 terser: 5.31.6 webpack: 5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.15))(esbuild@0.27.3)(webpack-cli@5.1.4) optionalDependencies: @@ -59789,7 +59709,7 @@ snapshots: dayjs: 1.11.9 https-proxy-agent: 5.0.1 jsonwebtoken: 9.0.3 - qs: 6.14.0 + qs: 6.15.0 scmp: 2.1.0 url-parse: 1.5.10 xmlbuilder: 13.0.2 @@ -60420,7 +60340,7 @@ snapshots: express-rate-limit: 5.5.1 fast-safe-stringify: 2.1.1 handlebars: 4.7.8 - js-yaml: 4.1.0 + js-yaml: 4.1.1 jsonwebtoken: 9.0.2 kleur: 4.1.5 lodash: 4.17.21