From 286f8e160b261c17ec67897eb8916bd09c7abb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Fri, 23 Jan 2026 14:55:10 +0100 Subject: [PATCH] feat: improve webhook integration (customized body and headers) --- apps/api/Dockerfile | 2 + apps/start/package.json | 7 + .../forms/webhook-integration.tsx | 269 ++++++++- apps/start/src/components/json-editor.tsx | 218 ++++++++ apps/worker/Dockerfile | 2 + apps/worker/package.json | 1 + apps/worker/src/jobs/notification.ts | 53 +- packages/js-runtime/index.ts | 1 + packages/js-runtime/package.json | 21 + packages/js-runtime/src/ast-walker.ts | 165 ++++++ packages/js-runtime/src/constants.ts | 141 +++++ packages/js-runtime/src/execute.ts | 31 ++ packages/js-runtime/src/index.ts | 2 + packages/js-runtime/src/validate.test.ts | 332 +++++++++++ packages/js-runtime/src/validate.ts | 280 ++++++++++ packages/js-runtime/tsconfig.json | 12 + packages/trpc/package.json | 1 + packages/trpc/src/routers/integration.ts | 19 +- packages/validation/src/index.ts | 2 + pnpm-lock.yaml | 526 +++++++++++++++--- 20 files changed, 1994 insertions(+), 91 deletions(-) create mode 100644 apps/start/src/components/json-editor.tsx create mode 100644 packages/js-runtime/index.ts create mode 100644 packages/js-runtime/package.json create mode 100644 packages/js-runtime/src/ast-walker.ts create mode 100644 packages/js-runtime/src/constants.ts create mode 100644 packages/js-runtime/src/execute.ts create mode 100644 packages/js-runtime/src/index.ts create mode 100644 packages/js-runtime/src/validate.test.ts create mode 100644 packages/js-runtime/src/validate.ts create mode 100644 packages/js-runtime/tsconfig.json diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index a81fab10..249c1599 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -41,6 +41,7 @@ COPY packages/payments/package.json packages/payments/ COPY packages/constants/package.json packages/constants/ COPY packages/validation/package.json packages/validation/ COPY packages/integrations/package.json packages/integrations/ +COPY packages/js-runtime/package.json packages/js-runtime/ COPY patches ./patches # BUILD @@ -108,6 +109,7 @@ COPY --from=build /app/packages/payments ./packages/payments COPY --from=build /app/packages/constants ./packages/constants COPY --from=build /app/packages/validation ./packages/validation COPY --from=build /app/packages/integrations ./packages/integrations +COPY --from=build /app/packages/js-runtime ./packages/js-runtime COPY --from=build /app/tooling/typescript ./tooling/typescript RUN pnpm db:codegen diff --git a/apps/start/package.json b/apps/start/package.json index 98ec9948..3d0e49a2 100644 --- a/apps/start/package.json +++ b/apps/start/package.json @@ -84,9 +84,16 @@ "@types/d3": "^7.4.3", "ai": "^4.2.10", "bind-event-listener": "^3.0.0", + "@codemirror/commands": "^6.7.0", + "@codemirror/lang-javascript": "^6.2.0", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/state": "^6.4.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.35.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^0.2.1", + "codemirror": "^6.0.1", "d3": "^7.8.5", "date-fns": "^3.3.1", "debounce": "^2.2.0", diff --git a/apps/start/src/components/integrations/forms/webhook-integration.tsx b/apps/start/src/components/integrations/forms/webhook-integration.tsx index 69b60cc6..ca39b686 100644 --- a/apps/start/src/components/integrations/forms/webhook-integration.tsx +++ b/apps/start/src/components/integrations/forms/webhook-integration.tsx @@ -1,18 +1,53 @@ -import { InputWithLabel } from '@/components/forms/input-with-label'; +import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label'; +import { JsonEditor } from '@/components/json-editor'; import { Button } from '@/components/ui/button'; +import { Combobox } from '@/components/ui/combobox'; +import { Input } from '@/components/ui/input'; import { useAppParams } from '@/hooks/use-app-params'; import { useTRPC } from '@/integrations/trpc/react'; import type { RouterOutputs } from '@/trpc/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { zCreateWebhookIntegration } from '@openpanel/validation'; import { useMutation } from '@tanstack/react-query'; +import { PlusIcon, TrashIcon } from 'lucide-react'; import { path, mergeDeepRight } from 'ramda'; +import { useEffect } from 'react'; +import { Controller, useFieldArray, useWatch } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import type { z } from 'zod'; type IForm = z.infer; +const DEFAULT_TRANSFORMER = `(payload) => { + return payload; +}`; + +// Convert Record to array format for form +function headersToArray( + headers: Record | undefined, +): { key: string; value: string }[] { + if (!headers || Object.keys(headers).length === 0) { + return []; + } + return Object.entries(headers).map(([key, value]) => ({ key, value })); +} + +// Convert array format back to Record for API +function headersToRecord( + headers: { key: string; value: string }[], +): Record { + return headers.reduce( + (acc, { key, value }) => { + if (key.trim()) { + acc[key.trim()] = value; + } + return acc; + }, + {} as Record, + ); +} + export function WebhookIntegrationForm({ defaultValues, onSuccess, @@ -21,6 +56,13 @@ export function WebhookIntegrationForm({ onSuccess: () => void; }) { const { organizationId } = useAppParams(); + + // Convert headers from Record to array format for form UI + const defaultHeaders = + defaultValues?.config && 'headers' in defaultValues.config + ? headersToArray(defaultValues.config.headers) + : []; + const form = useForm({ defaultValues: mergeDeepRight( { @@ -30,18 +72,68 @@ export function WebhookIntegrationForm({ type: 'webhook' as const, url: '', headers: {}, + mode: 'message' as const, + javascriptTemplate: undefined, }, }, defaultValues ?? {}, ), resolver: zodResolver(zCreateWebhookIntegration), }); + + // Use a separate form for headers array to work with useFieldArray + const headersForm = useForm<{ headers: { key: string; value: string }[] }>({ + defaultValues: { + headers: defaultHeaders, + }, + }); + + const headersArray = useFieldArray({ + control: headersForm.control, + name: 'headers', + }); + + // Watch headers array and sync to main form + const watchedHeaders = useWatch({ + control: headersForm.control, + name: 'headers', + }); + + // Sync headers array changes back to main form + useEffect(() => { + if (watchedHeaders) { + const validHeaders = watchedHeaders.filter( + (h): h is { key: string; value: string } => + h !== undefined && + typeof h.key === 'string' && + typeof h.value === 'string', + ); + form.setValue('config.headers', headersToRecord(validHeaders), { + shouldValidate: false, + }); + } + }, [watchedHeaders, form]); + + const mode = form.watch('config.mode'); const trpc = useTRPC(); const mutation = useMutation( trpc.integration.createOrUpdate.mutationOptions({ onSuccess, - onError() { - toast.error('Failed to create integration'); + onError(error) { + // Handle validation errors from tRPC + if (error.data?.code === 'BAD_REQUEST') { + const errorMessage = error.message || 'Invalid JavaScript template'; + toast.error(errorMessage); + // Set form error if it's a JavaScript template error + if (errorMessage.includes('JavaScript template')) { + form.setError('config.javascriptTemplate', { + type: 'manual', + message: errorMessage, + }); + } + } else { + toast.error('Failed to create integration'); + } }, }), ); @@ -70,7 +162,176 @@ export function WebhookIntegrationForm({ {...form.register('config.url')} error={path(['config', 'url', 'message'], form.formState.errors)} /> - + + +
+ {headersArray.fields.map((field, index) => ( +
+ + + +
+ ))} + +
+
+ + ( + + + + )} + /> + + {mode === 'javascript' && ( + ( + +

+ Write a JavaScript function that transforms the event + payload. The function receives payload as a + parameter and should return an object. +

+

+ Available in payload: +

+
    +
  • + payload.name - Event name +
  • +
  • + payload.profileId - User profile ID +
  • +
  • + payload.properties - Full properties object +
  • +
  • + payload.properties.your.property - Nested + property value +
  • +
  • + payload.profile.firstName - Profile property +
  • +
  • +
    + country + city + device + os + browser + path + createdAt + and more... +
    +
  • +
+

+ Available helpers: +

+
    +
  • + Math, Date, JSON,{' '} + Array, String,{' '} + Object +
  • +
+

+ Example: +

+
+                    {`(payload) => ({
+  event: payload.name,
+  user: payload.profileId,
+  data: payload.properties,
+  timestamp: new Date(payload.createdAt).toISOString(),
+  location: \`\${payload.city}, \${payload.country}\`
+})`}
+                  
+

+ Security: Network calls, file system + access, and other dangerous operations are blocked. +

+ + } + > + { + field.onChange(value); + // Clear error when user starts typing + if (form.formState.errors.config?.javascriptTemplate) { + form.clearErrors('config.javascriptTemplate'); + } + }} + placeholder={DEFAULT_TRANSFORMER} + minHeight="300px" + language="javascript" + /> + {form.formState.errors.config?.javascriptTemplate && ( +

+ {form.formState.errors.config.javascriptTemplate.message} +

+ )} +
+ )} + /> + )} + + ); } diff --git a/apps/start/src/components/json-editor.tsx b/apps/start/src/components/json-editor.tsx new file mode 100644 index 00000000..7cb711db --- /dev/null +++ b/apps/start/src/components/json-editor.tsx @@ -0,0 +1,218 @@ +'use client'; + +import { basicSetup } from 'codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { json } from '@codemirror/lang-json'; +import { oneDark } from '@codemirror/theme-one-dark'; +import { + Compartment, + EditorState, + type Extension, +} from '@codemirror/state'; +import { EditorView, keymap } from '@codemirror/view'; +import { useEffect, useRef, useState } from 'react'; +import { useTheme } from './theme-provider'; + +interface JsonEditorProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; + minHeight?: string; + language?: 'json' | 'javascript'; + onValidate?: (isValid: boolean, error?: string) => void; +} + +export function JsonEditor({ + value, + onChange, + placeholder = '{}', + className = '', + minHeight = '200px', + language = 'json', + onValidate, +}: JsonEditorProps) { + const editorRef = useRef(null); + const viewRef = useRef(null); + const themeCompartmentRef = useRef(null); + const languageCompartmentRef = useRef(null); + const { appTheme } = useTheme(); + const [isValid, setIsValid] = useState(true); + const [error, setError] = useState(); + const isUpdatingRef = useRef(false); + + const validateContent = (content: string) => { + if (!content.trim()) { + setIsValid(true); + setError(undefined); + onValidate?.(true); + return; + } + + if (language === 'json') { + try { + JSON.parse(content); + setIsValid(true); + setError(undefined); + onValidate?.(true); + } catch (e) { + setIsValid(false); + const errorMsg = + e instanceof Error ? e.message : 'Invalid JSON syntax'; + setError(errorMsg); + onValidate?.(false, errorMsg); + } + } else if (language === 'javascript') { + // No frontend validation for JavaScript - validation happens in tRPC + setIsValid(true); + setError(undefined); + onValidate?.(true); + } + }; + + // Create editor once on mount + useEffect(() => { + if (!editorRef.current || viewRef.current) return; + + const themeCompartment = new Compartment(); + themeCompartmentRef.current = themeCompartment; + + const languageCompartment = new Compartment(); + languageCompartmentRef.current = languageCompartment; + + const extensions: Extension[] = [ + basicSetup, + languageCompartment.of(language === 'javascript' ? [javascript()] : [json()]), + EditorState.tabSize.of(2), + EditorView.updateListener.of((update) => { + if (update.docChanged) { + isUpdatingRef.current = true; + const newValue = update.state.doc.toString(); + onChange(newValue); + validateContent(newValue); + + // Reset flag after a short delay + setTimeout(() => { + isUpdatingRef.current = false; + }, 0); + } + }), + EditorView.theme({ + '&': { + fontSize: '14px', + minHeight, + maxHeight: '400px', + }, + '&.cm-editor': { + borderRadius: '6px', + border: `1px solid ${ + isValid ? 'hsl(var(--border))' : 'hsl(var(--destructive))' + }`, + overflow: 'hidden', + }, + '.cm-scroller': { + minHeight, + maxHeight: '400px', + overflow: 'auto', + }, + '.cm-content': { + padding: '12px 12px 12px 0', + fontFamily: + 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace', + minHeight, + }, + '.cm-focused': { + outline: 'none', + }, + '.cm-gutters': { + backgroundColor: 'hsl(var(--muted))', + borderRight: '1px solid hsl(var(--border))', + paddingLeft: '8px', + }, + '.cm-lineNumbers .cm-gutterElement': { + color: 'hsl(var(--muted-foreground))', + paddingRight: '12px', + paddingLeft: '4px', + }, + }), + themeCompartment.of(appTheme === 'dark' ? [oneDark] : []), + ]; + + const state = EditorState.create({ + doc: value, + extensions, + }); + + const view = new EditorView({ + state, + parent: editorRef.current, + }); + + viewRef.current = view; + + // Initial validation + validateContent(value); + + return () => { + view.destroy(); + viewRef.current = null; + themeCompartmentRef.current = null; + }; + }, []); // Only create once + + // Update theme using compartment + useEffect(() => { + if (!viewRef.current || !themeCompartmentRef.current) return; + + viewRef.current.dispatch({ + effects: themeCompartmentRef.current.reconfigure( + appTheme === 'dark' ? [oneDark] : [], + ), + }); + }, [appTheme]); + + // Update language using compartment + useEffect(() => { + if (!viewRef.current || !languageCompartmentRef.current) return; + + viewRef.current.dispatch({ + effects: languageCompartmentRef.current.reconfigure( + language === 'javascript' ? [javascript()] : [json()], + ), + }); + validateContent(value); + }, [language, value]); + + // Update editor content when value changes externally + useEffect(() => { + if (!viewRef.current || isUpdatingRef.current) return; + + const currentContent = viewRef.current.state.doc.toString(); + if (currentContent !== value) { + viewRef.current.dispatch({ + changes: { + from: 0, + to: viewRef.current.state.doc.length, + insert: value, + }, + }); + + // Validate after external update + validateContent(value); + } + }, [value]); + + return ( +
+
+ {!isValid && ( +

+ {error || `Invalid ${language === 'javascript' ? 'JavaScript' : 'JSON'}. Please check your syntax.`} +

+ )} +
+ ); +} diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile index 99d35437..39d5c3fe 100644 --- a/apps/worker/Dockerfile +++ b/apps/worker/Dockerfile @@ -40,6 +40,7 @@ COPY packages/payments/package.json ./packages/payments/ COPY packages/constants/package.json ./packages/constants/ COPY packages/validation/package.json ./packages/validation/ COPY packages/integrations/package.json ./packages/integrations/ +COPY packages/js-runtime/package.json ./packages/js-runtime/ COPY patches ./patches # BUILD @@ -90,6 +91,7 @@ COPY --from=build /app/packages/payments ./packages/payments COPY --from=build /app/packages/constants ./packages/constants COPY --from=build /app/packages/validation ./packages/validation COPY --from=build /app/packages/integrations ./packages/integrations +COPY --from=build /app/packages/js-runtime ./packages/js-runtime COPY --from=build /app/tooling/typescript ./tooling/typescript RUN pnpm db:codegen diff --git a/apps/worker/package.json b/apps/worker/package.json index 7c3c1572..60366df0 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -17,6 +17,7 @@ "@openpanel/db": "workspace:*", "@openpanel/email": "workspace:*", "@openpanel/integrations": "workspace:^", + "@openpanel/js-runtime": "workspace:*", "@openpanel/json": "workspace:*", "@openpanel/logger": "workspace:*", "@openpanel/importer": "workspace:*", diff --git a/apps/worker/src/jobs/notification.ts b/apps/worker/src/jobs/notification.ts index ed076b38..702fa93e 100644 --- a/apps/worker/src/jobs/notification.ts +++ b/apps/worker/src/jobs/notification.ts @@ -1,11 +1,22 @@ import type { Job } from 'bullmq'; -import { db } from '@openpanel/db'; +import { Prisma, db } from '@openpanel/db'; import { sendDiscordNotification } from '@openpanel/integrations/src/discord'; import { sendSlackNotification } from '@openpanel/integrations/src/slack'; -import { setSuperJson } from '@openpanel/json'; +import { execute as executeJavaScriptTemplate } from '@openpanel/js-runtime'; import type { NotificationQueuePayload } from '@openpanel/queue'; -import { getRedisPub, publishEvent } from '@openpanel/redis'; +import { publishEvent } from '@openpanel/redis'; + +function isValidJson( + value: T | Prisma.NullableJsonNullValueInput | null | undefined, +): value is T { + return ( + value !== null && + value !== undefined && + value !== Prisma.JsonNull && + value !== Prisma.DbNull + ); +} export async function notificationJob(job: Job) { switch (job.data.type) { @@ -14,12 +25,10 @@ export async function notificationJob(job: Job) { if (notification.sendToApp) { publishEvent('notification', 'created', notification); - // empty for now return; } if (notification.sendToEmail) { - // empty for now return; } @@ -33,18 +42,44 @@ export async function notificationJob(job: Job) { }, }); + const payload = notification.payload; + + if (!isValidJson(payload)) { + return new Error('Invalid payload'); + } + switch (integration.config.type) { case 'webhook': { + let body: unknown; + + if (integration.config.mode === 'javascript') { + // We only transform event payloads for now (not funnel) + if ( + integration.config.javascriptTemplate && + payload.type === 'event' + ) { + const result = executeJavaScriptTemplate( + integration.config.javascriptTemplate, + payload.event, + ); + body = result; + } else { + body = payload; + } + } else { + body = { + title: notification.title, + message: notification.message, + }; + } + return fetch(integration.config.url, { method: 'POST', headers: { ...(integration.config.headers ?? {}), 'Content-Type': 'application/json', }, - body: JSON.stringify({ - title: notification.title, - message: notification.message, - }), + body: JSON.stringify(body), }); } case 'discord': { diff --git a/packages/js-runtime/index.ts b/packages/js-runtime/index.ts new file mode 100644 index 00000000..cba18435 --- /dev/null +++ b/packages/js-runtime/index.ts @@ -0,0 +1 @@ +export * from './src/index'; diff --git a/packages/js-runtime/package.json b/packages/js-runtime/package.json new file mode 100644 index 00000000..9fc70225 --- /dev/null +++ b/packages/js-runtime/package.json @@ -0,0 +1,21 @@ +{ + "name": "@openpanel/js-runtime", + "version": "0.0.1", + "type": "module", + "main": "index.ts", + "exports": { + ".": "./index.ts" + }, + "scripts": { + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@babel/parser": "^7.26.0" + }, + "devDependencies": { + "@openpanel/tsconfig": "workspace:*", + "typescript": "catalog:", + "vitest": "^2.1.8" + } +} diff --git a/packages/js-runtime/src/ast-walker.ts b/packages/js-runtime/src/ast-walker.ts new file mode 100644 index 00000000..25596556 --- /dev/null +++ b/packages/js-runtime/src/ast-walker.ts @@ -0,0 +1,165 @@ +import { ALLOWED_GLOBALS, ALLOWED_INSTANCE_METHODS, ALLOWED_METHODS } from './constants'; + +/** + * Simple recursive AST walker that doesn't require @babel/traverse + */ +export function walkNode( + node: unknown, + visitor: ( + node: Record, + parent?: Record, + ) => void, + parent?: Record, +): void { + if (!node || typeof node !== 'object') { + return; + } + + // Handle arrays + if (Array.isArray(node)) { + for (const child of node) { + walkNode(child, visitor, parent); + } + return; + } + + const nodeObj = node as Record; + + // Only visit AST nodes (they have a 'type' property) + if (typeof nodeObj.type === 'string') { + visitor(nodeObj, parent); + } + + // Recursively walk all properties + for (const key of Object.keys(nodeObj)) { + const value = nodeObj[key]; + if (value && typeof value === 'object') { + walkNode(value, visitor, nodeObj); + } + } +} + +/** + * Track declared variables/parameters to know what identifiers are "local" + */ +export function collectDeclaredIdentifiers(ast: unknown): Set { + const declared = new Set(); + + walkNode(ast, (node) => { + // Variable declarations: const x = ..., let y = ..., var z = ... + if (node.type === 'VariableDeclarator') { + const id = node.id as Record; + if (id.type === 'Identifier') { + declared.add(id.name as string); + } + // Handle destructuring patterns + if (id.type === 'ObjectPattern') { + collectPatternIdentifiers(id, declared); + } + if (id.type === 'ArrayPattern') { + collectPatternIdentifiers(id, declared); + } + } + + // Function parameters + if ( + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' || + node.type === 'FunctionDeclaration' + ) { + const params = node.params as Array>; + for (const param of params) { + collectPatternIdentifiers(param, declared); + } + } + }); + + return declared; +} + +/** + * Collect identifiers from destructuring patterns + */ +function collectPatternIdentifiers( + pattern: Record, + declared: Set, +): void { + if (pattern.type === 'Identifier') { + declared.add(pattern.name as string); + } else if (pattern.type === 'ObjectPattern') { + const properties = pattern.properties as Array>; + for (const prop of properties) { + if (prop.type === 'ObjectProperty') { + collectPatternIdentifiers( + prop.value as Record, + declared, + ); + } else if (prop.type === 'RestElement') { + collectPatternIdentifiers( + prop.argument as Record, + declared, + ); + } + } + } else if (pattern.type === 'ArrayPattern') { + const elements = pattern.elements as Array | null>; + for (const elem of elements) { + if (elem) { + collectPatternIdentifiers(elem, declared); + } + } + } else if (pattern.type === 'RestElement') { + collectPatternIdentifiers( + pattern.argument as Record, + declared, + ); + } else if (pattern.type === 'AssignmentPattern') { + // Default parameter values: (x = 5) => ... + collectPatternIdentifiers( + pattern.left as Record, + declared, + ); + } +} + +/** + * Check if an identifier is used as a property key (not a value reference) + */ +export function isPropertyKey( + node: Record, + parent?: Record, +): boolean { + if (!parent) return false; + + // Property in object literal: { foo: value } - foo is a key + if ( + parent.type === 'ObjectProperty' && + parent.key === node && + !parent.computed + ) { + return true; + } + + // Property access: obj.foo - foo is a property access, not a global reference + if ( + parent.type === 'MemberExpression' && + parent.property === node && + !parent.computed + ) { + return true; + } + + // Optional chaining: obj?.foo - foo is a property access + if ( + parent.type === 'OptionalMemberExpression' && + parent.property === node && + !parent.computed + ) { + return true; + } + + // Arrow function parameter used in callback: t => t.toUpperCase() + // The 't' in arrow function is already collected as declared identifier + + return false; +} diff --git a/packages/js-runtime/src/constants.ts b/packages/js-runtime/src/constants.ts new file mode 100644 index 00000000..c3e19f86 --- /dev/null +++ b/packages/js-runtime/src/constants.ts @@ -0,0 +1,141 @@ +/** + * Allowed global identifiers - super restricted allowlist + */ +export const ALLOWED_GLOBALS = new Set([ + // Basic values for comparisons + 'undefined', + 'null', + + // Type coercion functions + 'parseInt', + 'parseFloat', + 'isNaN', + 'isFinite', + + // Safe built-in objects (for static methods only) + 'Math', + 'Date', + 'JSON', +]); + +/** + * Allowed methods on built-in objects (static methods) + */ +export const ALLOWED_METHODS: Record> = { + // Math methods + Math: new Set(['abs', 'ceil', 'floor', 'round', 'min', 'max', 'random']), + + // JSON methods + JSON: new Set(['parse', 'stringify']), + + // Date static methods + Date: new Set(['now']), +}; + +/** + * Allowed instance methods (methods called on values, not on global objects) + * These are safe methods that can be called on any value + */ +export const ALLOWED_INSTANCE_METHODS = new Set([ + // Array instance methods + 'map', + 'filter', + 'reduce', + 'reduceRight', + 'forEach', + 'find', + 'findIndex', + 'findLast', + 'findLastIndex', + 'some', + 'every', + 'includes', + 'indexOf', + 'lastIndexOf', + 'slice', + 'concat', + 'join', + 'flat', + 'flatMap', + 'sort', + 'reverse', + 'fill', + 'at', + 'with', + 'toSorted', + 'toReversed', + 'toSpliced', + + // String instance methods + 'toLowerCase', + 'toUpperCase', + 'toLocaleLowerCase', + 'toLocaleUpperCase', + 'trim', + 'trimStart', + 'trimEnd', + 'padStart', + 'padEnd', + 'repeat', + 'replace', + 'replaceAll', + 'split', + 'substring', + 'substr', + 'charAt', + 'charCodeAt', + 'codePointAt', + 'startsWith', + 'endsWith', + 'match', + 'matchAll', + 'search', + 'normalize', + 'localeCompare', + + // Number instance methods + 'toFixed', + 'toPrecision', + 'toExponential', + 'toLocaleString', + + // Date instance methods + 'getTime', + 'getFullYear', + 'getMonth', + 'getDate', + 'getDay', + 'getHours', + 'getMinutes', + 'getSeconds', + 'getMilliseconds', + 'getUTCFullYear', + 'getUTCMonth', + 'getUTCDate', + 'getUTCDay', + 'getUTCHours', + 'getUTCMinutes', + 'getUTCSeconds', + 'getUTCMilliseconds', + 'getTimezoneOffset', + 'toISOString', + 'toJSON', + 'toDateString', + 'toTimeString', + 'toLocaleDateString', + 'toLocaleTimeString', + 'valueOf', + + // Object instance methods + 'hasOwnProperty', + 'toString', + 'valueOf', + + // RegExp instance methods + 'test', + 'exec', + + // Common property access (length, etc.) + 'length', + 'size', +]); diff --git a/packages/js-runtime/src/execute.ts b/packages/js-runtime/src/execute.ts new file mode 100644 index 00000000..a17918f1 --- /dev/null +++ b/packages/js-runtime/src/execute.ts @@ -0,0 +1,31 @@ +/** + * Executes a JavaScript function template + * @param code - JavaScript function code (arrow function or function expression) + * @param payload - Payload object to pass to the function + * @returns The result of executing the function + */ +export function execute( + code: string, + payload: Record, +): unknown { + try { + // Create the function code that will be executed + // 'use strict' ensures 'this' is undefined (not global object) + const funcCode = ` + 'use strict'; + return (${code})(payload); + `; + + // Create function with safe globals in scope + const func = new Function('payload', funcCode); + + // Execute the function + return func(payload); + } catch (error) { + throw new Error( + `Error executing JavaScript template: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +} diff --git a/packages/js-runtime/src/index.ts b/packages/js-runtime/src/index.ts new file mode 100644 index 00000000..05ad0ece --- /dev/null +++ b/packages/js-runtime/src/index.ts @@ -0,0 +1,2 @@ +export { validate } from './validate'; +export { execute } from './execute'; diff --git a/packages/js-runtime/src/validate.test.ts b/packages/js-runtime/src/validate.test.ts new file mode 100644 index 00000000..f7278af5 --- /dev/null +++ b/packages/js-runtime/src/validate.test.ts @@ -0,0 +1,332 @@ +import { describe, expect, it } from 'vitest'; +import { execute, validate } from './index'; + +describe('validate', () => { + describe('Valid templates', () => { + it('should accept arrow function', () => { + const result = validate('(payload) => ({ event: payload.name })'); + expect(result.valid).toBe(true); + }); + + it('should reject function expression', () => { + // Function expressions are not allowed - only arrow functions + const result = validate( + '(function(payload) { return { event: payload.name }; })', + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('arrow functions'); + }); + + it('should accept complex transformations', () => { + const code = `(payload) => ({ + event: payload.name, + user: payload.profileId, + data: payload.properties, + timestamp: new Date(payload.createdAt).toISOString() + })`; + const result = validate(code); + expect(result.valid).toBe(true); + }); + + it('should accept array operations', () => { + const code = `(payload) => ({ + tags: payload.tags?.map(t => t.toUpperCase()) || [] + })`; + const result = validate(code); + expect(result.valid).toBe(true); + }); + }); + + describe('Blocked operations', () => { + it('should block fetch calls', () => { + const result = validate('(payload) => fetch("https://evil.com")'); + expect(result.valid).toBe(false); + expect(result.error).toContain('fetch'); + }); + + it('should block XMLHttpRequest', () => { + const result = validate('(payload) => new XMLHttpRequest()'); + expect(result.valid).toBe(false); + expect(result.error).toContain('XMLHttpRequest'); + }); + + it('should block require calls', () => { + const result = validate('(payload) => require("fs")'); + expect(result.valid).toBe(false); + expect(result.error).toContain('require'); + }); + + it('should block import statements', () => { + const result = validate('import fs from "fs"; (payload) => ({})'); + expect(result.valid).toBe(false); + // Import statements cause multiple statements error first + expect(result.error).toContain('single function'); + }); + + it('should block eval calls', () => { + const result = validate('(payload) => eval("evil")'); + expect(result.valid).toBe(false); + expect(result.error).toContain('eval'); + }); + + it('should block setTimeout', () => { + const result = validate('(payload) => setTimeout(() => {}, 1000)'); + expect(result.valid).toBe(false); + expect(result.error).toContain('setTimeout'); + }); + + it('should block process access', () => { + const result = validate('(payload) => process.env.SECRET'); + expect(result.valid).toBe(false); + expect(result.error).toContain('process'); + }); + + it('should block while loops', () => { + const result = validate( + '(payload) => { while(true) {} return payload; }', + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('Loops are not allowed'); + }); + + it('should block for loops', () => { + const result = validate( + '(payload) => { for(let i = 0; i < 10; i++) {} return payload; }', + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('Loops are not allowed'); + }); + + it('should block try/catch', () => { + const result = validate( + '(payload) => { try { return payload; } catch(e) {} }', + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('try/catch'); + }); + + it('should block async/await', () => { + const result = validate( + 'async (payload) => { await something(); return payload; }', + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('async/await'); + }); + + it('should block classes', () => { + const result = validate('(payload) => { class Foo {} return payload; }'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Class'); + }); + + it('should block new Array()', () => { + const result = validate('(payload) => new Array(10)'); + expect(result.valid).toBe(false); + expect(result.error).toContain('new Array()'); + }); + + it('should block new Object()', () => { + const result = validate('(payload) => new Object()'); + expect(result.valid).toBe(false); + expect(result.error).toContain('new Object()'); + }); + + it('should allow new Date()', () => { + const result = validate( + '(payload) => ({ timestamp: new Date().toISOString() })', + ); + expect(result.valid).toBe(true); + }); + }); + + describe('Invalid syntax', () => { + it('should reject non-function code', () => { + const result = validate('const x = 1;'); + expect(result.valid).toBe(false); + expect(result.error).toContain('function'); + }); + + it('should reject invalid JavaScript', () => { + const result = validate('(payload) => { invalid syntax }'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Parse error'); + }); + }); +}); + +describe('execute', () => { + const basePayload = { + name: 'page_view', + profileId: 'user-123', + country: 'US', + city: 'New York', + device: 'desktop', + os: 'Windows', + browser: 'Chrome', + longitude: -73.935242, + latitude: 40.73061, + createdAt: '2024-01-15T10:30:00Z', + properties: { + plan: 'premium', + userId: 'user-456', + metadata: { + source: 'web', + campaign: 'summer-sale', + }, + }, + profile: { + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + }, + tags: ['tag1', 'tag2'], + }; + + describe('Basic transformations', () => { + it('should execute simple arrow function', () => { + const code = '(payload) => ({ event: payload.name })'; + const result = execute(code, basePayload); + expect(result).toEqual({ event: 'page_view' }); + }); + + it('should access nested properties', () => { + const code = '(payload) => ({ plan: payload.properties.plan })'; + const result = execute(code, basePayload); + expect(result).toEqual({ plan: 'premium' }); + }); + + it('should handle multiple properties', () => { + const code = `(payload) => ({ + event: payload.name, + user: payload.profileId, + location: payload.city + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ + event: 'page_view', + user: 'user-123', + location: 'New York', + }); + }); + }); + + describe('Date operations', () => { + it('should format dates', () => { + const code = `(payload) => ({ + timestamp: new Date(payload.createdAt).toISOString() + })`; + const result = execute(code, basePayload); + expect(result).toHaveProperty('timestamp'); + expect((result as { timestamp: string }).timestamp).toBe( + '2024-01-15T10:30:00.000Z', + ); + }); + }); + + describe('Array operations', () => { + it('should transform arrays', () => { + const code = `(payload) => ({ + tags: payload.tags?.map(t => t.toUpperCase()) || [] + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ tags: ['TAG1', 'TAG2'] }); + }); + + it('should filter arrays', () => { + const code = `(payload) => ({ + filtered: payload.tags?.filter(t => t.includes('tag1')) || [] + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ filtered: ['tag1'] }); + }); + }); + + describe('String operations', () => { + it('should use template literals', () => { + const code = `(payload) => ({ + location: \`\${payload.city}, \${payload.country}\` + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ location: 'New York, US' }); + }); + + it('should use string methods', () => { + const code = `(payload) => ({ + upperName: payload.name.toUpperCase() + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ upperName: 'PAGE_VIEW' }); + }); + }); + + describe('Math operations', () => { + it('should use Math functions', () => { + const code = `(payload) => ({ + roundedLng: Math.round(payload.longitude) + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ roundedLng: -74 }); + }); + }); + + describe('Conditional logic', () => { + it('should handle ternary operators', () => { + const code = `(payload) => ({ + plan: payload.properties?.plan || 'free' + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ plan: 'premium' }); + }); + + it('should handle if conditions', () => { + const code = `(payload) => { + const result = { event: payload.name }; + if (payload.properties?.plan === 'premium') { + result.plan = 'premium'; + } + return result; + }`; + const result = execute(code, basePayload); + expect(result).toEqual({ event: 'page_view', plan: 'premium' }); + }); + }); + + describe('Complex transformations', () => { + it('should handle nested object construction', () => { + const code = `(payload) => ({ + event: payload.name, + user: { + id: payload.profileId, + name: \`\${payload.profile?.firstName} \${payload.profile?.lastName}\` + }, + data: payload.properties, + meta: { + location: \`\${payload.city}, \${payload.country}\`, + device: payload.device + } + })`; + const result = execute(code, basePayload); + expect(result).toEqual({ + event: 'page_view', + user: { + id: 'user-123', + name: 'John Doe', + }, + data: basePayload.properties, + meta: { + location: 'New York, US', + device: 'desktop', + }, + }); + }); + }); + + describe('Error handling', () => { + it('should handle runtime errors gracefully', () => { + const code = '(payload) => payload.nonexistent.property'; + expect(() => { + execute(code, basePayload); + }).toThrow('Error executing JavaScript template'); + }); + }); +}); diff --git a/packages/js-runtime/src/validate.ts b/packages/js-runtime/src/validate.ts new file mode 100644 index 00000000..c52a523a --- /dev/null +++ b/packages/js-runtime/src/validate.ts @@ -0,0 +1,280 @@ +import { parse } from '@babel/parser'; + +import { + ALLOWED_GLOBALS, + ALLOWED_INSTANCE_METHODS, + ALLOWED_METHODS, +} from './constants'; +import { + collectDeclaredIdentifiers, + isPropertyKey, + walkNode, +} from './ast-walker'; + +/** + * Validates that a JavaScript function is safe to execute + * by checking the AST for allowed operations only (allowlist approach) + */ +export function validate(code: string): { + valid: boolean; + error?: string; +} { + if (!code || typeof code !== 'string') { + return { valid: false, error: 'Code must be a non-empty string' }; + } + + try { + // Parse the code to AST + const ast = parse(code, { + sourceType: 'module', + allowReturnOutsideFunction: true, + plugins: ['typescript'], + }); + + // Validate root structure: must be exactly one function expression + const program = ast.program; + const body = program.body; + + if (body.length === 0) { + return { valid: false, error: 'Code cannot be empty' }; + } + + if (body.length > 1) { + return { + valid: false, + error: + 'Code must contain only a single function. Multiple statements are not allowed.', + }; + } + + const rootStatement = body[0]!; + + // Must be an expression statement containing a function + if (rootStatement.type !== 'ExpressionStatement') { + if (rootStatement.type === 'VariableDeclaration') { + return { + valid: false, + error: + 'Variable declarations (const, let, var) are not allowed. Use a direct function expression instead.', + }; + } + if (rootStatement.type === 'FunctionDeclaration') { + return { + valid: false, + error: + 'Function declarations are not allowed. Use an arrow function or function expression instead: (payload) => { ... } or function(payload) { ... }', + }; + } + return { + valid: false, + error: 'Code must be a function expression or arrow function', + }; + } + + const rootExpression = rootStatement.expression; + if (rootExpression.type !== 'ArrowFunctionExpression') { + if (rootExpression.type === 'FunctionExpression') { + return { + valid: false, + error: + 'Function expressions are not allowed. Use arrow functions instead: (payload) => { ... }', + }; + } + return { + valid: false, + error: 'Code must be an arrow function, e.g.: (payload) => { ... }', + }; + } + + // Collect all declared identifiers (variables, parameters) + const declaredIdentifiers = collectDeclaredIdentifiers(ast); + + let validationError: string | undefined; + + // Walk the AST to check for allowed patterns only + walkNode(ast, (node, parent) => { + // Skip if we already found an error + if (validationError) { + return; + } + + // Block import/export declarations + if ( + node.type === 'ImportDeclaration' || + node.type === 'ExportDeclaration' + ) { + validationError = 'import/export statements are not allowed'; + return; + } + + // Block function declarations inside the function body + // (FunctionDeclaration creates a named function, not allowed) + if (node.type === 'FunctionDeclaration') { + validationError = + 'Named function declarations are not allowed inside the function body.'; + return; + } + + // Block loops - use array methods like .map(), .filter() instead + if ( + node.type === 'WhileStatement' || + node.type === 'DoWhileStatement' || + node.type === 'ForStatement' || + node.type === 'ForInStatement' || + node.type === 'ForOfStatement' + ) { + validationError = + 'Loops are not allowed. Use array methods like .map(), .filter(), .reduce() instead.'; + return; + } + + // Block advanced/dangerous features + if (node.type === 'TryStatement') { + validationError = 'try/catch statements are not allowed'; + return; + } + + if (node.type === 'ThrowStatement') { + validationError = 'throw statements are not allowed'; + return; + } + + if (node.type === 'WithStatement') { + validationError = 'with statements are not allowed'; + return; + } + + if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') { + validationError = 'Class definitions are not allowed'; + return; + } + + if (node.type === 'AwaitExpression') { + validationError = 'async/await is not allowed'; + return; + } + + if (node.type === 'YieldExpression') { + validationError = 'Generators are not allowed'; + return; + } + + // Block 'this' keyword - arrow functions don't have their own 'this' + // but we block it entirely to prevent any scope leakage + if (node.type === 'ThisExpression') { + validationError = + "'this' keyword is not allowed. Use the payload parameter instead."; + return; + } + + // Check identifiers that reference globals + if (node.type === 'Identifier') { + const name = node.name as string; + + // Block 'arguments' - not available in arrow functions anyway + // but explicitly block to prevent any confusion + if (name === 'arguments') { + validationError = + "'arguments' is not allowed. Use explicit parameters instead."; + return; + } + + // Skip if it's a property key (not a value reference) + if (isPropertyKey(node, parent)) { + return; + } + + // Skip if it's a declared local variable/parameter + if (declaredIdentifiers.has(name)) { + return; + } + + // Check if it's an allowed global + if (!ALLOWED_GLOBALS.has(name)) { + validationError = `Use of '${name}' is not allowed. Only safe built-in functions are permitted.`; + return; + } + } + + // Check method calls on global objects (like Math.random, JSON.parse) + // Handles both regular calls and optional chaining (?.) + if ( + node.type === 'CallExpression' || + node.type === 'OptionalCallExpression' + ) { + const callee = node.callee as Record; + const isMemberExpr = + callee.type === 'MemberExpression' || + callee.type === 'OptionalMemberExpression'; + + if (isMemberExpr) { + const obj = callee.object as Record; + const prop = callee.property as Record; + const computed = callee.computed as boolean; + + // Static method call on global object: Math.random(), JSON.parse() + if ( + obj.type === 'Identifier' && + prop.type === 'Identifier' && + !computed + ) { + const objName = obj.name as string; + const methodName = prop.name as string; + + // Check if it's a call on an allowed global object + if (ALLOWED_GLOBALS.has(objName) && ALLOWED_METHODS[objName]) { + if (!ALLOWED_METHODS[objName].has(methodName)) { + validationError = `Method '${objName}.${methodName}' is not allowed. Only safe methods are permitted.`; + return; + } + } + } + + // Instance method call: arr.map(), str.toLowerCase(), arr?.map() + // We allow these if the method name is in ALLOWED_INSTANCE_METHODS + if (prop.type === 'Identifier' && !computed) { + const methodName = prop.name as string; + + // If calling on something other than an allowed global, + // check if the method is in the allowed instance methods + if ( + obj.type !== 'Identifier' || + !ALLOWED_GLOBALS.has(obj.name as string) + ) { + if (!ALLOWED_INSTANCE_METHODS.has(methodName)) { + validationError = `Method '.${methodName}()' is not allowed. Only safe methods are permitted.`; + return; + } + } + } + } + } + + // Check 'new' expressions - only allow new Date() + if (node.type === 'NewExpression') { + const callee = node.callee as Record; + if (callee.type === 'Identifier') { + const name = callee.name as string; + if (name !== 'Date') { + validationError = `'new ${name}()' is not allowed. Only 'new Date()' is permitted.`; + return; + } + } + } + }); + + if (validationError) { + return { valid: false, error: validationError }; + } + + return { valid: true }; + } catch (error) { + return { + valid: false, + error: + error instanceof Error + ? `Parse error: ${error.message}` + : 'Unknown parse error', + }; + } +} diff --git a/packages/js-runtime/tsconfig.json b/packages/js-runtime/tsconfig.json new file mode 100644 index 00000000..a291eef2 --- /dev/null +++ b/packages/js-runtime/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@openpanel/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/packages/trpc/package.json b/packages/trpc/package.json index e5ade19c..8199e253 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -14,6 +14,7 @@ "@openpanel/db": "workspace:*", "@openpanel/email": "workspace:*", "@openpanel/integrations": "workspace:^", + "@openpanel/js-runtime": "workspace:*", "@openpanel/payments": "workspace:^", "@openpanel/redis": "workspace:*", "@openpanel/validation": "workspace:*", diff --git a/packages/trpc/src/routers/integration.ts b/packages/trpc/src/routers/integration.ts index a25fc586..0c454c01 100644 --- a/packages/trpc/src/routers/integration.ts +++ b/packages/trpc/src/routers/integration.ts @@ -10,8 +10,9 @@ import { zCreateWebhookIntegration, } from '@openpanel/validation'; import { getOrganizationAccess } from '../access'; -import { TRPCAccessError } from '../errors'; +import { TRPCAccessError, TRPCBadRequestError } from '../errors'; import { createTRPCRouter, protectedProcedure } from '../trpc'; +import { validate as validateJavaScriptTemplate } from '@openpanel/js-runtime'; export const integrationRouter = createTRPCRouter({ get: protectedProcedure @@ -93,6 +94,22 @@ export const integrationRouter = createTRPCRouter({ createOrUpdate: protectedProcedure .input(z.union([zCreateDiscordIntegration, zCreateWebhookIntegration])) .mutation(async ({ input }) => { + // Validate JavaScript template if mode is javascript + if ( + input.config.type === 'webhook' && + input.config.mode === 'javascript' && + input.config.javascriptTemplate + ) { + const validation = validateJavaScriptTemplate( + input.config.javascriptTemplate, + ); + if (!validation.valid) { + throw TRPCBadRequestError( + `Invalid JavaScript template: ${validation.error}`, + ); + } + } + if (input.id) { return db.integration.update({ where: { diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 0b101226..5ef2ef9b 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -369,6 +369,8 @@ export const zWebhookConfig = z.object({ url: z.string().url(), headers: z.record(z.string()), payload: z.record(z.string(), z.unknown()).optional(), + mode: z.enum(['message', 'javascript']).default('message'), + javascriptTemplate: z.string().optional(), }); export type IWebhookConfig = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fb2d4d7..e498cc92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -396,6 +396,24 @@ importers: '@ai-sdk/react': specifier: ^1.2.5 version: 1.2.5(react@19.2.3)(zod@3.24.2) + '@codemirror/commands': + specifier: ^6.7.0 + version: 6.10.1 + '@codemirror/lang-javascript': + specifier: ^6.2.0 + version: 6.2.4 + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.2 + '@codemirror/state': + specifier: ^6.4.0 + version: 6.5.4 + '@codemirror/theme-one-dark': + specifier: ^6.1.3 + version: 6.1.3 + '@codemirror/view': + specifier: ^6.35.0 + version: 6.39.11 '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -603,6 +621,9 @@ importers: cmdk: specifier: ^0.2.1 version: 0.2.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + codemirror: + specifier: ^6.0.1 + version: 6.0.2 d3: specifier: ^7.8.5 version: 7.8.5 @@ -874,6 +895,9 @@ importers: '@openpanel/integrations': specifier: workspace:^ version: link:../../packages/integrations + '@openpanel/js-runtime': + specifier: workspace:* + version: link:../../packages/js-runtime '@openpanel/json': specifier: workspace:* version: link:../../packages/json @@ -1276,6 +1300,22 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/js-runtime: + dependencies: + '@babel/parser': + specifier: ^7.26.0 + version: 7.28.5 + devDependencies: + '@openpanel/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + typescript: + specifier: 'catalog:' + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@24.10.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.27.1) + packages/json: dependencies: superjson: @@ -1614,6 +1654,9 @@ importers: '@openpanel/integrations': specifier: workspace:^ version: link:../integrations + '@openpanel/js-runtime': + specifier: workspace:* + version: link:../js-runtime '@openpanel/payments': specifier: workspace:^ version: link:../payments @@ -2041,11 +2084,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.26.3': - resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.28.3': resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} engines: {node: '>=6.0.0'} @@ -2902,6 +2940,36 @@ packages: cpu: [x64] os: [win32] + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.1': + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.1': + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} + + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} + + '@codemirror/search@6.6.0': + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + + '@codemirror/state@6.5.4': + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.39.11': + resolution: {integrity: sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -4711,6 +4779,21 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.7': + resolution: {integrity: sha512-wNIFWdSUfX9Jc6ePMzxSPVgTVB4EOfDIwLQLWASyiUdHKaMsiilj9bYiGkGQCKVodd0x6bgQCV207PILGFCF9Q==} + '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -4720,6 +4803,9 @@ packages: engines: {node: '>=18'} hasBin: true + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@maxmind/geoip2-node@6.1.0': resolution: {integrity: sha512-yJWQNxKRqPaGKorzpDKZBAR+gLk80XnZ3w/fhLEdfGnqls+Rv3ui0qQSd3akpvAC2NX8cjn7VJ5xxrlVw7i6KA==} @@ -9304,9 +9390,23 @@ packages: '@vitest/expect@1.6.1': resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.1.3': resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.1.3': resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==} peerDependencies: @@ -9318,30 +9418,45 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.1.3': resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==} '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.1.3': resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==} '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.1.3': resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==} '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.1.3': resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==} '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.1.3': resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} @@ -10283,6 +10398,9 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -10482,6 +10600,9 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -16395,6 +16516,9 @@ packages: structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -16664,6 +16788,10 @@ packages: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -17470,6 +17598,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.1.3: resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -17773,6 +17906,31 @@ packages: jsdom: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.1.3: resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -17834,6 +17992,9 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -18314,7 +18475,7 @@ snapshots: '@astrojs/telemetry@3.2.1': dependencies: ci-info: 4.2.0 - debug: 4.4.1 + debug: 4.4.3 dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -18329,7 +18490,7 @@ snapshots: '@babel/code-frame@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -18351,10 +18512,10 @@ snapshots: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.24.5) '@babel/helpers': 7.26.0 - '@babel/parser': 7.26.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -18371,9 +18532,9 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helpers': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 @@ -18405,16 +18566,16 @@ snapshots: '@babel/generator@7.26.3': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.0.2 '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.0.2 @@ -18489,7 +18650,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -18532,7 +18693,7 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1 + debug: 4.4.3 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -18557,7 +18718,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -18571,14 +18732,14 @@ snapshots: '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -18587,8 +18748,8 @@ snapshots: dependencies: '@babel/core': 7.24.5 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -18596,8 +18757,8 @@ snapshots: dependencies: '@babel/core': 7.28.3 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -18605,8 +18766,8 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -18646,7 +18807,7 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -18655,7 +18816,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -18669,7 +18830,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -18697,12 +18858,12 @@ snapshots: '@babel/helpers@7.26.0': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/helpers@7.28.4': dependencies: @@ -18711,17 +18872,13 @@ snapshots: '@babel/highlight@7.23.4': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 '@babel/parser@7.24.5': dependencies: - '@babel/types': 7.28.2 - - '@babel/parser@7.26.3': - dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/parser@7.28.3': dependencies: @@ -19659,7 +19816,7 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/types': 7.28.2 '@babel/traverse@7.28.3': @@ -19667,7 +19824,7 @@ snapshots: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.2 debug: 4.4.1 @@ -19861,6 +20018,74 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260111.0': optional: true + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + + '@codemirror/commands@6.10.1': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.1': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.7 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.2': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + crelt: 1.0.6 + + '@codemirror/search@6.6.0': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + crelt: 1.0.6 + + '@codemirror/state@6.5.4': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.39.11': + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@colors/colors@1.6.0': {} '@cspotcode/source-map-support@0.8.1': @@ -20685,7 +20910,7 @@ snapshots: '@expo/sdk-runtime-versions': 1.0.0 '@react-native/normalize-color': 2.1.0 chalk: 4.1.2 - debug: 4.4.1 + debug: 4.4.3 find-up: 5.0.0 getenv: 1.0.0 glob: 7.1.6 @@ -20748,7 +20973,7 @@ snapshots: dependencies: '@expo/spawn-async': 1.7.2 chalk: 4.1.2 - debug: 4.4.1 + debug: 4.4.3 find-up: 5.0.0 minimatch: 3.1.2 p-limit: 3.1.0 @@ -21343,12 +21568,34 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color '@kwsites/promise-deferred@1.1.1': {} + '@lezer/common@1.5.0': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.0 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.7 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.7 + + '@lezer/lr@1.4.7': + dependencies: + '@lezer/common': 1.5.0 + '@lukeed/ms@2.0.2': {} '@mapbox/node-pre-gyp@2.0.0': @@ -21364,6 +21611,8 @@ snapshots: - encoding - supports-color + '@marijn/find-cluster-break@1.0.2': {} + '@maxmind/geoip2-node@6.1.0': dependencies: maxmind: 4.3.25 @@ -24526,7 +24775,7 @@ snapshots: '@react-native/codegen@0.73.3(@babel/preset-env@7.23.9(@babel/core@7.28.3))': dependencies: - '@babel/parser': 7.26.3 + '@babel/parser': 7.28.5 '@babel/preset-env': 7.23.9(@babel/core@7.28.3) flow-parser: 0.206.0 glob: 7.2.3 @@ -25635,7 +25884,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.3 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 '@tanstack/router-utils': 1.132.51 babel-dead-code-elimination: 1.0.10 @@ -25881,7 +26130,7 @@ snapshots: '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.2 '@tanstack/router-core': 1.132.47 '@tanstack/router-generator': 1.132.51 @@ -25906,7 +26155,7 @@ snapshots: dependencies: '@babel/core': 7.28.3 '@babel/generator': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/preset-typescript': 7.27.1(@babel/core@7.28.3) ansis: 4.2.0 diff: 8.0.2 @@ -25922,8 +26171,8 @@ snapshots: '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@tanstack/directive-functions-plugin': 1.132.53(vite@6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2)) babel-dead-code-elimination: 1.0.10 tiny-invariant: 1.3.3 @@ -26756,6 +27005,13 @@ snapshots: '@vitest/utils': 1.6.1 chai: 4.5.0 + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + '@vitest/expect@3.1.3': dependencies: '@vitest/spy': 3.1.3 @@ -26763,6 +27019,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1) + '@vitest/mocker@3.1.3(vite@6.3.3(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.1.3 @@ -26771,6 +27035,10 @@ snapshots: optionalDependencies: vite: 6.3.3(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.1.3': dependencies: tinyrainbow: 2.0.0 @@ -26781,6 +27049,11 @@ snapshots: p-limit: 5.0.0 pathe: 1.1.2 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.1.3': dependencies: '@vitest/utils': 3.1.3 @@ -26792,6 +27065,12 @@ snapshots: pathe: 1.1.2 pretty-format: 29.7.0 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/snapshot@3.1.3': dependencies: '@vitest/pretty-format': 3.1.3 @@ -26802,6 +27081,10 @@ snapshots: dependencies: tinyspy: 2.2.1 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.1.3': dependencies: tinyspy: 3.0.2 @@ -26813,6 +27096,12 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 + '@vitest/utils@3.1.3': dependencies: '@vitest/pretty-format': 3.1.3 @@ -26866,7 +27155,7 @@ snapshots: '@vue/compiler-core@3.5.20': dependencies: - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@vue/shared': 3.5.20 entities: 4.5.0 estree-walker: 2.0.2 @@ -26892,7 +27181,7 @@ snapshots: '@vue/compiler-sfc@3.5.20': dependencies: - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@vue/compiler-core': 3.5.20 '@vue/compiler-dom': 3.5.20 '@vue/compiler-ssr': 3.5.20 @@ -27033,7 +27322,7 @@ snapshots: agent-base@7.1.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -27212,7 +27501,7 @@ snapshots: ast-kit@2.1.2: dependencies: - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 pathe: 2.0.3 ast-kit@2.2.0: @@ -27392,8 +27681,8 @@ snapshots: babel-dead-code-elimination@1.0.10: dependencies: '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 - '@babel/traverse': 7.28.3 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -28065,6 +28354,16 @@ snapshots: transitivePeerDependencies: - '@types/react' + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + collapse-white-space@2.1.0: {} color-convert@1.9.3: @@ -28250,6 +28549,8 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.5.2 + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -31301,7 +31602,7 @@ snapshots: jscodeshift@0.14.0(@babel/preset-env@7.23.9(@babel/core@7.28.3)): dependencies: '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.28.3) '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.28.3) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.28.3) @@ -31624,7 +31925,7 @@ snapshots: mlly: 1.8.0 node-forge: 1.3.1 pathe: 1.1.2 - std-env: 3.9.0 + std-env: 3.10.0 ufo: 1.6.1 untun: 0.1.3 uqr: 0.1.2 @@ -31802,8 +32103,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 source-map-js: 1.2.1 magicast@0.5.1: @@ -32152,8 +32453,8 @@ snapshots: metro-source-map@0.80.6: dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 invariant: 2.2.4 metro-symbolicate: 0.80.6 nullthrows: 1.1.1 @@ -32177,9 +32478,9 @@ snapshots: metro-transform-plugins@0.80.6: dependencies: '@babel/core': 7.28.3 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color @@ -32187,7 +32488,7 @@ snapshots: metro-transform-worker@0.80.6: dependencies: '@babel/core': 7.28.3 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 metro: 0.80.6 @@ -32208,11 +32509,11 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.3 - '@babel/generator': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -32514,7 +32815,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.1 @@ -34936,7 +35237,7 @@ snapshots: require-in-the-middle@7.4.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -35075,7 +35376,7 @@ snapshots: rolldown-plugin-dts@0.15.9(rolldown@1.0.0-beta.43)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/types': 7.28.2 ast-kit: 2.1.2 birpc: 2.5.0 @@ -35376,7 +35677,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -35625,7 +35926,7 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -35914,6 +36215,8 @@ snapshots: structured-headers@0.4.1: {} + style-mod@4.1.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -35955,7 +36258,7 @@ snapshots: sucrase@3.34.0: dependencies: - '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 7.1.6 lines-and-columns: 1.2.4 @@ -36233,6 +36536,8 @@ snapshots: tinypool@1.0.2: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@2.2.1: {} @@ -36988,6 +37293,24 @@ snapshots: - supports-color - terser + vite-node@2.1.9(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.1.3(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -37047,7 +37370,7 @@ snapshots: vite-plugin-inspect@11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2)): dependencies: ansis: 4.2.0 - debug: 4.4.1 + debug: 4.4.3 error-stack-parser-es: 1.0.5 ohash: 2.0.11 open: 10.2.0 @@ -37093,6 +37416,17 @@ snapshots: lightningcss: 1.30.2 terser: 5.27.1 + vite@5.4.21(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.5 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + lightningcss: 1.30.2 + terser: 5.27.1 + vite@6.3.3(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2): dependencies: esbuild: 0.25.9 @@ -37204,6 +37538,42 @@ snapshots: - supports-color - terser + vitest@2.1.9(@types/node@24.10.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.27.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.3 + expect-type: 1.2.1 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1) + vite-node: 2.1.9(@types/node@24.10.1)(lightningcss@1.30.2)(terser@5.27.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.1 + jsdom: 26.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.1.3(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.27.1)(tsx@4.20.5)(yaml@2.8.2): dependencies: '@vitest/expect': 3.1.3 @@ -37277,6 +37647,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0