diff --git a/apps/bridge-deltachat/eslint.config.mjs b/apps/bridge-deltachat/eslint.config.mjs new file mode 100644 index 0000000..2997d1b --- /dev/null +++ b/apps/bridge-deltachat/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@link-stack/eslint-config/node"; + +export default config; diff --git a/apps/bridge-deltachat/package.json b/apps/bridge-deltachat/package.json index 9960ef2..9ce251c 100644 --- a/apps/bridge-deltachat/package.json +++ b/apps/bridge-deltachat/package.json @@ -4,23 +4,31 @@ "main": "build/main/index.js", "author": "Darren Clarke ", "license": "AGPL-3.0-or-later", + "prettier": "@link-stack/prettier-config", "dependencies": { "@deltachat/jsonrpc-client": "^1.151.1", "@deltachat/stdio-rpc-server": "^1.151.1", "@hono/node-server": "^1.13.8", "hono": "^4.7.4", - "pino": "^9.6.0", - "pino-pretty": "^13.0.0" + "@link-stack/logger": "workspace:*" }, "devDependencies": { + "@link-stack/eslint-config": "workspace:*", + "@link-stack/prettier-config": "workspace:*", + "@link-stack/typescript-config": "workspace:*", "@types/node": "*", "dotenv-cli": "^10.0.0", + "eslint": "^9.23.0", + "prettier": "^3.5.3", "tsx": "^4.20.6", "typescript": "^5.9.3" }, "scripts": { "build": "tsc -p tsconfig.json", "dev": "dotenv -- tsx src/index.ts", - "start": "node build/main/index.js" + "start": "node build/main/index.js", + "lint": "eslint src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/" } } diff --git a/apps/bridge-deltachat/src/attachments.ts b/apps/bridge-deltachat/src/attachments.ts index abe4bde..850b782 100644 --- a/apps/bridge-deltachat/src/attachments.ts +++ b/apps/bridge-deltachat/src/attachments.ts @@ -11,9 +11,9 @@ */ export function getMaxAttachmentSize(): number { const envValue = process.env.BRIDGE_MAX_ATTACHMENT_SIZE_MB; - const sizeInMB = envValue ? parseInt(envValue, 10) : 50; + const sizeInMB = envValue ? Number.parseInt(envValue, 10) : 50; - if (isNaN(sizeInMB) || sizeInMB <= 0) { + if (Number.isNaN(sizeInMB) || sizeInMB <= 0) { console.warn(`Invalid BRIDGE_MAX_ATTACHMENT_SIZE_MB value: ${envValue}, using default 50MB`); return 50 * 1024 * 1024; } diff --git a/apps/bridge-deltachat/src/index.ts b/apps/bridge-deltachat/src/index.ts index c8841e9..729b8b0 100644 --- a/apps/bridge-deltachat/src/index.ts +++ b/apps/bridge-deltachat/src/index.ts @@ -1,7 +1,8 @@ import { serve } from "@hono/node-server"; -import DeltaChatService from "./service.ts"; +import { createLogger } from "@link-stack/logger"; + import { createRoutes } from "./routes.ts"; -import { createLogger } from "./lib/logger"; +import DeltaChatService from "./service.ts"; const logger = createLogger("bridge-deltachat-index"); @@ -10,7 +11,7 @@ const main = async () => { await service.initialize(); const app = createRoutes(service); - const port = parseInt(process.env.PORT || "5001", 10); + const port = Number.parseInt(process.env.PORT || "5001", 10); serve({ fetch: app.fetch, port }, (info) => { logger.info({ port: info.port }, "bridge-deltachat listening"); @@ -26,7 +27,7 @@ const main = async () => { process.on("SIGINT", shutdown); }; -main().catch((err) => { - logger.error(err); +main().catch((error) => { + logger.error(error); process.exit(1); }); diff --git a/apps/bridge-deltachat/src/lib/logger.ts b/apps/bridge-deltachat/src/lib/logger.ts deleted file mode 100644 index c72a3c9..0000000 --- a/apps/bridge-deltachat/src/lib/logger.ts +++ /dev/null @@ -1,77 +0,0 @@ -import pino, { Logger as PinoLogger, LoggerOptions } from 'pino'; - -export type Logger = PinoLogger; - -const getLogLevel = (): string => { - return process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'); -}; - -const getPinoConfig = (): LoggerOptions => { - const isDevelopment = process.env.NODE_ENV !== 'production'; - - const baseConfig: LoggerOptions = { - level: getLogLevel(), - formatters: { - level: (label) => { - return { level: label.toUpperCase() }; - }, - }, - timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, - redact: { - paths: [ - 'password', - 'token', - 'secret', - 'api_key', - 'apiKey', - 'authorization', - 'cookie', - 'access_token', - 'refresh_token', - '*.password', - '*.token', - '*.secret', - '*.api_key', - '*.apiKey', - '*.authorization', - '*.cookie', - '*.access_token', - '*.refresh_token', - 'headers.authorization', - 'headers.cookie', - 'headers.Authorization', - 'headers.Cookie', - 'credentials.password', - 'credentials.secret', - 'credentials.token', - ], - censor: '[REDACTED]', - }, - }; - - if (isDevelopment) { - return { - ...baseConfig, - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:standard', - ignore: 'pid,hostname', - singleLine: false, - messageFormat: '{msg}', - }, - }, - }; - } - - return baseConfig; -}; - -export const logger: Logger = pino(getPinoConfig()); - -export const createLogger = (name: string, context?: Record): Logger => { - return logger.child({ name, ...context }); -}; - -export default logger; diff --git a/apps/bridge-deltachat/src/routes.ts b/apps/bridge-deltachat/src/routes.ts index 3480d50..a4d6d6d 100644 --- a/apps/bridge-deltachat/src/routes.ts +++ b/apps/bridge-deltachat/src/routes.ts @@ -1,9 +1,12 @@ +import { createLogger } from "@link-stack/logger"; import { Hono } from "hono"; + import type DeltaChatService from "./service.ts"; -import { createLogger } from "./lib/logger"; const logger = createLogger("bridge-deltachat-routes"); +const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error)); + export function createRoutes(service: DeltaChatService): Hono { const app = new Hono(); @@ -15,9 +18,9 @@ export function createRoutes(service: DeltaChatService): Hono { const result = await service.configure(id, email, password); logger.info({ id, email }, "Bot configured"); return c.json(result); - } catch (err: any) { - logger.error({ id, error: err.message }, "Failed to configure bot"); - return c.json({ error: err.message }, 500); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to configure bot"); + return c.json({ error: errorMessage(error) }, 500); } }); @@ -34,9 +37,14 @@ export function createRoutes(service: DeltaChatService): Hono { attachments?: Array<{ data: string; filename: string; mime_type: string }>; }>(); - const result = await service.send(id, email, message, attachments); - logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message"); - return c.json({ result }); + try { + const result = await service.send(id, email, message, attachments); + logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message"); + return c.json({ result }); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to send message"); + return c.json({ error: errorMessage(error) }, 500); + } }); app.post("/api/bots/:id/unconfigure", async (c) => { diff --git a/apps/bridge-deltachat/src/service.ts b/apps/bridge-deltachat/src/service.ts index 7dca25c..17b37e0 100644 --- a/apps/bridge-deltachat/src/service.ts +++ b/apps/bridge-deltachat/src/service.ts @@ -1,13 +1,11 @@ -import { startDeltaChat, DeltaChat } from "@deltachat/stdio-rpc-server"; -import fs from "fs"; -import path from "path"; -import os from "os"; -import { createLogger } from "./lib/logger"; -import { - getMaxAttachmentSize, - getMaxTotalAttachmentSize, - MAX_ATTACHMENTS, -} from "./attachments"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { startDeltaChat, type DeltaChatOverJsonRpcServer } from "@deltachat/stdio-rpc-server"; +import { createLogger } from "@link-stack/logger"; + +import { getMaxAttachmentSize, getMaxTotalAttachmentSize, MAX_ATTACHMENTS } from "./attachments"; const logger = createLogger("bridge-deltachat-service"); @@ -16,7 +14,7 @@ interface BotMapping { } export default class DeltaChatService { - private dc: DeltaChat | null = null; + private dc: DeltaChatOverJsonRpcServer | null = null; private botMapping: BotMapping = {}; private dataDir: string; private mappingFile: string; @@ -47,8 +45,8 @@ export default class DeltaChatService { logger.warn({ botId, accountId }, "Account not configured, removing from mapping"); delete this.botMapping[botId]; } - } catch (err) { - logger.error({ botId, accountId, err }, "Failed to resume bot, removing from mapping"); + } catch (error) { + logger.error({ botId, accountId, err: error }, "Failed to resume bot, removing from mapping"); delete this.botMapping[botId]; } } @@ -63,8 +61,8 @@ export default class DeltaChatService { try { await this.dc.rpc.stopIo(accountId); logger.info({ botId, accountId }, "Stopped IO for bot"); - } catch (err) { - logger.error({ botId, accountId, err }, "Error stopping IO"); + } catch (error) { + logger.error({ botId, accountId, err: error }, "Error stopping IO"); } } this.dc.close(); @@ -75,18 +73,18 @@ export default class DeltaChatService { private loadBotMapping(): void { if (fs.existsSync(this.mappingFile)) { try { - const data = fs.readFileSync(this.mappingFile, "utf-8"); + const data = fs.readFileSync(this.mappingFile, "utf8"); this.botMapping = JSON.parse(data); logger.info({ botCount: Object.keys(this.botMapping).length }, "Loaded bot mapping"); - } catch (err) { - logger.error({ err }, "Failed to load bot mapping, starting fresh"); + } catch (error) { + logger.error({ err: error }, "Failed to load bot mapping, starting fresh"); this.botMapping = {}; } } } private saveBotMapping(): void { - fs.writeFileSync(this.mappingFile, JSON.stringify(this.botMapping, null, 2), "utf-8"); + fs.writeFileSync(this.mappingFile, JSON.stringify(this.botMapping, null, 2), "utf8"); } private validateBotId(id: string): void { @@ -102,29 +100,14 @@ export default class DeltaChatService { private registerEventListeners(): void { if (!this.dc) return; - const dc = this.dc; - - (async () => { - for await (const event of dc.events) { - try { - if (event.kind === "IncomingMsg") { - const { accountId, chatId, msgId } = event; - await this.handleIncomingMessage(accountId, chatId, msgId); - } - } catch (err) { - logger.error({ err, event: event.kind }, "Error handling event"); - } - } - })().catch((err) => { - logger.error({ err }, "Event listener loop exited"); + this.dc.on("IncomingMsg", (accountId, event) => { + this.handleIncomingMessage(accountId, event.chatId, event.msgId).catch((error) => { + logger.error({ err: error, accountId }, "Error handling incoming message"); + }); }); } - private async handleIncomingMessage( - accountId: number, - chatId: number, - msgId: number, - ): Promise { + private async handleIncomingMessage(accountId: number, chatId: number, msgId: number): Promise { if (!this.dc) return; const botId = this.getBotIdForAccount(accountId); @@ -135,9 +118,10 @@ export default class DeltaChatService { const msg = await this.dc.rpc.getMessage(accountId, msgId); - // Skip bot messages and non-chat messages (plain email) - if (msg.isBot || !msg.isIncoming) { - logger.debug({ msgId, isBot: msg.isBot, isIncoming: msg.isIncoming }, "Skipping message"); + // Incoming states: 10=fresh, 13=noticed, 16=seen + const isIncoming = msg.state === 10 || msg.state === 13 || msg.state === 16; + if (msg.isBot || !isIncoming) { + logger.debug({ msgId, isBot: msg.isBot, state: msg.state }, "Skipping message"); return; } @@ -159,8 +143,8 @@ export default class DeltaChatService { filename = msg.fileName || path.basename(msg.file); mimeType = msg.fileMime || "application/octet-stream"; logger.info({ filename, mimeType, size: fileData.length }, "Attachment found"); - } catch (err) { - logger.error({ err, file: msg.file }, "Failed to read attachment file"); + } catch (error) { + logger.error({ err: error, file: msg.file }, "Failed to read attachment file"); } } @@ -180,37 +164,30 @@ export default class DeltaChatService { const zammadUrl = process.env.ZAMMAD_URL || "http://zammad-nginx:8080"; try { - const response = await fetch( - `${zammadUrl}/api/v1/channels_cdr_deltachat_bot_webhook/${botId}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }, - ); + const response = await fetch(`${zammadUrl}/api/v1/channels_cdr_deltachat_bot_webhook/${botId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); - if (!response.ok) { + if (response.ok) { + logger.info({ botId, msgId }, "Message forwarded to Zammad"); + } else { const errorText = await response.text(); logger.error({ status: response.status, error: errorText, botId }, "Failed to send message to Zammad"); - } else { - logger.info({ botId, msgId }, "Message forwarded to Zammad"); } - } catch (err) { - logger.error({ err, botId }, "Failed to POST to Zammad webhook"); + } catch (error) { + logger.error({ err: error, botId }, "Failed to POST to Zammad webhook"); } try { await this.dc.rpc.markseenMsgs(accountId, [msgId]); - } catch (err) { - logger.error({ err, msgId }, "Failed to mark message as seen"); + } catch (error) { + logger.error({ err: error, msgId }, "Failed to mark message as seen"); } } - async configure( - botId: string, - email: string, - password: string, - ): Promise<{ accountId: number; email: string }> { + async configure(botId: string, email: string, password: string): Promise<{ accountId: number; email: string }> { this.validateBotId(botId); if (!this.dc) throw new Error("DeltaChat not initialized"); @@ -240,14 +217,14 @@ export default class DeltaChatService { this.saveBotMapping(); return { accountId, email }; - } catch (err) { - logger.error({ botId, accountId, err }, "Configuration failed, removing account"); + } catch (error) { + logger.error({ botId, accountId, err: error }, "Configuration failed, removing account"); try { await this.dc.rpc.removeAccount(accountId); - } catch (removeErr) { - logger.error({ removeErr }, "Failed to clean up account after configuration failure"); + } catch (error_) { + logger.error({ removeErr: error_ }, "Failed to clean up account after configuration failure"); } - throw err; + throw error; } } @@ -280,14 +257,14 @@ export default class DeltaChatService { try { await this.dc.rpc.stopIo(accountId); - } catch (err) { - logger.warn({ botId, accountId, err }, "Error stopping IO during unconfigure"); + } catch (error) { + logger.warn({ botId, accountId, err: error }, "Error stopping IO during unconfigure"); } try { await this.dc.rpc.removeAccount(accountId); - } catch (err) { - logger.warn({ botId, accountId, err }, "Error removing account during unconfigure"); + } catch (error) { + logger.warn({ botId, accountId, err: error }, "Error removing account during unconfigure"); } delete this.botMapping[botId]; @@ -299,7 +276,7 @@ export default class DeltaChatService { botId: string, email: string, message: string, - attachments?: Array<{ data: string; filename: string; mime_type: string }>, + attachments?: Array<{ data: string; filename: string; mime_type: string }> ): Promise<{ recipient: string; timestamp: string; source: string }> { this.validateBotId(botId); if (!this.dc) throw new Error("DeltaChat not initialized"); @@ -328,14 +305,17 @@ export default class DeltaChatService { if (estimatedSize > MAX_ATTACHMENT_SIZE) { logger.warn( { filename: att.filename, size: estimatedSize, maxSize: MAX_ATTACHMENT_SIZE }, - "Attachment exceeds size limit, skipping", + "Attachment exceeds size limit, skipping" ); continue; } totalSize += estimatedSize; if (totalSize > MAX_TOTAL_SIZE) { - logger.warn({ totalSize, maxTotalSize: MAX_TOTAL_SIZE }, "Total attachment size exceeds limit, skipping remaining"); + logger.warn( + { totalSize, maxTotalSize: MAX_TOTAL_SIZE }, + "Total attachment size exceeds limit, skipping remaining" + ); break; } @@ -346,7 +326,14 @@ export default class DeltaChatService { try { await this.dc.rpc.sendMsg(accountId, chatId, { text: message, + html: null, + viewtype: null, file: tmpFile, + filename: att.filename, + location: null, + overrideSenderName: null, + quotedMessageId: null, + quotedText: null, }); // Only include text with the first attachment; clear for subsequent message = ""; diff --git a/apps/bridge-deltachat/tsconfig.json b/apps/bridge-deltachat/tsconfig.json index 7c4a507..0c55ac9 100644 --- a/apps/bridge-deltachat/tsconfig.json +++ b/apps/bridge-deltachat/tsconfig.json @@ -1,26 +1,8 @@ { + "extends": "@link-stack/typescript-config/tsconfig.node.json", "compilerOptions": { - "target": "es2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", "outDir": "build/main", - "rootDir": "src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "inlineSources": true, - "skipLibCheck": true, - "strict": true, - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "isolatedModules": true, - "incremental": true, - "composite": true, - "rewriteRelativeImportExtensions": true, - "types": ["node"], - "lib": ["es2022", "DOM"] + "rootDir": "src" }, "include": ["src/**/*.ts", "src/**/.*.ts"], "exclude": ["node_modules/**"] diff --git a/apps/bridge-signal/Dockerfile b/apps/bridge-signal/Dockerfile new file mode 100644 index 0000000..1e464d4 --- /dev/null +++ b/apps/bridge-signal/Dockerfile @@ -0,0 +1,56 @@ +FROM node:22-bookworm-slim AS base + +FROM base AS builder +ARG APP_DIR=/opt/bridge-signal +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN mkdir -p ${APP_DIR}/ +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate +RUN pnpm add -g turbo +WORKDIR ${APP_DIR} +COPY . . +RUN turbo prune --scope=@link-stack/bridge-signal --docker + +FROM base AS installer +ARG APP_DIR=/opt/bridge-signal +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +WORKDIR ${APP_DIR} +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate +COPY --from=builder ${APP_DIR}/out/json/ . +COPY --from=builder ${APP_DIR}/out/full/ . +COPY --from=builder ${APP_DIR}/out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN pnpm install --frozen-lockfile +RUN pnpm add -g turbo +RUN turbo run build --filter=@link-stack/bridge-signal + +FROM base as runner +ARG BUILD_DATE +ARG VERSION +ARG APP_DIR=/opt/bridge-signal +ARG SIGNAL_CLI_VERSION=0.13.12 +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN mkdir -p ${APP_DIR}/ +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get install -y --no-install-recommends \ + dumb-init curl && \ + ARCH=$(dpkg --print-architecture) && \ + curl -L "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-native-linux-${ARCH}-${SIGNAL_CLI_VERSION}.tar.gz" \ + | tar xz -C /opt && \ + ln -s /opt/signal-cli-native-linux-*/bin/signal-cli-native /usr/local/bin/signal-cli && \ + apt-get remove -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate +WORKDIR ${APP_DIR} +COPY --from=installer ${APP_DIR} ./ +RUN chown -R node:node ${APP_DIR} +WORKDIR ${APP_DIR}/apps/bridge-signal/ +RUN chmod +x docker-entrypoint.sh +USER node +RUN mkdir /home/node/signal-data +EXPOSE 5002 +ENV PORT 5002 +ENV NODE_ENV production +ENV SIGNAL_DATA_DIR /home/node/signal-data +ENV COREPACK_ENABLE_NETWORK=0 +ENTRYPOINT ["/opt/bridge-signal/apps/bridge-signal/docker-entrypoint.sh"] diff --git a/apps/bridge-signal/docker-entrypoint.sh b/apps/bridge-signal/docker-entrypoint.sh new file mode 100644 index 0000000..7fc8f17 --- /dev/null +++ b/apps/bridge-signal/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e +echo "starting bridge-signal" +exec dumb-init pnpm run start diff --git a/apps/bridge-signal/eslint.config.mjs b/apps/bridge-signal/eslint.config.mjs new file mode 100644 index 0000000..2997d1b --- /dev/null +++ b/apps/bridge-signal/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@link-stack/eslint-config/node"; + +export default config; diff --git a/apps/bridge-signal/package.json b/apps/bridge-signal/package.json new file mode 100644 index 0000000..77b4509 --- /dev/null +++ b/apps/bridge-signal/package.json @@ -0,0 +1,32 @@ +{ + "name": "@link-stack/bridge-signal", + "version": "3.5.0-beta.1", + "main": "build/main/index.js", + "author": "Darren Clarke ", + "license": "AGPL-3.0-or-later", + "prettier": "@link-stack/prettier-config", + "dependencies": { + "@hono/node-server": "^1.13.8", + "hono": "^4.7.4", + "@link-stack/logger": "workspace:*" + }, + "devDependencies": { + "@link-stack/eslint-config": "workspace:*", + "@link-stack/prettier-config": "workspace:*", + "@link-stack/typescript-config": "workspace:*", + "@types/node": "*", + "dotenv-cli": "^10.0.0", + "eslint": "^9.23.0", + "prettier": "^3.5.3", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "dotenv -- tsx src/index.ts", + "start": "node build/main/index.js", + "lint": "eslint src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/" + } +} diff --git a/apps/bridge-signal/src/attachments.ts b/apps/bridge-signal/src/attachments.ts new file mode 100644 index 0000000..850b782 --- /dev/null +++ b/apps/bridge-signal/src/attachments.ts @@ -0,0 +1,35 @@ +/** + * Attachment size configuration for messaging channels + * + * Environment variables: + * - BRIDGE_MAX_ATTACHMENT_SIZE_MB: Maximum size for a single attachment in MB (default: 50) + */ + +/** + * Get the maximum attachment size in bytes from environment variable + * Defaults to 50MB if not set + */ +export function getMaxAttachmentSize(): number { + const envValue = process.env.BRIDGE_MAX_ATTACHMENT_SIZE_MB; + const sizeInMB = envValue ? Number.parseInt(envValue, 10) : 50; + + if (Number.isNaN(sizeInMB) || sizeInMB <= 0) { + console.warn(`Invalid BRIDGE_MAX_ATTACHMENT_SIZE_MB value: ${envValue}, using default 50MB`); + return 50 * 1024 * 1024; + } + + return sizeInMB * 1024 * 1024; +} + +/** + * Get the maximum total size for all attachments in a message + * This is 4x the single attachment size + */ +export function getMaxTotalAttachmentSize(): number { + return getMaxAttachmentSize() * 4; +} + +/** + * Maximum number of attachments per message + */ +export const MAX_ATTACHMENTS = 10; diff --git a/apps/bridge-signal/src/index.ts b/apps/bridge-signal/src/index.ts new file mode 100644 index 0000000..dfb61d8 --- /dev/null +++ b/apps/bridge-signal/src/index.ts @@ -0,0 +1,33 @@ +import { serve } from "@hono/node-server"; +import { createLogger } from "@link-stack/logger"; + +import { createRoutes } from "./routes.ts"; +import SignalService from "./service.ts"; + +const logger = createLogger("bridge-signal-index"); + +const main = async () => { + const service = new SignalService(); + await service.initialize(); + + const app = createRoutes(service); + const port = Number.parseInt(process.env.PORT || "5002", 10); + + serve({ fetch: app.fetch, port }, (info) => { + logger.info({ port: info.port }, "bridge-signal listening"); + }); + + const shutdown = async () => { + logger.info("Shutting down..."); + await service.teardown(); + process.exit(0); + }; + + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); +}; + +main().catch((error) => { + logger.error(error); + process.exit(1); +}); diff --git a/apps/bridge-signal/src/routes.ts b/apps/bridge-signal/src/routes.ts new file mode 100644 index 0000000..da34b7d --- /dev/null +++ b/apps/bridge-signal/src/routes.ts @@ -0,0 +1,130 @@ +import { createLogger } from "@link-stack/logger"; +import { Hono } from "hono"; + +import type SignalService from "./service.ts"; + +const logger = createLogger("bridge-signal-routes"); + +const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error)); + +export function createRoutes(service: SignalService): Hono { + const app = new Hono(); + + // Start device linking + app.post("/api/bots/:id/register", async (c) => { + const id = c.req.param("id"); + const { phoneNumber, deviceName } = await c.req.json<{ + phoneNumber: string; + deviceName?: string; + }>(); + + try { + const result = await service.register(id, phoneNumber, deviceName); + logger.info({ id }, "Device linking started"); + return c.json(result); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to start device linking"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // Bot status + app.get("/api/bots/:id", async (c) => { + const id = c.req.param("id"); + try { + return c.json(await service.getBot(id)); + } catch (error) { + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // Send message + app.post("/api/bots/:id/send", async (c) => { + const id = c.req.param("id"); + const { recipient, message, attachments, autoGroup } = await c.req.json<{ + recipient: string; + message: string; + attachments?: Array<{ data: string; filename: string; mime_type: string }>; + autoGroup?: { ticketNumber: string }; + }>(); + + try { + const result = await service.send(id, recipient, message, attachments, autoGroup); + logger.info({ id, recipient: result.recipient, attachmentCount: attachments?.length || 0 }, "Sent message"); + return c.json({ result }); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to send message"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // Unregister bot + app.post("/api/bots/:id/unregister", async (c) => { + const id = c.req.param("id"); + try { + await service.unregister(id); + logger.info({ id }, "Bot unregistered"); + return c.body(null, 200); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to unregister bot"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // Create group + app.post("/api/bots/:id/groups", async (c) => { + const id = c.req.param("id"); + const { name, members, description } = await c.req.json<{ + name: string; + members: string[]; + description?: string; + }>(); + + try { + const result = await service.createGroup(id, name, members, description); + logger.info({ id, groupId: result.groupId }, "Group created"); + return c.json(result); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to create group"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // Update group + app.put("/api/bots/:id/groups/:groupId", async (c) => { + const id = c.req.param("id"); + const groupId = c.req.param("groupId"); + const { name, description } = await c.req.json<{ + name?: string; + description?: string; + }>(); + + try { + await service.updateGroup(id, groupId, name, description); + logger.info({ id, groupId }, "Group updated"); + return c.json({ success: true }); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to update group"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // List groups + app.get("/api/bots/:id/groups", async (c) => { + const id = c.req.param("id"); + try { + const groups = await service.listGroups(id); + return c.json(groups); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to list groups"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + // Health check + app.get("/api/health", (c) => { + return c.json({ status: "ok" }); + }); + + return app; +} diff --git a/apps/bridge-signal/src/service.ts b/apps/bridge-signal/src/service.ts new file mode 100644 index 0000000..ec5b36b --- /dev/null +++ b/apps/bridge-signal/src/service.ts @@ -0,0 +1,462 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { createLogger } from "@link-stack/logger"; + +import { getMaxAttachmentSize, getMaxTotalAttachmentSize, MAX_ATTACHMENTS } from "./attachments.ts"; +import { SignalCli, SignalEnvelope } from "./signal-cli.ts"; + +const logger = createLogger("bridge-signal-service"); + +interface BotMapping { + [botId: string]: { + phoneNumber: string; + webhookToken?: string; + }; +} + +interface Attachment { + data: string; // base64 + filename: string; + mime_type: string; +} + +interface SendResult { + recipient: string; + timestamp: number; + source: string; + groupId?: string; +} + +export default class SignalService { + private cli: SignalCli | null = null; + private botMapping: BotMapping = {}; + private dataDir: string; + private mappingFile: string; + private autoGroupsEnabled: boolean; + + constructor() { + this.dataDir = process.env.SIGNAL_DATA_DIR || "/home/node/signal-data"; + this.mappingFile = path.join(this.dataDir, "bot-mapping.json"); + this.autoGroupsEnabled = process.env.BRIDGE_SIGNAL_AUTO_GROUPS?.toLowerCase() === "true"; + } + + async initialize(): Promise { + // Ensure data directory exists + if (!fs.existsSync(this.dataDir)) { + fs.mkdirSync(this.dataDir, { recursive: true }); + } + + // Load bot mapping + this.loadBotMapping(); + + // Start signal-cli + this.cli = new SignalCli(this.dataDir); + await this.cli.start(); + + // Register message listener + this.cli.on("message", ({ account, envelope }: { account: string; envelope: SignalEnvelope }) => { + this.handleIncomingMessage(account, envelope).catch((error) => { + logger.error({ err: error, account }, "Error handling incoming message"); + }); + }); + + this.cli.on("close", (code: number | null) => { + logger.warn({ code }, "signal-cli process closed unexpectedly"); + }); + + this.cli.on("error", (err: Error) => { + logger.error({ err }, "signal-cli process error"); + }); + + logger.info({ dataDir: this.dataDir, botCount: Object.keys(this.botMapping).length }, "SignalService initialized"); + } + + async teardown(): Promise { + if (this.cli) { + this.cli.close(); + this.cli = null; + } + } + + // --- Bot management --- + + async register(botId: string, phoneNumber: string, deviceName = "Zammad"): Promise<{ linkUri: string }> { + this.validateBotId(botId); + if (!this.cli) throw new Error("SignalService not initialized"); + + logger.info({ botId, phoneNumber }, "Starting device linking"); + + const result = (await this.cli.call("startLink")) as Record | undefined; + const linkUri = result?.deviceLinkUri as string; + if (!linkUri) { + throw new Error("signal-cli startLink did not return a deviceLinkUri"); + } + + // Finish linking in the background + (async () => { + try { + const finishResult = await this.cli!.call("finishLink", { + deviceLinkUri: linkUri, + deviceName, + }); + const linkedNumber = (finishResult as string) || phoneNumber; + + this.botMapping[botId] = { phoneNumber: linkedNumber }; + this.saveBotMapping(); + logger.info({ botId, phoneNumber: linkedNumber }, "Device linking completed"); + } catch (error) { + logger.error({ err: error, botId }, "Device linking failed"); + } + })(); + + return { linkUri }; + } + + async getBot(botId: string): Promise<{ registered: boolean; phoneNumber: string | null }> { + this.validateBotId(botId); + + const mapping = this.botMapping[botId]; + if (!mapping) { + return { registered: false, phoneNumber: null }; + } + + return { registered: true, phoneNumber: mapping.phoneNumber }; + } + + async unregister(botId: string): Promise { + this.validateBotId(botId); + + const mapping = this.botMapping[botId]; + if (!mapping) { + logger.warn({ botId }, "Bot not found for unregister"); + return; + } + + delete this.botMapping[botId]; + this.saveBotMapping(); + logger.info({ botId }, "Bot unregistered"); + } + + // --- Messaging --- + + async send( + botId: string, + recipient: string, + message: string, + attachments?: Attachment[], + autoGroup?: { ticketNumber: string } + ): Promise { + this.validateBotId(botId); + if (!this.cli) throw new Error("SignalService not initialized"); + + const mapping = this.botMapping[botId]; + if (!mapping) throw new Error(`Bot ${botId} is not registered`); + + const account = mapping.phoneNumber; + let finalRecipient = recipient; + let groupId: string | undefined; + + // Auto-group: create a group if enabled and recipient is a phone number + if (this.autoGroupsEnabled && autoGroup && !recipient.startsWith("group.")) { + try { + const groupName = `Support Request: ${autoGroup.ticketNumber}`; + logger.info({ botId, groupName, recipient }, "Creating auto-group"); + + const createResult = (await this.cli.call("updateGroup", { + account, + name: groupName, + members: [recipient], + description: "Private support conversation", + })) as Record | undefined; + + if (createResult?.groupId) { + groupId = createResult.groupId as string; + finalRecipient = groupId; + logger.info({ botId, groupId, groupName }, "Auto-group created"); + } + } catch (error) { + logger.error({ err: error, botId }, "Failed to create auto-group, sending to original recipient"); + } + } + + // Build base64 attachments + const base64Attachments: string[] = []; + if (attachments && attachments.length > 0) { + const MAX_SIZE = getMaxAttachmentSize(); + const MAX_TOTAL = getMaxTotalAttachmentSize(); + + if (attachments.length > MAX_ATTACHMENTS) { + logger.warn({ count: attachments.length, max: MAX_ATTACHMENTS }, "Too many attachments, truncating"); + attachments = attachments.slice(0, MAX_ATTACHMENTS); + } + + let totalSize = 0; + for (const att of attachments) { + const estimatedSize = (att.data.length * 3) / 4; + if (estimatedSize > MAX_SIZE) { + logger.warn({ filename: att.filename, size: estimatedSize }, "Attachment too large, skipping"); + continue; + } + totalSize += estimatedSize; + if (totalSize > MAX_TOTAL) { + logger.warn({ totalSize }, "Total attachment size exceeded, skipping remaining"); + break; + } + base64Attachments.push(att.data); + } + } + + // Send the message + const isGroup = finalRecipient.startsWith("group."); + const sendParams: Record = { + account, + message, + }; + + if (isGroup) { + sendParams.groupId = finalRecipient; + } else { + sendParams.recipients = [finalRecipient]; + } + + if (base64Attachments.length > 0) { + sendParams.base64Attachments = base64Attachments; + } + + const result = (await this.cli.call("send", sendParams)) as Record | undefined; + const timestamp = (result?.timestamp as number) || Date.now(); + + return { + recipient: finalRecipient, + timestamp, + source: account, + groupId, + }; + } + + // --- Groups --- + + async createGroup( + botId: string, + name: string, + members: string[], + description?: string + ): Promise<{ groupId: string }> { + this.validateBotId(botId); + if (!this.cli) throw new Error("SignalService not initialized"); + + const mapping = this.botMapping[botId]; + if (!mapping) throw new Error(`Bot ${botId} is not registered`); + + const params: Record = { + account: mapping.phoneNumber, + name, + members, + }; + if (description) params.description = description; + + const result = (await this.cli.call("updateGroup", params)) as Record | undefined; + return { groupId: (result?.groupId as string) || String(result) }; + } + + async updateGroup(botId: string, groupId: string, name?: string, description?: string): Promise { + this.validateBotId(botId); + if (!this.cli) throw new Error("SignalService not initialized"); + + const mapping = this.botMapping[botId]; + if (!mapping) throw new Error(`Bot ${botId} is not registered`); + + const params: Record = { + account: mapping.phoneNumber, + groupId, + }; + if (name) params.name = name; + if (description) params.description = description; + + await this.cli.call("updateGroup", params); + } + + async listGroups(botId: string): Promise { + this.validateBotId(botId); + if (!this.cli) throw new Error("SignalService not initialized"); + + const mapping = this.botMapping[botId]; + if (!mapping) throw new Error(`Bot ${botId} is not registered`); + + const result = await this.cli.call("listGroups", { account: mapping.phoneNumber }); + return Array.isArray(result) ? result : []; + } + + // --- Incoming message handler --- + + private async handleIncomingMessage(account: string, envelope: SignalEnvelope): Promise { + // Find botId for this account + const botId = this.findBotIdByAccount(account); + if (!botId) { + logger.debug({ account }, "No bot mapping for account, ignoring message"); + return; + } + + const source = envelope.sourceNumber || envelope.source; + const sourceUuid = envelope.sourceUuid; + + // Skip messages from self + if (source === account) { + return; + } + + const dataMessage = envelope.dataMessage; + if (!dataMessage) { + // Could be typing indicator, receipt, etc. -- ignore + return; + } + + // Check for group info + const isGroup = !!dataMessage.groupInfo?.groupId; + const groupId = dataMessage.groupInfo?.groupId; + const groupType = dataMessage.groupInfo?.type; + + // Detect group join events + if ( + isGroup && + groupType && + ["DELIVER", "UPDATE"].includes(groupType) && // Group update events (member joins) -- forward to Zammad + groupType === "UPDATE" && + source + ) { + await this.postGroupMemberJoined(botId, groupId!, source); + } + + // Process data messages with content + const messageText = dataMessage.message; + if (!messageText && (!dataMessage.attachments || dataMessage.attachments.length === 0)) { + return; + } + + // Handle attachments + let attachment: string | undefined; + let filename: string | undefined; + let mimeType: string | undefined; + + if (dataMessage.attachments && dataMessage.attachments.length > 0) { + const att = dataMessage.attachments[0]; + const storedFile = att.storedFilename; + if (storedFile) { + const filePath = path.join(this.dataDir, "attachments", storedFile); + try { + if (fs.existsSync(filePath)) { + const fileData = fs.readFileSync(filePath); + attachment = fileData.toString("base64"); + filename = att.filename || storedFile; + mimeType = att.contentType || "application/octet-stream"; + logger.info({ filename, mimeType, size: fileData.length }, "Attachment found"); + } + } catch (error) { + logger.error({ err: error, filePath }, "Failed to read attachment file"); + } + } + } + + const messageId = `${source}@${dataMessage.timestamp || envelope.timestamp}`; + const sentAt = dataMessage.timestamp || envelope.timestamp; + + const payload: Record = { + from: source, + to: isGroup ? groupId : account, + user_id: sourceUuid, + message: messageText || "", + message_id: messageId, + sent_at: sentAt ? new Date(sentAt).toISOString() : new Date().toISOString(), + is_group: isGroup, + }; + + if (attachment) { + payload.attachment = attachment; + payload.filename = filename; + payload.mime_type = mimeType; + } + + // POST to Zammad webhook + const zammadUrl = process.env.ZAMMAD_URL || "http://zammad-nginx:8080"; + try { + const response = await fetch(`${zammadUrl}/api/v1/channels_cdr_signal_bot_webhook/${botId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + logger.info({ botId, messageId }, "Message forwarded to Zammad"); + } else { + const errorText = await response.text(); + logger.error({ status: response.status, error: errorText, botId }, "Failed to send message to Zammad"); + } + } catch (error) { + logger.error({ err: error, botId }, "Failed to POST to Zammad webhook"); + } + } + + private async postGroupMemberJoined(botId: string, groupId: string, memberPhone: string): Promise { + const zammadUrl = process.env.ZAMMAD_URL || "http://zammad-nginx:8080"; + const payload = { + event: "group_member_joined", + group_id: groupId, + member_phone: memberPhone, + timestamp: new Date().toISOString(), + }; + + try { + const response = await fetch(`${zammadUrl}/api/v1/channels_cdr_signal_bot_webhook/${botId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + logger.info({ botId, groupId, memberPhone }, "Group member join notification sent to Zammad"); + } else { + logger.error({ status: response.status, botId, groupId }, "Failed to notify Zammad of group member join"); + } + } catch (error) { + logger.error({ err: error, botId }, "Failed to POST group_member_joined to Zammad"); + } + } + + private findBotIdByAccount(account: string): string | undefined { + for (const [botId, mapping] of Object.entries(this.botMapping)) { + if (mapping.phoneNumber === account) { + return botId; + } + } + return undefined; + } + + private validateBotId(botId: string): void { + if (!botId || !/^[a-zA-Z0-9_-]+$/.test(botId)) { + throw new Error(`Invalid bot ID: ${botId}`); + } + } + + private loadBotMapping(): void { + try { + if (fs.existsSync(this.mappingFile)) { + const data = fs.readFileSync(this.mappingFile, "utf8"); + this.botMapping = JSON.parse(data); + logger.info({ count: Object.keys(this.botMapping).length }, "Loaded bot mapping"); + } + } catch (error) { + logger.error({ err: error }, "Failed to load bot mapping, starting fresh"); + this.botMapping = {}; + } + } + + private saveBotMapping(): void { + try { + fs.writeFileSync(this.mappingFile, JSON.stringify(this.botMapping, null, 2)); + logger.debug("Saved bot mapping"); + } catch (error) { + logger.error({ err: error }, "Failed to save bot mapping"); + } + } +} diff --git a/apps/bridge-signal/src/signal-cli.ts b/apps/bridge-signal/src/signal-cli.ts new file mode 100644 index 0000000..2d0b104 --- /dev/null +++ b/apps/bridge-signal/src/signal-cli.ts @@ -0,0 +1,247 @@ +import { ChildProcess, spawn } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { createInterface, Interface } from "node:readline"; + +import { createLogger } from "@link-stack/logger"; + +const logger = createLogger("bridge-signal-cli"); + +interface PendingRequest { + resolve: (value: unknown) => void; + reject: (reason: unknown) => void; + method: string; + timer: ReturnType; +} + +interface JsonRpcRequest { + jsonrpc: "2.0"; + method: string; + params?: Record; + id: string; +} + +interface JsonRpcResponse { + jsonrpc: "2.0"; + id?: string; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; + method?: string; + params?: Record; +} + +export interface SignalEnvelope { + source?: string; + sourceNumber?: string; + sourceUuid?: string; + sourceName?: string; + timestamp?: number; + dataMessage?: { + timestamp?: number; + message?: string; + groupInfo?: { + groupId?: string; + type?: string; + }; + attachments?: Array<{ + id?: string; + contentType?: string; + filename?: string; + size?: number; + storedFilename?: string; + }>; + }; + syncMessage?: { + sentMessage?: { + destination?: string; + destinationNumber?: string; + timestamp?: number; + message?: string; + groupInfo?: { groupId?: string }; + }; + }; + typingMessage?: unknown; + receiptMessage?: unknown; +} + +const REQUEST_TIMEOUT_MS = 60_000; + +// eslint-disable-next-line unicorn/prefer-event-target +export class SignalCli extends EventEmitter { + private process: ChildProcess | null = null; + private readline: Interface | null = null; + private pending: Map = new Map(); + private nextId = 1; + private configDir: string; + private closed = false; + + constructor(configDir: string) { + super(); + this.configDir = configDir; + } + + async start(): Promise { + const args = ["--config", this.configDir, "--output=json", "jsonRpc"]; + + logger.info({ configDir: this.configDir, args }, "Starting signal-cli subprocess"); + + this.process = spawn("signal-cli", args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + if (!this.process.stdout || !this.process.stdin) { + throw new Error("Failed to open signal-cli stdio pipes"); + } + + this.readline = createInterface({ + input: this.process.stdout, + crlfDelay: Infinity, + }); + + this.readline.on("line", (line: string) => { + this.handleLine(line); + }); + + this.process.stderr?.on("data", (data: Buffer) => { + const msg = data.toString().trim(); + if (msg) { + logger.warn({ stderr: msg }, "signal-cli stderr"); + } + }); + + this.process.on("close", (code: number | null) => { + this.closed = true; + logger.info({ code }, "signal-cli process exited"); + this.rejectAllPending("signal-cli process exited"); + this.emit("close", code); + }); + + this.process.on("error", (err: Error) => { + logger.error({ err }, "signal-cli process error"); + this.emit("error", err); + }); + + // Wait briefly for the process to start (or fail immediately) + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + resolve(); + }, 1000); + + this.process!.on("error", (err) => { + clearTimeout(timer); + reject(err); + }); + + this.process!.on("close", (code) => { + if (code !== null && code !== 0) { + clearTimeout(timer); + reject(new Error(`signal-cli exited with code ${code}`)); + } + }); + }); + + logger.info("signal-cli subprocess started"); + } + + async call(method: string, params: Record = {}): Promise { + if (this.closed || !this.process?.stdin) { + throw new Error("signal-cli is not running"); + } + + const id = String(this.nextId++); + const request: JsonRpcRequest = { + jsonrpc: "2.0", + method, + params, + id, + }; + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error(`signal-cli request timed out: ${method} (id=${id})`)); + }, REQUEST_TIMEOUT_MS); + + this.pending.set(id, { resolve, reject, method, timer }); + + const line = JSON.stringify(request) + "\n"; + logger.debug({ method, id, params: Object.keys(params) }, "Sending JSON-RPC request"); + + this.process!.stdin!.write(line, (err) => { + if (err) { + clearTimeout(timer); + this.pending.delete(id); + reject(new Error(`Failed to write to signal-cli stdin: ${err.message}`)); + } + }); + }); + } + + close(): void { + this.closed = true; + this.rejectAllPending("signal-cli closing"); + if (this.readline) { + this.readline.close(); + this.readline = null; + } + if (this.process) { + this.process.kill("SIGTERM"); + // Force kill after 5 seconds + setTimeout(() => { + if (this.process && !this.process.killed) { + this.process.kill("SIGKILL"); + } + }, 5000); + this.process = null; + } + } + + private handleLine(line: string): void { + if (!line.trim()) return; + + let msg: JsonRpcResponse; + try { + msg = JSON.parse(line); + } catch { + logger.warn({ line: line.slice(0, 200) }, "Non-JSON output from signal-cli"); + return; + } + + // Response to a pending request + if (msg.id !== undefined) { + const pending = this.pending.get(String(msg.id)); + if (pending) { + this.pending.delete(String(msg.id)); + clearTimeout(pending.timer); + + if (msg.error) { + logger.warn({ method: pending.method, error: msg.error }, "JSON-RPC error response"); + pending.reject(new Error(`signal-cli ${pending.method}: ${msg.error.message}`)); + } else { + logger.debug({ method: pending.method, id: msg.id }, "JSON-RPC response received"); + pending.resolve(msg.result); + } + } else { + logger.warn({ id: msg.id }, "Received response for unknown request id"); + } + return; + } + + // Notification (no id field) + if (msg.method === "receive") { + const envelope = msg.params?.envelope as SignalEnvelope | undefined; + const account = msg.params?.account as string | undefined; + if (envelope) { + logger.debug({ account, source: envelope.source || envelope.sourceNumber }, "Received message notification"); + this.emit("message", { account, envelope }); + } + } + } + + private rejectAllPending(reason: string): void { + for (const [_id, pending] of this.pending) { + clearTimeout(pending.timer); + pending.reject(new Error(reason)); + } + this.pending.clear(); + } +} diff --git a/apps/bridge-signal/tsconfig.json b/apps/bridge-signal/tsconfig.json new file mode 100644 index 0000000..0c55ac9 --- /dev/null +++ b/apps/bridge-signal/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@link-stack/typescript-config/tsconfig.node.json", + "compilerOptions": { + "outDir": "build/main", + "rootDir": "src" + }, + "include": ["src/**/*.ts", "src/**/.*.ts"], + "exclude": ["node_modules/**"] +} diff --git a/apps/bridge-whatsapp/eslint.config.mjs b/apps/bridge-whatsapp/eslint.config.mjs new file mode 100644 index 0000000..2997d1b --- /dev/null +++ b/apps/bridge-whatsapp/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@link-stack/eslint-config/node"; + +export default config; diff --git a/apps/bridge-whatsapp/package.json b/apps/bridge-whatsapp/package.json index 7632b63..c632d53 100644 --- a/apps/bridge-whatsapp/package.json +++ b/apps/bridge-whatsapp/package.json @@ -4,25 +4,33 @@ "main": "build/main/index.js", "author": "Darren Clarke ", "license": "AGPL-3.0-or-later", + "prettier": "@link-stack/prettier-config", "dependencies": { "@adiwajshing/keyed-db": "0.2.4", "@hono/node-server": "^1.13.8", "@whiskeysockets/baileys": "6.7.21", "hono": "^4.7.4", "link-preview-js": "^3.1.0", - "pino": "^9.6.0", - "pino-pretty": "^13.0.0" + "@link-stack/logger": "workspace:*" }, "devDependencies": { + "@link-stack/eslint-config": "workspace:*", + "@link-stack/prettier-config": "workspace:*", + "@link-stack/typescript-config": "workspace:*", "@types/long": "^5", "@types/node": "*", "dotenv-cli": "^10.0.0", + "eslint": "^9.23.0", + "prettier": "^3.5.3", "tsx": "^4.20.6", "typescript": "^5.9.3" }, "scripts": { "build": "tsc -p tsconfig.json", "dev": "dotenv -- tsx src/index.ts", - "start": "node build/main/index.js" + "start": "node build/main/index.js", + "lint": "eslint src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/" } } diff --git a/apps/bridge-whatsapp/src/attachments.ts b/apps/bridge-whatsapp/src/attachments.ts index 9abc01d..f0d0d74 100644 --- a/apps/bridge-whatsapp/src/attachments.ts +++ b/apps/bridge-whatsapp/src/attachments.ts @@ -11,10 +11,10 @@ */ export function getMaxAttachmentSize(): number { const envValue = process.env.BRIDGE_MAX_ATTACHMENT_SIZE_MB; - const sizeInMB = envValue ? parseInt(envValue, 10) : 50; + const sizeInMB = envValue ? Number.parseInt(envValue, 10) : 50; // Validate the value - if (isNaN(sizeInMB) || sizeInMB <= 0) { + if (Number.isNaN(sizeInMB) || sizeInMB <= 0) { console.warn(`Invalid BRIDGE_MAX_ATTACHMENT_SIZE_MB value: ${envValue}, using default 50MB`); return 50 * 1024 * 1024; } diff --git a/apps/bridge-whatsapp/src/index.ts b/apps/bridge-whatsapp/src/index.ts index d66f037..2b603a0 100644 --- a/apps/bridge-whatsapp/src/index.ts +++ b/apps/bridge-whatsapp/src/index.ts @@ -1,7 +1,8 @@ import { serve } from "@hono/node-server"; -import WhatsappService from "./service.ts"; +import { createLogger } from "@link-stack/logger"; + import { createRoutes } from "./routes.ts"; -import { createLogger } from "./lib/logger"; +import WhatsappService from "./service.ts"; const logger = createLogger("bridge-whatsapp-index"); @@ -10,7 +11,7 @@ const main = async () => { await service.initialize(); const app = createRoutes(service); - const port = parseInt(process.env.PORT || "5000", 10); + const port = Number.parseInt(process.env.PORT || "5000", 10); serve({ fetch: app.fetch, port }, (info) => { logger.info({ port: info.port }, "bridge-whatsapp listening"); @@ -26,7 +27,7 @@ const main = async () => { process.on("SIGINT", shutdown); }; -main().catch((err) => { - logger.error(err); +main().catch((error) => { + logger.error(error); process.exit(1); }); diff --git a/apps/bridge-whatsapp/src/lib/logger.ts b/apps/bridge-whatsapp/src/lib/logger.ts deleted file mode 100644 index c72a3c9..0000000 --- a/apps/bridge-whatsapp/src/lib/logger.ts +++ /dev/null @@ -1,77 +0,0 @@ -import pino, { Logger as PinoLogger, LoggerOptions } from 'pino'; - -export type Logger = PinoLogger; - -const getLogLevel = (): string => { - return process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'); -}; - -const getPinoConfig = (): LoggerOptions => { - const isDevelopment = process.env.NODE_ENV !== 'production'; - - const baseConfig: LoggerOptions = { - level: getLogLevel(), - formatters: { - level: (label) => { - return { level: label.toUpperCase() }; - }, - }, - timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, - redact: { - paths: [ - 'password', - 'token', - 'secret', - 'api_key', - 'apiKey', - 'authorization', - 'cookie', - 'access_token', - 'refresh_token', - '*.password', - '*.token', - '*.secret', - '*.api_key', - '*.apiKey', - '*.authorization', - '*.cookie', - '*.access_token', - '*.refresh_token', - 'headers.authorization', - 'headers.cookie', - 'headers.Authorization', - 'headers.Cookie', - 'credentials.password', - 'credentials.secret', - 'credentials.token', - ], - censor: '[REDACTED]', - }, - }; - - if (isDevelopment) { - return { - ...baseConfig, - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:standard', - ignore: 'pid,hostname', - singleLine: false, - messageFormat: '{msg}', - }, - }, - }; - } - - return baseConfig; -}; - -export const logger: Logger = pino(getPinoConfig()); - -export const createLogger = (name: string, context?: Record): Logger => { - return logger.child({ name, ...context }); -}; - -export default logger; diff --git a/apps/bridge-whatsapp/src/routes.ts b/apps/bridge-whatsapp/src/routes.ts index d70df22..693e8c5 100644 --- a/apps/bridge-whatsapp/src/routes.ts +++ b/apps/bridge-whatsapp/src/routes.ts @@ -1,12 +1,36 @@ +import { createLogger } from "@link-stack/logger"; import { Hono } from "hono"; + import type WhatsappService from "./service.ts"; -import { createLogger } from "./lib/logger"; const logger = createLogger("bridge-whatsapp-routes"); +const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error)); + export function createRoutes(service: WhatsappService): Hono { const app = new Hono(); + app.post("/api/bots/:id/register", async (c) => { + const id = c.req.param("id"); + try { + await service.register(id); + logger.info({ id }, "Bot registered"); + return c.body(null, 200); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to register bot"); + return c.json({ error: errorMessage(error) }, 500); + } + }); + + app.get("/api/bots/:id", async (c) => { + const id = c.req.param("id"); + try { + return c.json(service.getBot(id)); + } catch (error) { + return c.json({ error: errorMessage(error) }, 500); + } + }); + app.post("/api/bots/:id/send", async (c) => { const id = c.req.param("id"); const { phoneNumber, message, attachments } = await c.req.json<{ @@ -15,43 +39,30 @@ export function createRoutes(service: WhatsappService): Hono { attachments?: Array<{ data: string; filename: string; mime_type: string }>; }>(); - await service.send(id, phoneNumber, message, attachments); - logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message"); - - return c.json({ - result: { - recipient: phoneNumber, - timestamp: new Date().toISOString(), - source: id, - }, - }); - }); - - app.get("/api/bots/:id/receive", async (c) => { - const id = c.req.param("id"); - const date = new Date(); - const twoDaysAgo = new Date(date.getTime()); - twoDaysAgo.setDate(date.getDate() - 2); - - const messages = await service.receive(id, twoDaysAgo); - return c.json(messages); - }); - - app.post("/api/bots/:id/register", async (c) => { - const id = c.req.param("id"); - await service.register(id); - return c.body(null, 200); + try { + const result = await service.send(id, phoneNumber, message, attachments); + logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message"); + return c.json({ result }); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to send message"); + return c.json({ error: errorMessage(error) }, 500); + } }); app.post("/api/bots/:id/unverify", async (c) => { const id = c.req.param("id"); - await service.unverify(id); - return c.body(null, 200); + try { + await service.unverify(id); + logger.info({ id }, "Bot unverified"); + return c.body(null, 200); + } catch (error) { + logger.error({ id, error: errorMessage(error) }, "Failed to unverify bot"); + return c.json({ error: errorMessage(error) }, 500); + } }); - app.get("/api/bots/:id", async (c) => { - const id = c.req.param("id"); - return c.json(service.getBot(id)); + app.get("/api/health", (c) => { + return c.json({ status: "ok" }); }); return app; diff --git a/apps/bridge-whatsapp/src/service.ts b/apps/bridge-whatsapp/src/service.ts index a311cfc..219aa94 100644 --- a/apps/bridge-whatsapp/src/service.ts +++ b/apps/bridge-whatsapp/src/service.ts @@ -1,4 +1,9 @@ +import fs from "node:fs"; + +import { createLogger } from "@link-stack/logger"; import makeWASocket, { + type WASocket, + type SocketConfig, DisconnectReason, proto, downloadContentFromMessage, @@ -8,22 +13,21 @@ import makeWASocket, { useMultiFileAuthState, } from "@whiskeysockets/baileys"; +import { getMaxAttachmentSize, getMaxTotalAttachmentSize, MAX_ATTACHMENTS } from "./attachments"; + type MediaType = "audio" | "document" | "image" | "video" | "sticker"; -import fs from "fs"; -import { createLogger } from "./lib/logger"; -import { - getMaxAttachmentSize, - getMaxTotalAttachmentSize, - MAX_ATTACHMENTS, -} from "./attachments"; const logger = createLogger("bridge-whatsapp-service"); export type AuthCompleteCallback = (error?: string) => void; +interface BotConnection { + socket: WASocket; +} + export default class WhatsappService { - connections: { [key: string]: any } = {}; - loginConnections: { [key: string]: any } = {}; + connections: Record = {}; + loginConnections: Record = {}; static browserDescription: [string, string, string] = ["Bridge", "Chrome", "2.0"]; @@ -71,7 +75,7 @@ export default class WhatsappService { private async resetConnections() { for (const connection of Object.values(this.connections)) { try { - connection.end(null); + connection.socket.end(new Error("Connection reset")); } catch (error) { logger.error({ error }, "Connection reset error"); } @@ -81,18 +85,16 @@ export default class WhatsappService { private async createConnection( botID: string, - options: any, - authCompleteCallback?: any, + options: Partial, + authCompleteCallback?: AuthCompleteCallback ) { const authDirectory = this.getAuthDirectory(botID); const { state, saveCreds } = await useMultiFileAuthState(authDirectory); - const msgRetryCounterMap: any = {}; const socket = makeWASocket({ ...options, auth: state, generateHighQualityLinkPreview: false, syncFullHistory: true, - msgRetryCounterMap, shouldIgnoreJid: (jid) => isJidBroadcast(jid) || isJidStatusBroadcast(jid), }); let pause = 5000; @@ -115,7 +117,8 @@ export default class WhatsappService { logger.info("opened connection"); } else if (connectionState === "close") { logger.info({ lastDisconnect }, "connection closed"); - const disconnectStatusCode = (lastDisconnect?.error as any)?.output?.statusCode; + const disconnectStatusCode = (lastDisconnect?.error as { output?: { statusCode?: number } } | undefined) + ?.output?.statusCode; if (disconnectStatusCode === DisconnectReason.restartRequired) { logger.info("reconnecting after got new login"); await this.createConnection(botID, options); @@ -145,17 +148,14 @@ export default class WhatsappService { if (events["messaging-history.set"]) { const { messages, isLatest } = events["messaging-history.set"]; - logger.info( - { messageCount: messages.length, isLatest }, - "received message history on connection", - ); + logger.info({ messageCount: messages.length, isLatest }, "received message history on connection"); if (messages.length > 0) { await this.queueUnreadMessages(botID, messages); } } }); - this.connections[botID] = { socket, msgRetryCounterMap }; + this.connections[botID] = { socket }; } private async updateConnections() { @@ -188,17 +188,13 @@ export default class WhatsappService { const { id, fromMe, remoteJid } = key; // Baileys 7 uses LIDs (Linked IDs) instead of phone numbers in some cases. // senderPn contains the actual phone number when available. - const senderPn = (key as any).senderPn as string | undefined; - const participantPn = (key as any).participantPn as string | undefined; - logger.info( - { remoteJid, senderPn, participantPn, fromMe }, - "Processing incoming message", - ); + const senderPn = (key as { senderPn?: string }).senderPn; + const participantPn = (key as { participantPn?: string }).participantPn; + logger.info({ remoteJid, senderPn, participantPn, fromMe }, "Processing incoming message"); const isValidMessage = message && remoteJid !== "status@broadcast" && !fromMe; if (isValidMessage) { const { audioMessage, documentMessage, imageMessage, videoMessage } = message; - const isMediaMessage = - audioMessage || documentMessage || imageMessage || videoMessage; + const isMediaMessage = audioMessage || documentMessage || imageMessage || videoMessage; const messageContent = Object.values(message)[0]; let messageType: MediaType; @@ -229,8 +225,8 @@ export default class WhatsappService { const stream = await downloadContentFromMessage( messageContent, - // @ts-ignore - messageType, + // @ts-expect-error messageType is dynamically resolved + messageType ); let buffer = Buffer.from([]); for await (const chunk of stream) { @@ -244,12 +240,9 @@ export default class WhatsappService { const extendedTextMessage = message?.extendedTextMessage?.text; const imageMessage = message?.imageMessage?.caption; const videoMessage = message?.videoMessage?.caption; - const messageText = [ - conversation, - extendedTextMessage, - imageMessage, - videoMessage, - ].find((text) => text && text !== ""); + const messageText = [conversation, extendedTextMessage, imageMessage, videoMessage].find( + (text) => text && text !== "" + ); // Extract phone number and user ID (LID) separately // remoteJid may contain LIDs (Baileys 7+) which are not phone numbers @@ -257,7 +250,8 @@ export default class WhatsappService { const isLidJid = remoteJid?.endsWith("@lid"); // Phone number: prefer senderPn/participantPn, fall back to remoteJid only if it's not a LID - const senderPhone = senderPn?.split("@")[0] || participantPn?.split("@")[0] || (!isLidJid ? jidValue : undefined); + const senderPhone = + senderPn?.split("@")[0] || participantPn?.split("@")[0] || (isLidJid ? undefined : jidValue); // User ID (LID): extract from remoteJid if it's a LID format const senderUserId = isLidJid ? jidValue : undefined; @@ -271,27 +265,24 @@ export default class WhatsappService { const payload = { to: botID, from: senderPhone, - userId: senderUserId, - messageId: id, - sentAt: new Date((messageTimestamp as number) * 1000).toISOString(), + user_id: senderUserId, + message_id: id, + sent_at: new Date((messageTimestamp as number) * 1000).toISOString(), message: messageText, attachment, filename, - mimeType, + mime_type: mimeType, }; // Send directly to Zammad's WhatsApp webhook - const zammadUrl = process.env.ZAMMAD_URL || 'http://zammad-nginx:8080'; - const response = await fetch( - `${zammadUrl}/api/v1/channels_cdr_whatsapp_bot_webhook/${botID}`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), + const zammadUrl = process.env.ZAMMAD_URL || "http://zammad-nginx:8080"; + const response = await fetch(`${zammadUrl}/api/v1/channels_cdr_whatsapp_bot_webhook/${botID}`, { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify(payload), + }); if (!response.ok) { const errorText = await response.text(); @@ -307,7 +298,7 @@ export default class WhatsappService { } } - getBot(botID: string): Record { + getBot(botID: string): Record { const botDirectory = this.getBotDirectory(botID); const qrPath = `${botDirectory}/qr.txt`; const verifiedFile = `${botDirectory}/verified`; @@ -327,7 +318,7 @@ export default class WhatsappService { } catch (error) { logger.warn({ botID, error }, "Error during logout, forcing disconnect"); try { - connection.socket.end(undefined); + connection.socket.end(new Error("Forced disconnect")); } catch (endError) { logger.warn({ botID, endError }, "Error ending socket connection"); } @@ -346,11 +337,7 @@ export default class WhatsappService { async register(botID: string, callback?: AuthCompleteCallback): Promise { const { version } = await fetchLatestBaileysVersion(); - await this.createConnection( - botID, - { version, browser: WhatsappService.browserDescription }, - callback, - ); + await this.createConnection(botID, { version, browser: WhatsappService.browserDescription }, callback); callback?.(); } @@ -358,10 +345,10 @@ export default class WhatsappService { botID: string, phoneNumber: string, message: string, - attachments?: Array<{ data: string; filename: string; mime_type: string }>, - ): Promise { + attachments?: Array<{ data: string; filename: string; mime_type: string }> + ): Promise<{ recipient: string; timestamp: string; source: string }> { const connection = this.connections[botID]?.socket; - const digits = phoneNumber.replace(/\D+/g, ""); + const digits = phoneNumber.replaceAll(/\D+/g, ""); // LIDs are 15+ digits, phone numbers with country code are typically 10-14 digits const suffix = digits.length > 14 ? "@lid" : "@s.whatsapp.net"; const recipient = `${digits}${suffix}`; @@ -377,9 +364,7 @@ export default class WhatsappService { const MAX_TOTAL_SIZE = getMaxTotalAttachmentSize(); if (attachments.length > MAX_ATTACHMENTS) { - throw new Error( - `Too many attachments: ${attachments.length} (max ${MAX_ATTACHMENTS})`, - ); + throw new Error(`Too many attachments: ${attachments.length} (max ${MAX_ATTACHMENTS})`); } let totalSize = 0; @@ -395,7 +380,7 @@ export default class WhatsappService { size: estimatedSize, maxSize: MAX_ATTACHMENT_SIZE, }, - "Attachment exceeds size limit, skipping", + "Attachment exceeds size limit, skipping" ); continue; } @@ -407,7 +392,7 @@ export default class WhatsappService { totalSize, maxTotalSize: MAX_TOTAL_SIZE, }, - "Total attachment size exceeds limit, skipping remaining", + "Total attachment size exceeds limit, skipping remaining" ); break; } @@ -438,16 +423,11 @@ export default class WhatsappService { } } } - } - async receive( - _botID: string, - _lastReceivedDate: Date, - ): Promise { - throw new Error( - "Message polling is no longer supported in Baileys 7.x. " + - "Please configure a webhook to receive messages instead. " + - "Messages are automatically forwarded to Zammad via ZAMMAD_URL/api/v1/channels_cdr_whatsapp_bot_webhook/{id}" - ); + return { + recipient: phoneNumber, + timestamp: new Date().toISOString(), + source: botID, + }; } } diff --git a/apps/bridge-whatsapp/tsconfig.json b/apps/bridge-whatsapp/tsconfig.json index 7c4a507..0c55ac9 100644 --- a/apps/bridge-whatsapp/tsconfig.json +++ b/apps/bridge-whatsapp/tsconfig.json @@ -1,26 +1,8 @@ { + "extends": "@link-stack/typescript-config/tsconfig.node.json", "compilerOptions": { - "target": "es2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", "outDir": "build/main", - "rootDir": "src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "inlineSources": true, - "skipLibCheck": true, - "strict": true, - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "isolatedModules": true, - "incremental": true, - "composite": true, - "rewriteRelativeImportExtensions": true, - "types": ["node"], - "lib": ["es2022", "DOM"] + "rootDir": "src" }, "include": ["src/**/*.ts", "src/**/.*.ts"], "exclude": ["node_modules/**"] diff --git a/docker/compose/bridge-signal.yml b/docker/compose/bridge-signal.yml new file mode 100644 index 0000000..faf9ed4 --- /dev/null +++ b/docker/compose/bridge-signal.yml @@ -0,0 +1,22 @@ +services: + bridge-signal: + container_name: bridge-signal + build: + context: ../../ + dockerfile: ./apps/bridge-signal/Dockerfile + image: registry.gitlab.com/digiresilience/link/link-stack/bridge-signal:${LINK_STACK_VERSION} + restart: ${RESTART} + environment: + PORT: 5002 + NODE_ENV: production + ZAMMAD_URL: http://zammad-nginx:8080 + SIGNAL_DATA_DIR: /home/node/signal-data + BRIDGE_SIGNAL_AUTO_GROUPS: ${BRIDGE_SIGNAL_AUTO_GROUPS:-false} + volumes: + - bridge-signal-data:/home/node/signal-data + ports: + - 5002:5002 + +volumes: + bridge-signal-data: + driver: local diff --git a/docker/compose/signal-cli-rest-api.yml b/docker/compose/signal-cli-rest-api.yml deleted file mode 100644 index e124b8a..0000000 --- a/docker/compose/signal-cli-rest-api.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - signal-cli-rest-api: - container_name: signal-cli-rest-api - build: ../signal-cli-rest-api - image: registry.gitlab.com/digiresilience/link/link-stack/signal-cli-rest-api:develop - environment: - - MODE=normal - volumes: - - signal-cli-rest-api-data:/home/.local/share/signal-cli - ports: - - 8080:8080 - -volumes: - signal-cli-rest-api-data: - driver: local diff --git a/docker/compose/zammad.yml b/docker/compose/zammad.yml index 5c27b78..19ac174 100644 --- a/docker/compose/zammad.yml +++ b/docker/compose/zammad.yml @@ -15,7 +15,7 @@ x-zammad-vars: &common-zammad-variables ELASTICSEARCH_SSL_VERIFY: "false" # this doesn't set es_ssl_verify as expected, but ideally it would ELASTICSEARCH_SCHEMA: "https" BRIDGE_SIGNAL_AUTO_GROUPS: ${BRIDGE_SIGNAL_AUTO_GROUPS} - SIGNAL_CLI_URL: "http://signal-cli-rest-api:8080" + BRIDGE_SIGNAL_URL: "http://bridge-signal:5002" BRIDGE_WHATSAPP_URL: "http://bridge-whatsapp:5000" FORMSTACK_FIELD_MAPPING: ${FORMSTACK_FIELD_MAPPING} diff --git a/docker/scripts/docker.js b/docker/scripts/docker.js index c9883d5..0c19b90 100644 --- a/docker/scripts/docker.js +++ b/docker/scripts/docker.js @@ -5,10 +5,11 @@ const app = process.argv[2]; const command = process.argv[3]; const files = { - all: ["zammad", "postgresql", "opensearch", "signal-cli-rest-api", "bridge-whatsapp", "bridge-deltachat"], - dev: ["zammad", "postgresql", "opensearch", "signal-cli-rest-api"], + all: ["zammad", "postgresql", "opensearch", "bridge-signal", "bridge-whatsapp", "bridge-deltachat"], + dev: ["zammad", "postgresql", "opensearch", "bridge-signal"], opensearch: ["opensearch"], - zammad: ["zammad", "postgresql", "opensearch", "signal-cli-rest-api"], + zammad: ["zammad", "postgresql", "opensearch", "bridge-signal"], + signal: ["bridge-signal"], whatsapp: ["bridge-whatsapp"], deltachat: ["bridge-deltachat"], }; diff --git a/docker/signal-cli-rest-api/Dockerfile b/docker/signal-cli-rest-api/Dockerfile deleted file mode 100644 index 1123734..0000000 --- a/docker/signal-cli-rest-api/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM bbernhard/signal-cli-rest-api:0.95 diff --git a/package.json b/package.json index b2f44ae..c9c8e77 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "scripts": { "dev": "pnpm --filter @link-stack/bridge-whatsapp run dev", "build": "turbo build", + "lint": "turbo lint", + "format": "turbo format", + "format:check": "turbo format:check", "update-version": "node --experimental-strip-types scripts/update-version.ts", "clean": "rm -f pnpm-lock.yaml && rm -rf node_modules && rm -rf .turbo && rm -rf apps/*/node_modules && rm -rf packages/*/node_modules && rm -rf packages/*/.turbo && rm -rf packages/*/build && rm -rf docker/zammad/addons/*", "docker:all:up": "node docker/scripts/docker.js all up", @@ -18,6 +21,8 @@ "docker:zammad:up": "node docker/scripts/docker.js zammad up", "docker:zammad:down": "node docker/scripts/docker.js zammad down", "docker:zammad:build": "node docker/scripts/docker.js zammad build", + "docker:signal:up": "node docker/scripts/docker.js signal up", + "docker:signal:down": "node docker/scripts/docker.js signal down", "docker:whatsapp:up": "node docker/scripts/docker.js whatsapp up", "docker:whatsapp:down": "node docker/scripts/docker.js whatsapp down", "docker:deltachat:up": "node docker/scripts/docker.js deltachat up", diff --git a/packages/eslint-config/node.js b/packages/eslint-config/node.js new file mode 100644 index 0000000..d4dec50 --- /dev/null +++ b/packages/eslint-config/node.js @@ -0,0 +1,52 @@ +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsparser from "@typescript-eslint/parser"; +import prettier from "eslint-config-prettier"; +import unicorn from "eslint-plugin-unicorn"; +import importX from "eslint-plugin-import-x"; + +export default [ + { + files: ["**/*.ts"], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + }, + plugins: { + "@typescript-eslint": tseslint, + unicorn, + "import-x": importX, + }, + rules: { + ...tseslint.configs.recommended.rules, + ...unicorn.configs.recommended.rules, + + // Relax unicorn rules + "unicorn/no-null": "off", + "unicorn/prevent-abbreviations": "off", + "unicorn/no-process-exit": "off", + "unicorn/prefer-top-level-await": "off", + + // Import rules + "import-x/no-duplicates": "error", + "import-x/order": [ + "error", + { + groups: ["builtin", "external", "internal", "parent", "sibling", "index"], + "newlines-between": "always", + alphabetize: { order: "asc" }, + }, + ], + + // TypeScript rules + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-explicit-any": "warn", + }, + }, + prettier, +]; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 0000000..55e4935 --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,20 @@ +{ + "name": "@link-stack/eslint-config", + "version": "3.5.0-beta.1", + "private": true, + "type": "module", + "exports": { + "./node": "./node.js" + }, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-unicorn": "^58.0.0", + "eslint-plugin-import-x": "^4.12.2" + }, + "peerDependencies": { + "eslint": "^9", + "typescript": "^5" + } +} diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..4979e41 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,27 @@ +{ + "name": "@link-stack/logger", + "version": "3.5.0-beta.1", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts --clean" + }, + "dependencies": { + "pino": "^9.6.0", + "pino-pretty": "^13.0.0" + }, + "devDependencies": { + "@link-stack/typescript-config": "workspace:*", + "@types/node": "*", + "tsup": "^8.5.0", + "typescript": "^5.9.3" + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000..ff7f255 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,86 @@ +import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino"; + +export type Logger = PinoLogger; + +const getLogLevel = (): string => { + return process.env.LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug"); +}; + +const getPinoConfig = (): LoggerOptions => { + const isDevelopment = process.env.NODE_ENV !== "production"; + + const baseConfig: LoggerOptions = { + level: getLogLevel(), + formatters: { + level: (label) => { + return { level: label.toUpperCase() }; + }, + }, + timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, + redact: { + paths: [ + "password", + "token", + "secret", + "api_key", + "apiKey", + "authorization", + "cookie", + "HandshakeKey", + "receivedSecret", + "access_token", + "refresh_token", + "zammadCsrfToken", + "clientSecret", + "*.password", + "*.token", + "*.secret", + "*.api_key", + "*.apiKey", + "*.authorization", + "*.cookie", + "*.access_token", + "*.refresh_token", + "*.zammadCsrfToken", + "*.HandshakeKey", + "*.receivedSecret", + "*.clientSecret", + "payload.HandshakeKey", + "headers.authorization", + "headers.cookie", + "headers.Authorization", + "headers.Cookie", + "credentials.password", + "credentials.secret", + "credentials.token", + ], + censor: "[REDACTED]", + }, + }; + + if (isDevelopment) { + return { + ...baseConfig, + transport: { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "SYS:standard", + ignore: "pid,hostname", + singleLine: false, + messageFormat: "{msg}", + }, + }, + }; + } + + return baseConfig; +}; + +export const logger: Logger = pino(getPinoConfig()); + +export const createLogger = (name: string, context?: Record): Logger => { + return logger.child({ name, ...context }); +}; + +export default logger; diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000..447d138 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@link-stack/typescript-config/tsconfig.node.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/prettier-config/index.json b/packages/prettier-config/index.json new file mode 100644 index 0000000..0022f40 --- /dev/null +++ b/packages/prettier-config/index.json @@ -0,0 +1,8 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "es5" +} diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json new file mode 100644 index 0000000..d1bfcc9 --- /dev/null +++ b/packages/prettier-config/package.json @@ -0,0 +1,6 @@ +{ + "name": "@link-stack/prettier-config", + "version": "3.5.0-beta.1", + "private": true, + "main": "index.json" +} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json new file mode 100644 index 0000000..5710a85 --- /dev/null +++ b/packages/typescript-config/package.json @@ -0,0 +1,6 @@ +{ + "name": "@link-stack/typescript-config", + "version": "3.5.0-beta.1", + "private": true, + "files": ["tsconfig.node.json"] +} diff --git a/packages/typescript-config/tsconfig.node.json b/packages/typescript-config/tsconfig.node.json new file mode 100644 index 0000000..bcea2ce --- /dev/null +++ b/packages/typescript-config/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "inlineSources": true, + "skipLibCheck": true, + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "composite": true, + "rewriteRelativeImportExtensions": true, + "types": ["node"], + "lib": ["es2022", "DOM"] + } +} diff --git a/packages/zammad-addon-link/src/app/assets/javascripts/app/controllers/_channel/cdr_signal.coffee b/packages/zammad-addon-link/src/app/assets/javascripts/app/controllers/_channel/cdr_signal.coffee index 3ed6865..28fe30f 100644 --- a/packages/zammad-addon-link/src/app/assets/javascripts/app/controllers/_channel/cdr_signal.coffee +++ b/packages/zammad-addon-link/src/app/assets/javascripts/app/controllers/_channel/cdr_signal.coffee @@ -13,7 +13,6 @@ class ChannelCdrSignal extends App.ControllerSubContent constructor: -> super - #@interval(@load, 60000) @load() load: => @@ -154,7 +153,7 @@ class ChannelCdrSignal extends App.ControllerSubContent ) class FormAdd extends App.ControllerModal - head: 'Add Web Form' + head: 'Add Signal Bot' shown: true button: 'Add' buttonCancel: true @@ -199,24 +198,101 @@ class FormAdd extends App.ControllerModal onSubmit: (e) => @formDisable(e) + params = @formParams() + + # Auto-generate bot_token if not provided + if !params.bot_token || params.bot_token.trim() == '' + params.bot_token = @generateToken() + @ajax( id: 'cdr_signal_app_verify' type: 'POST' url: "#{@apiPath}/channels_cdr_signal" - data: JSON.stringify(@formParams()) + data: JSON.stringify(params) processData: true - success: => + success: (data) => @isChanged = true - @close() + channelId = data.id + + # Start device linking if phone number provided + if params.phone_number && params.phone_number.trim() != '' + @startLinking(channelId, params.phone_number) + else + @close() error: (xhr) => data = JSON.parse(xhr.responseText) @formEnable(e) - error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.') + error_message = App.i18n.translateContent(data.error || 'Unable to save.') @el.find('.alert').removeClass('hidden').text(error_message) ) + startLinking: (channelId, phoneNumber) => + @ajax( + id: 'cdr_signal_register' + type: 'POST' + url: "#{@apiPath}/channels_cdr_signal_register/#{channelId}" + data: JSON.stringify(phone_number: phoneNumber) + processData: true + success: (data) => + if data.linkUri + @showLinkingStep(channelId, data.linkUri) + else + @close() + error: (xhr) => + data = JSON.parse(xhr.responseText) + error_message = App.i18n.translateContent(data.error || 'Failed to start device linking.') + @el.find('.alert').removeClass('hidden').text(error_message) + @el.find('.js-submit').removeAttr('disabled') + ) + + showLinkingStep: (channelId, linkUri) => + @el.find('.modal-body').html(App.view('cdr_signal/form_link')( + linkUri: linkUri + )) + @el.find('.js-submit').text(App.i18n.translateContent('Done')) + + # Generate QR code using an API service + qrImg = @el.find('.js-qr-image') + encodedUri = encodeURIComponent(linkUri) + qrImg.attr('src', "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=#{encodedUri}") + + # Poll for registration status + @pollingInterval = setInterval(=> + @checkRegistration(channelId) + , 3000) + + checkRegistration: (channelId) => + # Check bot status by reading channel and querying bridge + channel = App.Channel.find(channelId) + return unless channel + + @ajax( + id: 'cdr_signal_check_status' + type: 'GET' + url: "#{@apiPath}/channels_cdr_signal" + processData: true + success: (data) => + # Linking is complete when bridge confirms registration + # For now we rely on manual "Done" click + ) + + onClosed: => + if @pollingInterval + clearInterval(@pollingInterval) + @pollingInterval = null + return if !@isChanged + @isChanged = false + @load() + + generateToken: -> + chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-' + result = '' + for i in [0...32] + result += chars.charAt(Math.floor(Math.random() * chars.length)) + result + class FormEdit extends App.ControllerModal - head: 'Web Form Info' + head: 'Signal Bot Info' shown: true buttonCancel: true diff --git a/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_add.jst.eco b/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_add.jst.eco index 4bf488d..907b536 100644 --- a/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_add.jst.eco +++ b/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_add.jst.eco @@ -2,34 +2,27 @@
- +
- + +

<%- @T('The phone number linked to your Signal account. A QR code will be shown to link the device.') %>

- +
- + +

<%- @T('Leave blank to auto-generate.') %>

- -
-
- -
-
- -
-
- +
@@ -38,7 +31,7 @@
- +
diff --git a/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_edit.jst.eco b/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_edit.jst.eco index fc033df..091edc2 100644 --- a/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_edit.jst.eco +++ b/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_edit.jst.eco @@ -3,31 +3,22 @@
- +
-
+
- +
-
-
- -
-
- -
-
-
@@ -38,7 +29,7 @@
- +
@@ -46,7 +37,7 @@
- +
" class="form-control input js-select" readonly> diff --git a/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_link.jst.eco b/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_link.jst.eco new file mode 100644 index 0000000..c591bdd --- /dev/null +++ b/packages/zammad-addon-link/src/app/assets/javascripts/app/views/cdr_signal/form_link.jst.eco @@ -0,0 +1,10 @@ + +
+

<%- @T('Link Signal Device') %>

+

<%- @T('Scan this QR code with your Signal app to link this device.') %>

+

<%- @T('In Signal, go to Settings > Linked Devices > Link New Device.') %>

+
+ QR Code +
+

<%= @linkUri %>

+
diff --git a/packages/zammad-addon-link/src/app/controllers/channels_cdr_signal_controller.rb b/packages/zammad-addon-link/src/app/controllers/channels_cdr_signal_controller.rb index 8b5dae0..9fc0376 100644 --- a/packages/zammad-addon-link/src/app/controllers/channels_cdr_signal_controller.rb +++ b/packages/zammad-addon-link/src/app/controllers/channels_cdr_signal_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class ChannelsCdrSignalController < ApplicationController - prepend_before_action -> { authentication_check && authorize! }, except: [:webhook] - skip_before_action :verify_csrf_token, only: [:webhook] + prepend_before_action -> { authentication_check && authorize! }, except: %i[webhook bot_webhook] + skip_before_action :verify_csrf_token, only: %i[webhook bot_webhook] include CreatesTicketArticles @@ -60,7 +60,6 @@ class ChannelsCdrSignalController < ApplicationController adapter: 'cdr_signal', phone_number: params[:phone_number], bot_token: params[:bot_token], - bot_endpoint: params[:bot_endpoint], token: SecureRandom.urlsafe_base64(48), organization_id: params[:organization_id] }, @@ -87,7 +86,6 @@ class ChannelsCdrSignalController < ApplicationController begin channel.options[:phone_number] = params[:phone_number] channel.options[:bot_token] = params[:bot_token] - channel.options[:bot_endpoint] = params[:bot_endpoint] channel.options[:organization_id] = params[:organization_id] channel.group_id = params[:group_id] channel.save! @@ -97,6 +95,20 @@ class ChannelsCdrSignalController < ApplicationController render json: channel end + def register + channel = Channel.find_by(id: params[:id], area: 'Signal::Number') + unless channel + render json: { error: 'Channel not found' }, status: :not_found + return + end + + api = CdrSignalApi.new + result = api.register(channel.options[:bot_token], params[:phone_number]) + render json: result + rescue StandardError => e + render json: { error: e.message }, status: :unprocessable_entity + end + def rotate_token channel = Channel.find_by(id: params[:id], area: 'Signal::Number') channel.options[:token] = SecureRandom.urlsafe_base64(48) @@ -133,13 +145,33 @@ class ChannelsCdrSignalController < ApplicationController false end + # Webhook endpoint for incoming messages from bridge-signal (bot-centric) + # Bridge POSTs here with incoming messages, group events, etc. + def bot_webhook + bot_id = params[:id] + channel = Channel.where(area: 'Signal::Number', active: true) + .find { |c| c.options[:bot_token] == bot_id } + return render(json: {}, status: 401) unless channel + + # Handle events + case params[:event] + when 'group_created' + return update_group + when 'group_member_joined' + return handle_group_member_joined + end + + # Process incoming message + process_incoming_message(channel) + end + + # Legacy webhook endpoint (token-based) def webhook token = params['token'] return render json: {}, status: 401 unless token channel = channel_for_token(token) return render json: {}, status: 401 if !channel || !channel.active - # Use constant-time comparison to prevent timing attacks return render json: {}, status: 401 unless ActiveSupport::SecurityUtils.secure_compare( channel.options[:token].to_s, token.to_s @@ -155,16 +187,14 @@ class ChannelsCdrSignalController < ApplicationController return handle_group_member_joined end - channel_id = channel.id + process_incoming_message(channel) + end - # validate input + def update_group errors = {} - - # %i[to - # from - # message_id - # sent_at].each | field | - # (errors[field] = 'required' if params[field].blank?) + errors['event'] = 'required' unless params[:event].present? + errors['conversation_id'] = 'required' unless params[:conversation_id].present? + errors['group_id'] = 'required' unless params[:group_id].present? if errors.present? render json: { @@ -173,6 +203,139 @@ class ChannelsCdrSignalController < ApplicationController return end + unless params[:event] == 'group_created' + render json: { error: 'Unsupported event type' }, status: :bad_request + return + end + + ticket = Ticket.find_by(id: params[:conversation_id]) || + Ticket.find_by(number: params[:conversation_id]) + + unless ticket + Rails.logger.error "Signal group update: Ticket not found for conversation_id #{params[:conversation_id]}" + render json: { error: 'Ticket not found' }, status: :not_found + return + end + + existing_chat_id = ticket.preferences&.dig(:cdr_signal, :chat_id) || + ticket.preferences&.dig('cdr_signal', 'chat_id') + if existing_chat_id&.start_with?('group.') + Rails.logger.info "Signal group update: Ticket #{ticket.id} already has group #{existing_chat_id}, ignoring new group #{params[:group_id]}" + render json: { + success: true, + skipped: true, + reason: 'Ticket already has a group assigned', + existing_group_id: existing_chat_id, + ticket_id: ticket.id, + ticket_number: ticket.number + }, status: :ok + return + end + + ticket.preferences ||= {} + ticket.preferences[:cdr_signal] ||= {} + ticket.preferences[:cdr_signal][:chat_id] = params[:group_id] + ticket.preferences[:cdr_signal][:original_recipient] = params[:original_recipient] if params[:original_recipient].present? + ticket.preferences[:cdr_signal][:group_created_at] = params[:timestamp] if params[:timestamp].present? + ticket.preferences[:cdr_signal][:group_joined] = params[:group_joined] if params.key?(:group_joined) + + ticket.save! + + Rails.logger.info "Signal group #{params[:group_id]} associated with ticket #{ticket.id}" + + render json: { + success: true, + ticket_id: ticket.id, + ticket_number: ticket.number + }, status: :ok + end + + def handle_group_member_joined + errors = {} + errors['event'] = 'required' unless params[:event].present? + errors['group_id'] = 'required' unless params[:group_id].present? + errors['member_phone'] = 'required' unless params[:member_phone].present? + + if errors.present? + render json: { + errors: errors + }, status: :bad_request + return + end + + state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) + + ticket = Ticket.where.not(state_id: state_ids) + .where("preferences LIKE ?", "%chat_id: #{params[:group_id]}%") + .order(updated_at: :desc) + .first + + unless ticket + Rails.logger.warn "Signal group member joined: Ticket not found for group_id #{params[:group_id]}" + render json: { error: 'Ticket not found for this group' }, status: :not_found + return + end + + if ticket.preferences.dig('cdr_signal', 'group_joined') == true + render json: { + success: true, + ticket_id: ticket.id, + ticket_number: ticket.number, + group_joined: true, + already_joined: true + }, status: :ok + return + end + + member_phone = params[:member_phone] + ticket.preferences[:cdr_signal][:group_joined] = true + ticket.preferences[:cdr_signal][:group_joined_at] = params[:timestamp] if params[:timestamp].present? + ticket.preferences[:cdr_signal][:group_joined_by] = member_phone + + ticket.save! + + Rails.logger.info "Signal group member #{member_phone} joined group #{params[:group_id]} for ticket #{ticket.id}" + + articles_with_pending_notification = Ticket::Article.where(ticket_id: ticket.id) + .where("preferences LIKE ?", "%group_not_joined_note_added: true%") + + if articles_with_pending_notification.exists? + resolution_note_exists = Ticket::Article.where(ticket_id: ticket.id) + .where("preferences LIKE ?", "%group_joined_resolution: true%") + .exists? + + unless resolution_note_exists + Ticket::Article.create( + ticket_id: ticket.id, + content_type: 'text/plain', + body: 'Recipient has now joined the Signal group. Pending messages will be delivered shortly.', + internal: true, + sender: Ticket::Article::Sender.find_by(name: 'System'), + type: Ticket::Article::Type.find_by(name: 'note'), + preferences: { + delivery_message: true, + group_joined_resolution: true, + }, + updated_by_id: 1, + created_by_id: 1, + ) + Rails.logger.info "Ticket ##{ticket.number}: Added resolution note about customer joining Signal group" + end + end + + render json: { + success: true, + ticket_id: ticket.id, + ticket_number: ticket.number, + group_joined: true + }, status: :ok + end + + private + + def process_incoming_message(channel) + channel_id = channel.id + message_id = params[:message_id] return if Ticket::Article.exists?(message_id: "cdr_signal.#{message_id}") @@ -181,16 +344,9 @@ class ChannelsCdrSignalController < ApplicationController sender_phone_number = params[:from].present? ? params[:from].strip : nil sender_user_id = params[:user_id].present? ? params[:user_id].strip : nil - # Check if this is a group message using the is_group flag from bridge-worker - # This flag is set when: - # 1. The original message came from a Signal group - # 2. Bridge-worker created a new group for the conversation is_group_message = params[:is_group].to_s == 'true' - # Lookup customer with fallback chain: - # 1. Phone number in phone/mobile fields (preferred) - # 2. Signal user ID in signal_uid field - # 3. User ID in phone/mobile fields (legacy - we used to store UUIDs there) + # Lookup customer with fallback chain customer = nil if sender_phone_number.present? customer = User.find_by(phone: sender_phone_number) @@ -198,7 +354,6 @@ class ChannelsCdrSignalController < ApplicationController end if customer.nil? && sender_user_id.present? customer = User.find_by(signal_uid: sender_user_id) - # Legacy fallback: user ID might be stored in phone field customer ||= User.find_by(phone: sender_user_id) customer ||= User.find_by(mobile: sender_user_id) end @@ -220,16 +375,13 @@ class ChannelsCdrSignalController < ApplicationController ) end - # Update signal_uid if we have it and customer doesn't if sender_user_id.present? && customer.signal_uid.blank? customer.update(signal_uid: sender_user_id) end - # Update phone if we have it and customer only has user_id in phone field if sender_phone_number.present? && customer.phone == sender_user_id customer.update(phone: sender_phone_number) end - # set current user UserInfo.current_user_id = customer.id current_user_set(customer, 'token_auth') @@ -261,71 +413,40 @@ class ChannelsCdrSignalController < ApplicationController title = "Message from #{sender_display} at #{sent_at}" body = message - # find ticket or create one state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) if is_group_message - Rails.logger.info "=== SIGNAL GROUP TICKET LOOKUP ===" - Rails.logger.info "Looking for ticket with group_id: #{receiver_phone_number}" - Rails.logger.info "Customer ID: #{customer.id}" - Rails.logger.info "Customer Phone: #{sender_display}" - Rails.logger.info "Channel ID: #{channel.id}" - begin - # Use text search on preferences YAML to efficiently find tickets without loading all into memory - # This prevents DoS attacks from memory exhaustion ticket = Ticket.where.not(state_id: state_ids) .where("preferences LIKE ?", "%channel_id: #{channel.id}%") .where("preferences LIKE ?", "%chat_id: #{receiver_phone_number}%") .order(updated_at: :desc) .first - - if ticket - Rails.logger.info "=== FOUND MATCHING TICKET BY GROUP ID: ##{ticket.number} ===" - # Update customer if different (handles duplicate phone numbers) - if ticket.customer_id != customer.id - Rails.logger.info "Updating ticket customer from #{ticket.customer_id} to #{customer.id}" - ticket.customer_id = customer.id - end - else - Rails.logger.info "=== NO MATCHING TICKET BY GROUP ID - CHECKING BY PHONE NUMBER ===" - end rescue => e Rails.logger.error "Error during group ticket lookup: #{e.message}" Rails.logger.error e.backtrace.join("\n") end else - Rails.logger.info "Not a group message or no group_id, finding most recent ticket" ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first end if ticket - # check if title need to be updated ticket.title = title if ticket.title == '-' new_state = Ticket::State.find_by(default_create: true) ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id else - # Set up chat_id based on whether this is a group message - # For direct messages, prefer UUID (more stable than phone numbers which can change) chat_id = is_group_message ? receiver_phone_number : (sender_user_id.presence || sender_phone_number) - # Build preferences with group_id included if needed cdr_signal_prefs = { bot_token: channel.options[:bot_token], chat_id: chat_id, user_id: sender_user_id } - # Store original recipient phone for group tickets to enable ticket splitting if is_group_message cdr_signal_prefs[:original_recipient] = sender_phone_number end - Rails.logger.info "=== CREATING NEW TICKET ===" - Rails.logger.info "Preferences to be stored:" - Rails.logger.info " - channel_id: #{channel.id}" - Rails.logger.info " - cdr_signal: #{cdr_signal_prefs.inspect}" - ticket = Ticket.new( group_id: channel.group_id, title: title, @@ -361,9 +482,6 @@ class ChannelsCdrSignalController < ApplicationController if attachment_data_base64.present? article_params[:attachments] = [ - # i don't even... - # this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb - # we need help from the ruby gods { 'filename' => attachment_filename, :filename => attachment_filename, @@ -390,185 +508,4 @@ class ChannelsCdrSignalController < ApplicationController render json: result, status: :ok end - - # Webhook endpoint for receiving group creation notifications from bridge-worker - # This is called when a Signal group is created for a conversation - # Expected payload: - # { - # "event": "group_created", - # "conversation_id": "ticket_id_or_number", - # "original_recipient": "+1234567890", - # "group_id": "uuid-of-signal-group", - # "timestamp": "ISO8601 timestamp" - # } - def update_group - # Validate required parameters - errors = {} - errors['event'] = 'required' unless params[:event].present? - errors['conversation_id'] = 'required' unless params[:conversation_id].present? - errors['group_id'] = 'required' unless params[:group_id].present? - - if errors.present? - render json: { - errors: errors - }, status: :bad_request - return - end - - # Only handle group_created events for now - unless params[:event] == 'group_created' - render json: { error: 'Unsupported event type' }, status: :bad_request - return - end - - # Find the ticket by ID or number - # Try to find by both ID and number since ticket numbers can be numeric - ticket = Ticket.find_by(id: params[:conversation_id]) || - Ticket.find_by(number: params[:conversation_id]) - - unless ticket - Rails.logger.error "Signal group update: Ticket not found for conversation_id #{params[:conversation_id]}" - render json: { error: 'Ticket not found' }, status: :not_found - return - end - - # Idempotency check: if chat_id is already a group ID, don't overwrite it - # This prevents race conditions where multiple group_created webhooks arrive - # (e.g., due to retries after API timeouts during group creation) - existing_chat_id = ticket.preferences&.dig(:cdr_signal, :chat_id) || - ticket.preferences&.dig('cdr_signal', 'chat_id') - if existing_chat_id&.start_with?('group.') - Rails.logger.info "Signal group update: Ticket #{ticket.id} already has group #{existing_chat_id}, ignoring new group #{params[:group_id]}" - render json: { - success: true, - skipped: true, - reason: 'Ticket already has a group assigned', - existing_group_id: existing_chat_id, - ticket_id: ticket.id, - ticket_number: ticket.number - }, status: :ok - return - end - - # Update ticket preferences with the group information - ticket.preferences ||= {} - ticket.preferences[:cdr_signal] ||= {} - ticket.preferences[:cdr_signal][:chat_id] = params[:group_id] - ticket.preferences[:cdr_signal][:original_recipient] = params[:original_recipient] if params[:original_recipient].present? - ticket.preferences[:cdr_signal][:group_created_at] = params[:timestamp] if params[:timestamp].present? - - # Track whether user has joined the group (initially false) - # This will be updated to true when we receive a group join event from Signal - ticket.preferences[:cdr_signal][:group_joined] = params[:group_joined] if params.key?(:group_joined) - - ticket.save! - - Rails.logger.info "Signal group #{params[:group_id]} associated with ticket #{ticket.id}" - - render json: { - success: true, - ticket_id: ticket.id, - ticket_number: ticket.number - }, status: :ok - end - - # Webhook endpoint for receiving group member joined notifications from bridge-worker - # This is called when a user accepts the Signal group invitation - # Expected payload: - # { - # "event": "group_member_joined", - # "group_id": "group.base64encodedid", - # "member_phone": "+1234567890", - # "timestamp": "ISO8601 timestamp" - # } - def handle_group_member_joined - # Validate required parameters - errors = {} - errors['event'] = 'required' unless params[:event].present? - errors['group_id'] = 'required' unless params[:group_id].present? - errors['member_phone'] = 'required' unless params[:member_phone].present? - - if errors.present? - render json: { - errors: errors - }, status: :bad_request - return - end - - # Find ticket(s) with this group_id in preferences - # Use text search on preferences YAML for efficient lookup (prevents DoS from loading all tickets) - state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) - - ticket = Ticket.where.not(state_id: state_ids) - .where("preferences LIKE ?", "%chat_id: #{params[:group_id]}%") - .order(updated_at: :desc) - .first - - unless ticket - Rails.logger.warn "Signal group member joined: Ticket not found for group_id #{params[:group_id]}" - render json: { error: 'Ticket not found for this group' }, status: :not_found - return - end - - # Idempotency check: if already marked as joined, skip update and return success - # This prevents unnecessary database writes when the cron job sends duplicate notifications - if ticket.preferences.dig('cdr_signal', 'group_joined') == true - Rails.logger.debug "Signal group member #{params[:member_phone]} already marked as joined for group #{params[:group_id]} ticket #{ticket.id}, skipping update" - render json: { - success: true, - ticket_id: ticket.id, - ticket_number: ticket.number, - group_joined: true, - already_joined: true - }, status: :ok - return - end - - # Update group_joined flag - member_phone = params[:member_phone] - ticket.preferences[:cdr_signal][:group_joined] = true - ticket.preferences[:cdr_signal][:group_joined_at] = params[:timestamp] if params[:timestamp].present? - ticket.preferences[:cdr_signal][:group_joined_by] = member_phone - - ticket.save! - - Rails.logger.info "Signal group member #{member_phone} joined group #{params[:group_id]} for ticket #{ticket.id}" - - # Check if any articles had a group_not_joined notification and add resolution note - # Only add resolution note if we previously notified about the delivery issue - articles_with_pending_notification = Ticket::Article.where(ticket_id: ticket.id) - .where("preferences LIKE ?", "%group_not_joined_note_added: true%") - - if articles_with_pending_notification.exists? - # Check if we already added a resolution note for this ticket - resolution_note_exists = Ticket::Article.where(ticket_id: ticket.id) - .where("preferences LIKE ?", "%group_joined_resolution: true%") - .exists? - - unless resolution_note_exists - Ticket::Article.create( - ticket_id: ticket.id, - content_type: 'text/plain', - body: 'Recipient has now joined the Signal group. Pending messages will be delivered shortly.', - internal: true, - sender: Ticket::Article::Sender.find_by(name: 'System'), - type: Ticket::Article::Type.find_by(name: 'note'), - preferences: { - delivery_message: true, - group_joined_resolution: true, - }, - updated_by_id: 1, - created_by_id: 1, - ) - Rails.logger.info "Ticket ##{ticket.number}: Added resolution note about customer joining Signal group" - end - end - - render json: { - success: true, - ticket_id: ticket.id, - ticket_number: ticket.number, - group_joined: true - }, status: :ok - end end diff --git a/packages/zammad-addon-link/src/app/controllers/channels_cdr_whatsapp_controller.rb b/packages/zammad-addon-link/src/app/controllers/channels_cdr_whatsapp_controller.rb index 67c9aa6..10e97b2 100644 --- a/packages/zammad-addon-link/src/app/controllers/channels_cdr_whatsapp_controller.rb +++ b/packages/zammad-addon-link/src/app/controllers/channels_cdr_whatsapp_controller.rb @@ -125,22 +125,8 @@ class ChannelsCdrWhatsappController < ApplicationController channel = channel_for_bot_token(bot_token) return render json: { error: 'Channel not found' }, status: :not_found if !channel || !channel.active - # Normalize parameter names from bridge-whatsapp (camelCase) to Zammad (snake_case) - normalized_params = { - to: params[:to], - from: params[:from], - user_id: params[:userId] || params[:user_id], - message_id: params[:messageId] || params[:message_id], - sent_at: params[:sentAt] || params[:sent_at], - message: params[:message], - attachment: params[:attachment], - filename: params[:filename], - mime_type: params[:mimeType] || params[:mime_type] - } - # Use the channel's webhook token to reuse existing logic params[:token] = channel.options[:token] - normalized_params.each { |k, v| params[k] = v if v.present? } webhook end diff --git a/packages/zammad-addon-link/src/app/jobs/communicate_cdr_whatsapp_job.rb b/packages/zammad-addon-link/src/app/jobs/communicate_cdr_whatsapp_job.rb index 70e60b7..633dd23 100644 --- a/packages/zammad-addon-link/src/app/jobs/communicate_cdr_whatsapp_job.rb +++ b/packages/zammad-addon-link/src/app/jobs/communicate_cdr_whatsapp_job.rb @@ -29,7 +29,7 @@ class CommunicateCdrWhatsappJob < ApplicationJob log_error(article, "Can't find ticket.preferences['cdr_whatsapp']['chat_id'] for Ticket.find(#{article.ticket_id})") end - channel = ::CdrSignal.bot_by_bot_token(ticket.preferences['cdr_whatsapp']['bot_token']) + channel = ::CdrWhatsapp.bot_by_bot_token(ticket.preferences['cdr_whatsapp']['bot_token']) channel ||= ::Channel.lookup(id: ticket.preferences['channel_id']) unless channel log_error(article, diff --git a/packages/zammad-addon-link/src/config/initializers/signal_cli_config.rb b/packages/zammad-addon-link/src/config/initializers/signal_cli_config.rb index 3c6f1eb..cf8aba0 100644 --- a/packages/zammad-addon-link/src/config/initializers/signal_cli_config.rb +++ b/packages/zammad-addon-link/src/config/initializers/signal_cli_config.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -# Configuration for direct Signal CLI REST API access -# The SIGNAL_CLI_URL environment variable points to the signal-cli-rest-api container -# Default: http://signal-cli-rest-api:8080 -# -# This enables Zammad to poll for Signal messages directly without going through bridge-worker +# Configuration for bridge-signal API access +# The BRIDGE_SIGNAL_URL environment variable points to the bridge-signal container +# Default: http://bridge-signal:5002 Rails.application.config.after_initialize do - signal_cli_url = ENV.fetch('SIGNAL_CLI_URL', 'http://signal-cli-rest-api:8080') - Rails.logger.info "Signal CLI API URL configured: #{signal_cli_url}" + bridge_signal_url = ENV.fetch('BRIDGE_SIGNAL_URL', 'http://bridge-signal:5002') + Rails.logger.info "Bridge Signal API URL configured: #{bridge_signal_url}" end diff --git a/packages/zammad-addon-link/src/config/routes/channel_cdr_signal.rb b/packages/zammad-addon-link/src/config/routes/channel_cdr_signal.rb index 414056c..7ad11d8 100644 --- a/packages/zammad-addon-link/src/config/routes/channel_cdr_signal.rb +++ b/packages/zammad-addon-link/src/config/routes/channel_cdr_signal.rb @@ -8,6 +8,8 @@ Zammad::Application.routes.draw do match "#{api_path}/channels_cdr_signal/:id", to: 'channels_cdr_signal#update', via: :put match "#{api_path}/channels_cdr_signal_webhook/:token", to: 'channels_cdr_signal#webhook', via: :post match "#{api_path}/channels_cdr_signal_webhook/:token/update_group", to: 'channels_cdr_signal#update_group', via: :post + match "#{api_path}/channels_cdr_signal_bot_webhook/:id", to: 'channels_cdr_signal#bot_webhook', via: :post + match "#{api_path}/channels_cdr_signal_register/:id", to: 'channels_cdr_signal#register', via: :post match "#{api_path}/channels_cdr_signal_disable", to: 'channels_cdr_signal#disable', via: :post match "#{api_path}/channels_cdr_signal_enable", to: 'channels_cdr_signal#enable', via: :post match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#destroy', via: :delete diff --git a/packages/zammad-addon-link/src/db/addon/link/20260215000001_remove_signal_schedulers.rb b/packages/zammad-addon-link/src/db/addon/link/20260215000001_remove_signal_schedulers.rb new file mode 100644 index 0000000..20a426c --- /dev/null +++ b/packages/zammad-addon-link/src/db/addon/link/20260215000001_remove_signal_schedulers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemoveSignalSchedulers < ActiveRecord::Migration[5.2] + def self.up + # Remove polling schedulers -- bridge-signal now pushes messages via webhook + Scheduler.find_by(name: 'Fetch Signal messages')&.destroy + Scheduler.find_by(name: 'Check Signal group membership')&.destroy + end + + def self.down + # No-op: schedulers are no longer used + end +end diff --git a/packages/zammad-addon-link/src/lib/cdr_signal.rb b/packages/zammad-addon-link/src/lib/cdr_signal.rb index 7f2cef4..073b01b 100644 --- a/packages/zammad-addon-link/src/lib/cdr_signal.rb +++ b/packages/zammad-addon-link/src/lib/cdr_signal.rb @@ -5,92 +5,8 @@ require 'cdr_signal_api' class CdrSignal attr_accessor :client - # - # check token and return bot attributes of token - # - # bot = CdrSignal.check_token('token') - # - - def self.check_token(phone_number) - api = CdrSignalApi.new - unless api.check_number(phone_number) - raise "Phone number #{phone_number} is not registered with Signal CLI" - end - { 'id' => phone_number, 'number' => phone_number } - end - - # - # create or update channel, store bot attributes and verify token - # - # channel = CdrSignal.create_or_update_channel('token', params) - # - # returns - # - # channel # instance of Channel - # - - def self.create_or_update_channel(phone_number, params, channel = nil) - # verify phone number is registered with Signal CLI - bot = CdrSignal.check_token(phone_number) - - raise 'Bot already exists!' unless channel && CdrSignal.bot_duplicate?(bot['id']) - - raise 'Group needed!' if params[:group_id].blank? - - group = Group.find_by(id: params[:group_id]) - raise 'Group invalid!' unless group - - unless channel - channel = CdrSignal.bot_by_bot_id(bot['id']) - channel ||= Channel.new - end - channel.area = 'Signal::Account' - channel.options = { - adapter: 'cdr_signal', - phone_number: phone_number, - welcome: params[:welcome] - } - channel.group_id = group.id - channel.active = true - channel.save! - channel - end - - # - # check if bot already exists as channel - # - # success = CdrSignal.bot_duplicate?(bot_id) - # - # returns - # - # channel # instance of Channel - # - - def self.bot_duplicate?(bot_id, channel_id = nil) - Channel.where(area: 'Signal::Account').each do |channel| - next unless channel.options - next unless channel.options[:bot] - next unless channel.options[:bot][:id] - next if channel.options[:bot][:id] != bot_id - next if channel.id.to_s == channel_id.to_s - - return true - end - false - end - - # - # get channel by bot_id - # - # channel = CdrSignal.bot_by_bot_id(bot_id) - # - # returns - # - # true|false - # - def self.bot_by_bot_token(bot_token) - Channel.where(area: 'Signal::Account').each do |channel| + Channel.where(area: 'Signal::Number').each do |channel| next unless channel.options next unless channel.options[:bot_token] return channel if channel.options[:bot_token].to_s == bot_token.to_s @@ -98,14 +14,6 @@ class CdrSignal nil end - # - # date = CdrSignal.timestamp_to_date('1543414973285') - # - # returns - # - # 2018-11-28T14:22:53.285Z - # - def self.timestamp_to_date(timestamp_str) Time.at(timestamp_str.to_i).utc.to_datetime end @@ -114,32 +22,14 @@ class CdrSignal format('%s@%s', from: message_raw['from'], timestamp: message_raw['timestamp']) end - # - # client = CdrSignal.new('token') - # - def initialize(phone_number) @phone_number = phone_number - @api = CdrSignalApi.new - end - - # - # client.send_message(chat_id, 'some message') - # - - def send_message(recipient, message) - return if Rails.env.test? - - @api.send_message(@phone_number, [recipient], message) end def user(number) { - # id: params[:message][:from][:id], id: number, username: number - # first_name: params[:message][:from][:first_name], - # last_name: params[:message][:from][:last_name] } end @@ -147,10 +37,8 @@ class CdrSignal Rails.logger.debug { 'Create user from message...' } Rails.logger.debug { message.inspect } - # do message_user lookup message_user = user(message[:source]) - # create or update user login = message_user[:username] || message_user[:id] auth = Authorization.find_by(uid: message[:source], provider: 'cdr_signal') @@ -177,7 +65,6 @@ class CdrSignal ) end - # create or update authorization auth_data = { uid: message_user[:id], username: login, @@ -196,22 +83,13 @@ class CdrSignal def to_ticket(message, user, group_id, channel) UserInfo.current_user_id = user.id - Rails.logger.debug { 'Create ticket from message...' } - Rails.logger.debug { message.inspect } - Rails.logger.debug { user.inspect } - Rails.logger.debug { group_id.inspect } - - # prepare title title = '-' title = message[:message][:body] unless message[:message][:body].nil? title = "#{title[0, 60]}..." if title.length > 60 - # find ticket or create one state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first if ticket - - # check if title need to be updated ticket.title = title if ticket.title == '-' new_state = Ticket::State.find_by(default_create: true) ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id @@ -238,16 +116,11 @@ class CdrSignal end def to_article(message, user, ticket, channel) - Rails.logger.debug { 'Create article from message...' } - Rails.logger.debug { message.inspect } - Rails.logger.debug { user.inspect } - Rails.logger.debug { ticket.inspect } - UserInfo.current_user_id = user.id article = Ticket::Article.new( from: message[:source], - to: channel[:options][:bot][:number], + to: channel[:options][:phone_number], body: message[:message][:body], content_type: 'text/plain', message_id: "cdr_signal.#{message[:id]}", @@ -264,12 +137,7 @@ class CdrSignal } ) - # TODO: attachments - # TODO voice - # TODO emojis - # if message[:message][:body] - Rails.logger.debug { article.inspect } article.save! Store.remove( @@ -283,15 +151,11 @@ class CdrSignal end def to_group(message, group_id, channel) - # begin import Rails.logger.debug { 'signal import message' } - # TODO: handle messages in group chats - return if Ticket::Article.find_by(message_id: message[:id]) ticket = nil - # use transaction Transaction.execute(reset_user_id: true) do user = to_user(message) ticket = to_ticket(message, user, group_id, channel) @@ -302,25 +166,20 @@ class CdrSignal end def from_article(article) - # sends a message from a zammad article using direct Signal CLI API - Rails.logger.debug { "Create signal message from article..." } - # Get the recipient from ticket preferences ticket = Ticket.find_by(id: article.ticket_id) raise "No ticket found for article #{article.id}" unless ticket - # Get channel to find the bot phone number channel = Channel.find_by(id: ticket.preferences[:channel_id]) raise "No channel found for ticket #{ticket.id}" unless channel - bot_phone_number = channel.options[:phone_number] - raise "No phone number configured for channel #{channel.id}" unless bot_phone_number + bot_id = channel.options[:bot_token] + raise "No bot_token configured for channel #{channel.id}" unless bot_id recipient = ticket.preferences.dig('cdr_signal', 'chat_id') enable_auto_groups = ENV['BRIDGE_SIGNAL_AUTO_GROUPS'].to_s.downcase == 'true' - # If auto-groups is enabled and no chat_id, use original_recipient if recipient.blank? && enable_auto_groups recipient = ticket.preferences.dig('cdr_signal', 'original_recipient') raise "No Signal chat_id or original_recipient found in ticket preferences" unless recipient @@ -328,80 +187,45 @@ class CdrSignal raise "No Signal chat_id found in ticket preferences" end - Rails.logger.debug { "Sending to recipient: '#{recipient}'" } - - # Use Signal CLI API api = CdrSignalApi.new - - # Check if we need to create a group (auto-groups enabled, recipient is not already a group) - is_group_id = recipient.start_with?('group.') - final_recipient = recipient - - if enable_auto_groups && !is_group_id && recipient.present? - # Create a group for this conversation - begin - group_name = "Support Request: #{ticket.number}" - - Rails.logger.info "Creating Signal group '#{group_name}' for ticket ##{ticket.number}" - - create_result = api.create_group( - bot_phone_number, - name: group_name, - members: [recipient], - description: 'Private support conversation' - ) - - if create_result['id'].present? - final_recipient = create_result['id'] - - # Update ticket preferences with the new group ID - ticket.preferences[:cdr_signal] ||= {} - ticket.preferences[:cdr_signal][:chat_id] = final_recipient - ticket.preferences[:cdr_signal][:original_recipient] = recipient - ticket.preferences[:cdr_signal][:group_joined] = false - ticket.preferences[:cdr_signal][:group_created_at] = Time.current.iso8601 - ticket.save! - - Rails.logger.info "Created Signal group #{final_recipient} for ticket ##{ticket.number}" - end - rescue StandardError => e - Rails.logger.error "Failed to create Signal group: #{e.message}" - # Continue with original recipient if group creation fails - end - end - - # Get attachments from the article options = {} + + # Encode attachments attachments = Store.list(object: 'Ticket::Article', o_id: article.id) if attachments.any? - attachment_data = attachments.map do |attachment| + options[:attachments] = attachments.map do |a| { - data: Base64.strict_encode64(attachment.content), - filename: attachment.filename, - mime_type: attachment.preferences['Mime-Type'] || attachment.preferences['Content-Type'] || 'application/octet-stream' + data: Base64.strict_encode64(a.content), + filename: a.filename, + mime_type: a.preferences['Mime-Type'] || a.preferences['Content-Type'] || 'application/octet-stream' } end - options[:attachments] = attachment_data - Rails.logger.debug { "Sending #{attachment_data.length} attachment(s) with message" } end - # Send the message via direct Signal CLI API - result = api.send_message(bot_phone_number, [final_recipient], article[:body], options) - - Rails.logger.info "Sent Signal message to #{final_recipient}" - - # Update group name if needed (for consistency) - if final_recipient.start_with?('group.') - expected_name = "Support Request: #{ticket.number}" - api.update_group(bot_phone_number, final_recipient, name: expected_name) + # Auto-group: let bridge handle it + if enable_auto_groups && !recipient&.start_with?('group.') + recipient = ticket.preferences.dig('cdr_signal', 'original_recipient') || recipient + options[:auto_group] = { ticketNumber: ticket.number.to_s } end - # Return result in expected format + result = api.send_message(bot_id, recipient, article[:body], options) + send_result = result.is_a?(Hash) ? result['result'] : nil + + # If bridge created a group, update ticket preferences + if send_result.is_a?(Hash) && send_result['groupId'].present? + ticket.preferences[:cdr_signal] ||= {} + ticket.preferences[:cdr_signal][:chat_id] = send_result['groupId'] + ticket.preferences[:cdr_signal][:group_joined] = false + ticket.preferences[:cdr_signal][:group_created_at] = Time.current.iso8601 + ticket.save! + end + + # Return in expected format { 'result' => { - 'to' => final_recipient, - 'from' => bot_phone_number, - 'timestamp' => result['timestamp'] || Time.current.to_i * 1000 + 'to' => send_result&.dig('recipient') || recipient, + 'from' => send_result&.dig('source') || bot_id, + 'timestamp' => send_result&.dig('timestamp') || Time.current.to_i * 1000 } } end diff --git a/packages/zammad-addon-link/src/lib/cdr_signal_api.rb b/packages/zammad-addon-link/src/lib/cdr_signal_api.rb index 602b17c..b4f0326 100644 --- a/packages/zammad-addon-link/src/lib/cdr_signal_api.rb +++ b/packages/zammad-addon-link/src/lib/cdr_signal_api.rb @@ -1,148 +1,95 @@ # frozen_string_literal: true require 'json' -require 'net/http' -require 'net/https' -require 'uri' -# Direct Signal CLI API client for communicating with signal-cli-rest-api -# All Signal operations go through this single class +# Bridge-signal API client +# Communicates with the bridge-signal Node.js service (replaces signal-cli-rest-api) class CdrSignalApi def initialize(base_url = nil) - @base_url = base_url || ENV.fetch('SIGNAL_CLI_URL', 'http://signal-cli-rest-api:8080') + @base_url = base_url || ENV.fetch('BRIDGE_SIGNAL_URL', 'http://bridge-signal:5002') end - # Fetch pending messages for a phone number - # GET /v1/receive/{number} - def fetch_messages(phone_number) - url = "#{@base_url}/v1/receive/#{CGI.escape(phone_number)}" + # Register/link a phone number + def register(bot_id, phone_number, device_name = 'Zammad') + post("/api/bots/#{bot_id}/register", { phoneNumber: phone_number, deviceName: device_name }) + end + + # Get bot status + def get_bot(bot_id) + get("/api/bots/#{bot_id}") + end + + # Unregister a bot + def unregister(bot_id) + post("/api/bots/#{bot_id}/unregister", {}) + end + + # Send a message (with optional auto-group) + def send_message(bot_id, recipient, message, options = {}) + data = { recipient: recipient, message: message } + data[:attachments] = options[:attachments] if options[:attachments].present? + data[:autoGroup] = options[:auto_group] if options[:auto_group].present? + post("/api/bots/#{bot_id}/send", data) + end + + # Group operations + def create_group(bot_id, name:, members:, description: nil) + post("/api/bots/#{bot_id}/groups", { name: name, members: members, description: description }.compact) + end + + def update_group(bot_id, group_id, name: nil, description: nil) + put("/api/bots/#{bot_id}/groups/#{CGI.escape(group_id)}", { name: name, description: description }.compact) + end + + def list_groups(bot_id) + get("/api/bots/#{bot_id}/groups") + end + + # Health check + def health + get('/api/health') + end + + private + + def get(path) + url = "#{@base_url}#{path}" response = Faraday.get(url, nil, { 'Accept' => 'application/json' }) - return [] unless response.success? + + unless response.success? + Rails.logger.error "CdrSignalApi: GET #{path} failed: #{response.status} #{response.body}" + return nil + end JSON.parse(response.body) rescue JSON::ParserError, Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to fetch messages for #{phone_number}: #{e.message}" - [] - end - - # Fetch an attachment by ID - # GET /v1/attachments/{id} - def fetch_attachment(attachment_id) - url = "#{@base_url}/v1/attachments/#{CGI.escape(attachment_id)}" - response = Faraday.get(url) - return nil unless response.success? - - response.body - rescue Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to fetch attachment #{attachment_id}: #{e.message}" + Rails.logger.error "CdrSignalApi: GET #{path} error: #{e.message}" nil end - # List all groups for a phone number - # GET /v1/groups/{number} - def list_groups(phone_number) - url = "#{@base_url}/v1/groups/#{CGI.escape(phone_number)}" - response = Faraday.get(url, nil, { 'Accept' => 'application/json' }) - return [] unless response.success? - - JSON.parse(response.body) - rescue JSON::ParserError, Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to list groups for #{phone_number}: #{e.message}" - [] - end - - # Check if a phone number is registered with signal-cli - # GET /v1/about - def check_number(phone_number) - # Verify we can connect to signal-cli-rest-api - url = "#{@base_url}/v1/about" - response = Faraday.get(url, nil, { 'Accept' => 'application/json' }) - return false unless response.success? - - # Try to list groups for this number to verify it's registered - groups_url = "#{@base_url}/v1/groups/#{CGI.escape(phone_number)}" - groups_response = Faraday.get(groups_url, nil, { 'Accept' => 'application/json' }) - groups_response.success? - rescue Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to check number #{phone_number}: #{e.message}" - false - end - - # Send a message via Signal CLI - # POST /v2/send - def send_message(from_number, recipients, message, options = {}) - url = "#{@base_url}/v2/send" - - data = { - number: from_number, - recipients: Array(recipients), - message: message - } - - # Add base64 attachments if provided - if options[:attachments].present? - data[:base64Attachments] = options[:attachments].map { |a| a[:data] } - end - - # Add quote parameters if provided - if options[:quote_timestamp] && options[:quote_author] && options[:quote_message] - data[:quoteTimestamp] = options[:quote_timestamp] - data[:quoteAuthor] = options[:quote_author] - data[:quoteMessage] = options[:quote_message] - end - + def post(path, data) + url = "#{@base_url}#{path}" response = Faraday.post(url, data.to_json, { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }) unless response.success? - Rails.logger.error "CdrSignalApi: Failed to send message: #{response.status} #{response.body}" - raise "Failed to send Signal message: #{response.status}" + Rails.logger.error "CdrSignalApi: POST #{path} failed: #{response.status} #{response.body}" + raise "Bridge-signal request failed: #{response.status}" end JSON.parse(response.body) + rescue JSON::ParserError => e + # POST may return empty body on success + nil rescue Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to send message: #{e.message}" - raise "Failed to send Signal message: #{e.message}" + Rails.logger.error "CdrSignalApi: POST #{path} error: #{e.message}" + raise "Bridge-signal request failed: #{e.message}" end - # Create a new Signal group - # POST /v1/groups/{number} - def create_group(phone_number, name:, members:, description: nil) - url = "#{@base_url}/v1/groups/#{CGI.escape(phone_number)}" - - data = { - name: name, - members: Array(members), - description: description - }.compact - - response = Faraday.post(url, data.to_json, { - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - }) - - unless response.success? - Rails.logger.error "CdrSignalApi: Failed to create group: #{response.status} #{response.body}" - raise "Failed to create Signal group: #{response.status}" - end - - JSON.parse(response.body) - rescue Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to create group: #{e.message}" - raise "Failed to create Signal group: #{e.message}" - end - - # Update a Signal group - # PUT /v1/groups/{number}/{groupId} - def update_group(phone_number, group_id, name: nil, description: nil) - url = "#{@base_url}/v1/groups/#{CGI.escape(phone_number)}/#{CGI.escape(group_id)}" - - data = {} - data[:name] = name if name.present? - data[:description] = description if description.present? - + def put(path, data) + url = "#{@base_url}#{path}" response = Faraday.put(url, data.to_json, { 'Content-Type' => 'application/json', 'Accept' => 'application/json' @@ -150,7 +97,7 @@ class CdrSignalApi response.success? rescue Faraday::Error => e - Rails.logger.error "CdrSignalApi: Failed to update group: #{e.message}" + Rails.logger.error "CdrSignalApi: PUT #{path} error: #{e.message}" false end end diff --git a/packages/zammad-addon-link/src/lib/cdr_signal_poller.rb b/packages/zammad-addon-link/src/lib/cdr_signal_poller.rb deleted file mode 100644 index 49d1498..0000000 --- a/packages/zammad-addon-link/src/lib/cdr_signal_poller.rb +++ /dev/null @@ -1,428 +0,0 @@ -# frozen_string_literal: true - -# CdrSignalPoller handles polling Signal CLI for incoming messages and group membership changes. -# This replaces the bridge-worker tasks: -# - fetch-signal-messages.ts -# - check-group-membership.ts -# -# It runs via Zammad schedulers to poll at regular intervals. - -class CdrSignalPoller - class << self - # Fetch messages from all active Signal channels - # This is called by the scheduler every 30 seconds - def fetch_messages - api = CdrSignalApi.new - channels = Channel.where(area: 'Signal::Number', active: true) - - channels.each do |channel| - phone_number = channel.options[:phone_number] - bot_token = channel.options[:bot_token] - next unless phone_number.present? - - Rails.logger.debug { "CdrSignalPoller: Fetching messages for #{phone_number}" } - - messages = api.fetch_messages(phone_number) - process_messages(channel, messages, api) - rescue StandardError => e - Rails.logger.error "CdrSignalPoller: Error fetching messages for #{phone_number}: #{e.message}" - end - end - - # Check group membership for all active Signal channels - # This is called by the scheduler every 2 minutes - def check_group_membership - api = CdrSignalApi.new - channels = Channel.where(area: 'Signal::Number', active: true) - - channels.each do |channel| - phone_number = channel.options[:phone_number] - next unless phone_number.present? - - Rails.logger.debug { "CdrSignalPoller: Checking groups for #{phone_number}" } - - groups = api.list_groups(phone_number) - process_group_membership(channel, groups) - rescue StandardError => e - Rails.logger.error "CdrSignalPoller: Error checking groups for #{phone_number}: #{e.message}" - end - end - - private - - def process_messages(channel, messages, api) - messages.each do |msg| - envelope = msg['envelope'] - next unless envelope - - source = envelope['source'] - source_uuid = envelope['sourceUuid'] - data_message = envelope['dataMessage'] - sync_message = envelope['syncMessage'] - - # Log envelope types for debugging - Rails.logger.debug do - "CdrSignalPoller: Received envelope - source: #{source}, uuid: #{source_uuid}, " \ - "dataMessage: #{data_message.present?}, syncMessage: #{sync_message.present?}" - end - - # Handle group join events from groupInfo - if data_message && data_message['groupInfo'] - handle_group_info_event(channel, data_message['groupInfo'], source) - end - - # Process data messages with content - next unless data_message - - process_data_message(channel, data_message, source, source_uuid, api) - end - end - - def handle_group_info_event(channel, group_info, source) - type = group_info['type'] - return unless %w[JOIN JOINED].include?(type) - - group_id_raw = group_info['groupId'] - return unless group_id_raw - - group_id = "group.#{Base64.strict_encode64(group_id_raw.pack('c*'))}" - - Rails.logger.info "CdrSignalPoller: User #{source} joined group #{group_id}" - notify_group_member_joined(channel, group_id, source) - end - - def process_data_message(channel, data_message, source, source_uuid, api) - # Determine if this is a group message - is_group = data_message['groupV2'].present? || - data_message['groupContext'].present? || - data_message['groupInfo'].present? - - # Get group ID if applicable - group_id_raw = data_message.dig('groupV2', 'id') || - data_message.dig('groupContext', 'id') || - data_message.dig('groupInfo', 'groupId') - - phone_number = channel.options[:phone_number] - to_recipient = if group_id_raw - "group.#{Base64.strict_encode64(group_id_raw.is_a?(Array) ? group_id_raw.pack('c*') : group_id_raw)}" - else - phone_number - end - - # Skip if message is from self - return if source == phone_number - - message_text = data_message['message'] - raw_timestamp = data_message['timestamp'] - attachments = data_message['attachments'] - - # Generate unique message ID - message_id = "#{source_uuid}-#{raw_timestamp}" - - # Check for duplicate - return if Ticket::Article.exists?(message_id: "cdr_signal.#{message_id}") - - # Fetch and encode attachments - attachment_data = fetch_attachments(attachments, api) - - # Process the message through the webhook handler - process_incoming_message( - channel: channel, - to: to_recipient, - from: source, - user_id: source_uuid, - message_id: message_id, - message: message_text, - sent_at: raw_timestamp ? Time.at(raw_timestamp / 1000).iso8601 : Time.current.iso8601, - attachments: attachment_data, - is_group: is_group - ) - end - - def fetch_attachments(attachments, api) - return [] unless attachments.is_a?(Array) - - attachments.filter_map do |att| - id = att['id'] - content_type = att['contentType'] - filename = att['filename'] - - blob = api.fetch_attachment(id) - next unless blob - - # Generate filename if not provided - default_filename = filename - unless default_filename - extension = content_type&.split('/')&.last || 'bin' - default_filename = id.include?('.') ? id : "#{id}.#{extension}" - end - - { - filename: default_filename, - mime_type: content_type, - data: Base64.strict_encode64(blob) - } - end - end - - def process_incoming_message(channel:, to:, from:, user_id:, message_id:, message:, sent_at:, attachments:, is_group:) - # Find or create customer - customer = find_or_create_customer(from, user_id) - return unless customer - - # Set current user context - UserInfo.current_user_id = customer.id - - # Find or create ticket - ticket = find_or_create_ticket( - channel: channel, - customer: customer, - to: to, - from: from, - user_id: user_id, - is_group: is_group, - sent_at: sent_at - ) - - # Create article - create_article( - ticket: ticket, - from: from, - to: to, - user_id: user_id, - message_id: message_id, - message: message || 'No text content', - sent_at: sent_at, - attachments: attachments - ) - - Rails.logger.info "CdrSignalPoller: Created article for ticket ##{ticket.number} from #{from}" - end - - def find_or_create_customer(phone_number, user_id) - # Try phone number first - customer = User.find_by(phone: phone_number) if phone_number.present? - customer ||= User.find_by(mobile: phone_number) if phone_number.present? - - # Try user ID - if customer.nil? && user_id.present? - customer = User.find_by(signal_uid: user_id) - customer ||= User.find_by(phone: user_id) - customer ||= User.find_by(mobile: user_id) - end - - # Create new customer if not found - unless customer - role_ids = Role.signup_role_ids - customer = User.create!( - firstname: '', - lastname: '', - email: '', - password: '', - phone: phone_number.presence || user_id, - signal_uid: user_id, - note: 'CDR Signal', - active: true, - role_ids: role_ids, - updated_by_id: 1, - created_by_id: 1 - ) - end - - # Update signal_uid if needed - customer.update!(signal_uid: user_id) if user_id.present? && customer.signal_uid.blank? - - # Update phone if customer only has user_id - customer.update!(phone: phone_number) if phone_number.present? && customer.phone == user_id - - customer - end - - def find_or_create_ticket(channel:, customer:, to:, from:, user_id:, is_group:, sent_at:) - state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) - sender_display = from.presence || user_id - - if is_group - # Find ticket by group ID - ticket = Ticket.where.not(state_id: state_ids) - .where('preferences LIKE ?', "%channel_id: #{channel.id}%") - .where('preferences LIKE ?', "%chat_id: #{to}%") - .order(updated_at: :desc) - .first - else - # Find ticket by customer - ticket = Ticket.where(customer_id: customer.id) - .where.not(state_id: state_ids) - .order(:updated_at) - .first - end - - if ticket - ticket.title = "Message from #{sender_display} at #{sent_at}" if ticket.title == '-' - new_state = Ticket::State.find_by(default_create: true) - ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id - else - chat_id = is_group ? to : (user_id.presence || from) - - cdr_signal_prefs = { - bot_token: channel.options[:bot_token], - chat_id: chat_id, - user_id: user_id - } - cdr_signal_prefs[:original_recipient] = from if is_group - - ticket = Ticket.new( - group_id: channel.group_id, - title: "Message from #{sender_display} at #{sent_at}", - customer_id: customer.id, - preferences: { - channel_id: channel.id, - cdr_signal: cdr_signal_prefs - } - ) - end - - ticket.save! - ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id) - ticket - end - - def create_article(ticket:, from:, to:, user_id:, message_id:, message:, sent_at:, attachments:) - sender_display = from.presence || user_id - - article_params = { - ticket_id: ticket.id, - type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id, - sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, - from: sender_display, - to: to, - subject: "Message from #{sender_display} at #{sent_at}", - body: message, - content_type: 'text/plain', - message_id: "cdr_signal.#{message_id}", - internal: false, - preferences: { - cdr_signal: { - timestamp: sent_at, - message_id: message_id, - from: from, - user_id: user_id - } - } - } - - # Add primary attachment if present - if attachments.present? && attachments.first - primary = attachments.first - article_params[:attachments] = [{ - 'filename' => primary[:filename], - filename: primary[:filename], - data: primary[:data], - 'data' => primary[:data], - 'mime-type' => primary[:mime_type] - }] - end - - ticket.with_lock do - article = Ticket::Article.create!(article_params) - - # Create additional articles for extra attachments - ((attachments || [])[1..] || []).each_with_index do |att, index| - Ticket::Article.create!( - article_params.merge( - message_id: "cdr_signal.#{message_id}-#{index + 1}", - subject: att[:filename], - body: att[:filename], - attachments: [{ - 'filename' => att[:filename], - filename: att[:filename], - data: att[:data], - 'data' => att[:data], - 'mime-type' => att[:mime_type] - }] - ) - ) - end - - article - end - end - - def process_group_membership(channel, groups) - groups.each do |group| - group_id = group['id'] - internal_id = group['internalId'] - members = group['members'] || [] - next unless group_id && internal_id - - Rails.logger.debug do - "CdrSignalPoller: Group #{group['name']} - #{members.length} members, " \ - "#{(group['pendingInvites'] || []).length} pending" - end - - members.each do |member_phone| - notify_group_member_joined(channel, group_id, member_phone) - end - end - end - - def notify_group_member_joined(channel, group_id, member_phone) - # Find ticket with this group_id - state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) - - ticket = Ticket.where.not(state_id: state_ids) - .where('preferences LIKE ?', "%chat_id: #{group_id}%") - .order(updated_at: :desc) - .first - - return unless ticket - - # Idempotency check - return if ticket.preferences.dig('cdr_signal', 'group_joined') == true - - # Update group_joined flag - ticket.preferences[:cdr_signal] ||= {} - ticket.preferences[:cdr_signal][:group_joined] = true - ticket.preferences[:cdr_signal][:group_joined_at] = Time.current.iso8601 - ticket.preferences[:cdr_signal][:group_joined_by] = member_phone - ticket.save! - - Rails.logger.info "CdrSignalPoller: Member #{member_phone} joined group #{group_id} for ticket ##{ticket.number}" - - # Add resolution note if there were pending notifications - add_group_join_resolution_note(ticket) - end - - def add_group_join_resolution_note(ticket) - # Check if any articles had a group_not_joined notification - articles_with_pending = Ticket::Article.where(ticket_id: ticket.id) - .where('preferences LIKE ?', '%group_not_joined_note_added: true%') - - return unless articles_with_pending.exists? - - # Check if resolution note already exists - resolution_exists = Ticket::Article.where(ticket_id: ticket.id) - .where('preferences LIKE ?', '%group_joined_resolution: true%') - .exists? - - return if resolution_exists - - Ticket::Article.create!( - ticket_id: ticket.id, - content_type: 'text/plain', - body: 'Recipient has now joined the Signal group. Pending messages will be delivered shortly.', - internal: true, - sender: Ticket::Article::Sender.find_by(name: 'System'), - type: Ticket::Article::Type.find_by(name: 'note'), - preferences: { - delivery_message: true, - group_joined_resolution: true - }, - updated_by_id: 1, - created_by_id: 1 - ) - - Rails.logger.info "CdrSignalPoller: Added resolution note for ticket ##{ticket.number}" - end - end -end diff --git a/packages/zammad-addon-link/src/lib/signal_notification_sender.rb b/packages/zammad-addon-link/src/lib/signal_notification_sender.rb index 35aaa1e..1980630 100644 --- a/packages/zammad-addon-link/src/lib/signal_notification_sender.rb +++ b/packages/zammad-addon-link/src/lib/signal_notification_sender.rb @@ -24,15 +24,11 @@ class SignalNotificationSender return if recipient.blank? return if message.blank? - # Get the phone number from channel options - phone_number = channel.options['phone_number'] || channel.options[:phone_number] || - channel.options.dig('bot', 'number') || channel.options.dig(:bot, :number) + bot_id = channel.options['bot_token'] || channel.options[:bot_token] + return if bot_id.blank? - return if phone_number.blank? - - # Use direct Signal CLI API api = CdrSignalApi.new - api.send_message(phone_number, [recipient], message) + api.send_message(bot_id, recipient, message) end private diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 563c9f6..0ebcf68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,36 +18,122 @@ importers: specifier: latest version: 5.9.3 + apps/bridge-deltachat: + dependencies: + '@deltachat/jsonrpc-client': + specifier: ^1.151.1 + version: 1.160.0(ws@8.19.0) + '@deltachat/stdio-rpc-server': + specifier: ^1.151.1 + version: 1.160.0(@deltachat/jsonrpc-client@1.160.0(ws@8.19.0)) + '@hono/node-server': + specifier: ^1.13.8 + version: 1.19.9(hono@4.11.9) + '@link-stack/logger': + specifier: workspace:* + version: link:../../packages/logger + hono: + specifier: ^4.7.4 + version: 4.11.9 + devDependencies: + '@link-stack/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@link-stack/prettier-config': + specifier: workspace:* + version: link:../../packages/prettier-config + '@link-stack/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/node': + specifier: '*' + version: 24.10.9 + dotenv-cli: + specifier: ^10.0.0 + version: 10.0.0 + eslint: + specifier: ^9.23.0 + version: 9.39.2 + prettier: + specifier: ^3.5.3 + version: 3.8.1 + tsx: + specifier: ^4.20.6 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/bridge-signal: + dependencies: + '@hono/node-server': + specifier: ^1.13.8 + version: 1.19.9(hono@4.11.9) + '@link-stack/logger': + specifier: workspace:* + version: link:../../packages/logger + hono: + specifier: ^4.7.4 + version: 4.11.9 + devDependencies: + '@link-stack/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@link-stack/prettier-config': + specifier: workspace:* + version: link:../../packages/prettier-config + '@link-stack/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/node': + specifier: '*' + version: 24.10.9 + dotenv-cli: + specifier: ^10.0.0 + version: 10.0.0 + eslint: + specifier: ^9.23.0 + version: 9.39.2 + prettier: + specifier: ^3.5.3 + version: 3.8.1 + tsx: + specifier: ^4.20.6 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + apps/bridge-whatsapp: dependencies: '@adiwajshing/keyed-db': specifier: 0.2.4 version: 0.2.4 - '@hapi/hapi': - specifier: ^21.4.3 - version: 21.4.4 - '@hapipal/schmervice': - specifier: ^3.0.0 - version: 3.0.0(@hapi/hapi@21.4.4) - '@hapipal/toys': - specifier: ^4.0.0 - version: 4.0.0(@hapi/hapi@21.4.4) + '@hono/node-server': + specifier: ^1.13.8 + version: 1.19.9(hono@4.11.9) + '@link-stack/logger': + specifier: workspace:* + version: link:../../packages/logger '@whiskeysockets/baileys': specifier: 6.7.21 version: 6.7.21(link-preview-js@3.2.0)(sharp@0.34.5) - hapi-pino: - specifier: ^13.0.0 - version: 13.0.0 + hono: + specifier: ^4.7.4 + version: 4.11.9 link-preview-js: specifier: ^3.1.0 version: 3.2.0 - pino: - specifier: ^9.6.0 - version: 9.14.0 - pino-pretty: - specifier: ^13.0.0 - version: 13.1.3 devDependencies: + '@link-stack/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@link-stack/prettier-config': + specifier: workspace:* + version: link:../../packages/prettier-config + '@link-stack/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config '@types/long': specifier: ^5 version: 5.0.0 @@ -57,6 +143,12 @@ importers: dotenv-cli: specifier: ^10.0.0 version: 10.0.0 + eslint: + specifier: ^9.23.0 + version: 9.39.2 + prettier: + specifier: ^3.5.3 + version: 3.8.1 tsx: specifier: ^4.20.6 version: 4.21.0 @@ -64,6 +156,56 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/eslint-config: + dependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.0 + version: 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.0 + version: 8.55.0(eslint@9.39.2)(typescript@5.9.3) + eslint: + specifier: ^9 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.1.1 + version: 10.1.8(eslint@9.39.2) + eslint-plugin-import-x: + specifier: ^4.12.2 + version: 4.16.1(@typescript-eslint/utils@8.55.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2) + eslint-plugin-unicorn: + specifier: ^58.0.0 + version: 58.0.0(eslint@9.39.2) + typescript: + specifier: ^5 + version: 5.9.3 + + packages/logger: + dependencies: + pino: + specifier: ^9.6.0 + version: 9.14.0 + pino-pretty: + specifier: ^13.0.0 + version: 13.1.3 + devDependencies: + '@link-stack/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/node': + specifier: '*' + version: 24.10.9 + tsup: + specifier: ^8.5.0 + version: 8.5.1(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/prettier-config: {} + + packages/typescript-config: {} + packages/zammad-addon-link: devDependencies: '@types/node': @@ -94,7 +236,7 @@ importers: specifier: ^9.0.0 version: 9.2.1 express: - specifier: ^5.0.1 + specifier: ^5.2.1 version: 5.2.1 jest: specifier: ^29.7.0 @@ -292,9 +434,76 @@ packages: '@cacheable/utils@2.3.3': resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==} + '@deltachat/jsonrpc-client@1.160.0': + resolution: {integrity: sha512-8AspajerKQBT0p+hbCUXp3gHG3uepDa2CVTTUtxBgery7oi57/ok1pbdV3sfsimy4XpxBcjIhvAAZZBy9MgCDw==} + + '@deltachat/stdio-rpc-server-android-arm64@1.160.0': + resolution: {integrity: sha512-lfFHz1YMCmdgSpnCfH/wiCEaYBDCHlRwU7brJoE37CWMiofk4piAYgG2ifgN9nBFpgix4Q7+sNC8yRTzdXMk3Q==} + cpu: [arm64] + os: [android] + + '@deltachat/stdio-rpc-server-android-arm@1.160.0': + resolution: {integrity: sha512-S+33dGxnQllUQE6aw7TEHRrfTGCDgBEWFNyKXJ30TkSM5gPEq5EqV3qu0BP3TXiRUlhBJ7evvb1QgPyQEZmnxg==} + cpu: [arm] + os: [android] + + '@deltachat/stdio-rpc-server-darwin-arm64@1.160.0': + resolution: {integrity: sha512-QB9AzwTiW+/lEtyeDSyMVysfea/X+8Oyfzmshb0OY183lFm6H8+2Mh5KgRUKhGaVCE4E2RGlqK40m5+E4tgGtA==} + cpu: [arm64] + os: [darwin] + + '@deltachat/stdio-rpc-server-darwin-x64@1.160.0': + resolution: {integrity: sha512-CeBgMYUIf9x5oP/HrlCJxSiy+/Iw0Be5v0WU3xM/5cYvlryQFFKFSePZjzJcmJ3VE0AsAtagVqf5C4MSeej9Bw==} + cpu: [x64] + os: [darwin] + + '@deltachat/stdio-rpc-server-linux-arm64@1.160.0': + resolution: {integrity: sha512-O9ovsBD9K6LhjiPw7rkmLakdyD2OKadQjH44FEz//saco/A4DRm3Gz5tVwEc43MEo0W+D5ieoPujGFzkJ9m/Ug==} + cpu: [arm64] + os: [linux] + + '@deltachat/stdio-rpc-server-linux-arm@1.160.0': + resolution: {integrity: sha512-aSlYfv+uszW7DvG7Xye9XO7tBzTPOS0HJGdlUut4qeoIY2biO76h3KN6mGh86OCpTNKBhPaudXF8emC/SdJJqw==} + cpu: [arm] + os: [linux] + + '@deltachat/stdio-rpc-server-linux-ia32@1.160.0': + resolution: {integrity: sha512-wbQaw2pwoUR8qgsHPUdS5BdaXcZ1cp+UjdYLfNAw307hzZP14etG8qmutmvmVD9c3zVUmZH5hlwJjDV5D5ajHA==} + cpu: [ia32] + os: [linux] + + '@deltachat/stdio-rpc-server-linux-x64@1.160.0': + resolution: {integrity: sha512-GLgB3Zesu19lGlJUV1uELTWhjx1EWOOnRe2C43qlH1CtzGMsPTc4vJh9zsoDdFak8lPauEDzCrFzXYJsc1Jkig==} + cpu: [x64] + os: [linux] + + '@deltachat/stdio-rpc-server-win32-ia32@1.160.0': + resolution: {integrity: sha512-iyWvM1gcAYFgnZtAN1wx8dnhSaJKhtdtR4iu6zSwchSqhafMpMIJQx1Rg4g2s4qNCjaPeNykAvW7Sn+JUCAN/g==} + cpu: [ia32] + os: [win32] + + '@deltachat/stdio-rpc-server-win32-x64@1.160.0': + resolution: {integrity: sha512-kaFOI4N+m5I5TNVW6Z4KLg6NtLU99SOAu+LbLxbTmgZQVr+HyQXYiT/fIIG55exaI7nSqxN16ZucLgX9xKvghA==} + cpu: [x64] + os: [win32] + + '@deltachat/stdio-rpc-server@1.160.0': + resolution: {integrity: sha512-EVyrtpB7coo0C9/yTd5AHYvfLatLiyjXDZHRFi2qJyLld2/R74pQ1xw9yyaWQnfV7kONMQKCK/IkSJsXtMnNSw==} + peerDependencies: + '@deltachat/jsonrpc-client': '*' + + '@deltachat/tiny-emitter@3.0.0': + resolution: {integrity: sha512-iapvwGCEWFjXtObQ/vNzXS0+M9hrZrY2tMdV7SaX8kHXKVm3oIuJUVmUj5jhABTL7GstO6uKzV+36FUZVGOqCA==} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -451,117 +660,79 @@ packages: cpu: [x64] os: [win32] - '@hapi/accept@6.0.3': - resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@hapi/ammo@6.0.1': - resolution: {integrity: sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@hapi/b64@6.0.1': - resolution: {integrity: sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hapi/boom@10.0.1': - resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@hapi/boom@9.1.4': resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==} - '@hapi/bounce@3.0.2': - resolution: {integrity: sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==} - - '@hapi/bourne@3.0.0': - resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} - - '@hapi/call@9.0.1': - resolution: {integrity: sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==} - - '@hapi/catbox-memory@6.0.2': - resolution: {integrity: sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==} - - '@hapi/catbox@12.1.1': - resolution: {integrity: sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==} - - '@hapi/content@6.0.0': - resolution: {integrity: sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==} - - '@hapi/cryptiles@6.0.3': - resolution: {integrity: sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==} - engines: {node: '>=14.0.0'} - - '@hapi/file@3.0.0': - resolution: {integrity: sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==} - - '@hapi/hapi@21.4.4': - resolution: {integrity: sha512-vI6JPLR99WZDKI1nriD0qXDPp8sKFkZfNVGrDDZafDQ8jU+3ERMwS0vPac5aGae6yyyoGZGOBiYExw4N8ScSTQ==} - engines: {node: '>=14.15.0'} - - '@hapi/heavy@8.0.1': - resolution: {integrity: sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==} - - '@hapi/hoek@11.0.7': - resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} - '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - '@hapi/iron@7.0.1': - resolution: {integrity: sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==} - - '@hapi/mimos@7.0.1': - resolution: {integrity: sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==} - - '@hapi/nigel@5.0.1': - resolution: {integrity: sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==} - engines: {node: '>=14.0.0'} - - '@hapi/pez@6.1.0': - resolution: {integrity: sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==} - - '@hapi/podium@5.0.2': - resolution: {integrity: sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==} - - '@hapi/shot@6.0.2': - resolution: {integrity: sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==} - - '@hapi/somever@4.1.1': - resolution: {integrity: sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==} - - '@hapi/statehood@8.2.1': - resolution: {integrity: sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==} - - '@hapi/subtext@8.1.1': - resolution: {integrity: sha512-ex1Y2s/KuJktS8Ww0k6XJ5ysSKrzNym4i5pDVuCwlSgHHviHUsT1JNzE6FYhNU9TTHSNdyfue/t2m89bpkX9Jw==} - - '@hapi/teamwork@6.0.1': - resolution: {integrity: sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==} - engines: {node: '>=14.0.0'} - - '@hapi/topo@6.0.2': - resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} - - '@hapi/validate@2.0.1': - resolution: {integrity: sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==} - - '@hapi/vise@5.0.1': - resolution: {integrity: sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==} - - '@hapi/wreck@18.1.0': - resolution: {integrity: sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==} - - '@hapipal/schmervice@3.0.0': - resolution: {integrity: sha512-1v7GY6BPHP9vLrUkQ1mi8qnzrAKKLy0J0rRznhogkILcvuwZXMHuMuSBrmmyEmHh4fPvL+WxPgeINjrL0TRtxQ==} - engines: {node: '>=16'} + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} peerDependencies: - '@hapi/hapi': '>=20 <22' + hono: ^4 - '@hapipal/toys@4.0.0': - resolution: {integrity: sha512-X00yhzgirMQAmlPIino/ZMpD3kfniQK3sU2qMLKT+kTdbUF+ThIP2s1sd+fUJMoP3CxBpFK5/t6csI66rlCCnw==} - engines: {node: '>=16'} - peerDependencies: - '@hapi/hapi': '>=20 <22' - peerDependenciesMeta: - '@hapi/hapi': - optional: true + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} @@ -811,6 +982,9 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -844,6 +1018,131 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -860,6 +1159,9 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -878,6 +1180,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} @@ -902,6 +1207,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} @@ -918,6 +1226,9 @@ packages: '@types/node@24.10.9': resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -933,12 +1244,169 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@typescript-eslint/eslint-plugin@8.55.0': + resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.55.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.55.0': + resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.55.0': + resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.55.0': + resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.55.0': + resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.55.0': + resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.55.0': + resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.55.0': + resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.55.0': + resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.55.0': + resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + '@whiskeysockets/baileys@6.7.21': resolution: {integrity: sha512-xx9OHd6jlPiu5yZVuUdwEgFNAOXiEG8sULHxC6XfzNwssnwxnA9Lp44pR05H621GQcKyCfsH33TGy+Na6ygX4w==} engines: {node: '>=20.0.0'} @@ -959,13 +1427,23 @@ packages: resolution: {tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67} version: 2.0.1 - abstract-logging@2.0.1: - resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -990,6 +1468,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -997,6 +1478,9 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} @@ -1052,6 +1536,9 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1071,10 +1558,24 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + builtin-modules@5.0.0: + resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + engines: {node: '>=18.20'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacheable@2.3.2: resolution: {integrity: sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==} @@ -1116,13 +1617,25 @@ packages: resolution: {integrity: sha512-bQwNaDIBKID5ts/DsdhxrjqFXYfLw4ste+wMKqWA8DyKcS4qwsPP4Bk8ZNaTJjvpiX/qW3BT4sU7d6Bh5i+dag==} engines: {node: '>= 6'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1148,6 +1661,14 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-parser@1.4.5: + resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} + engines: {node: '>= 12.0.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1156,6 +1677,13 @@ packages: engines: {node: '>=18'} hasBin: true + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.1: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} @@ -1175,6 +1703,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-js-compat@3.48.0: + resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1214,6 +1745,9 @@ packages: babel-plugin-macros: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -1344,15 +1878,99 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-plugin-import-x@4.16.1: + resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-unicorn@58.0.0: + resolution: {integrity: sha512-fc3iaxCm9chBWOHPVjn+Czb/wHS0D2Mko7wkOdobqo9R2bbFObc4LyZaLTNy0mhZOP84nKkLhTUQxlLOZ7EjKw==} + engines: {node: ^18.20.0 || ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.22.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -1376,15 +1994,34 @@ packages: fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + file-type@21.3.0: resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} @@ -1397,10 +2034,28 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -1464,6 +2119,10 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} @@ -1474,6 +2133,14 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1486,10 +2153,6 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - hapi-pino@13.0.0: - resolution: {integrity: sha512-bKHmFqPEEWzhlGup/qztPlwls0YIcFlFQBsnGTDm824e3It5pL6ij7U23jUopaZ7fABLytC4jHRGCO+NDjFmUg==} - engines: {node: '>=20.0.0'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1513,9 +2176,17 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hono@4.11.9: + resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} + engines: {node: '>=16.9.0'} + hookified@1.15.0: resolution: {integrity: sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw==} + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1537,6 +2208,18 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -1546,6 +2229,14 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -1560,10 +2251,18 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1572,6 +2271,10 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1586,6 +2289,16 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -1754,19 +2467,40 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} @@ -1778,6 +2512,14 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1785,19 +2527,33 @@ packages: resolution: {integrity: sha512-FvrLltjOPGbTzt+RugbzM7g8XuUNLPO2U/INSLczrYdAA32E7nZVUrVL1gr61DGOArGJA2QkPGMEvNMLLsXREA==} engines: {node: '>=18'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.5: resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} engines: {node: 20 || >=22} @@ -1805,6 +2561,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1861,6 +2620,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1868,6 +2631,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1875,6 +2641,14 @@ packages: resolution: {integrity: sha512-OTlsv/FiCr+c4+fC6t9j/GTC/m1KKc3QtOTYHVEvvGLDLpPdtgf32pB7JXJ/Xi8qdIxwwh2PR8J/1t0QL1BxWQ==} engines: {node: '>=18'} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1891,6 +2665,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1902,6 +2680,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -1921,6 +2703,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -1933,6 +2719,10 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -1940,10 +2730,18 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -1976,6 +2774,9 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1983,6 +2784,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -2008,6 +2813,40 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2040,6 +2879,10 @@ packages: punycode@1.3.2: resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -2070,10 +2913,30 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + read-package-up@11.0.0: + resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} + engines: {node: '>=18'} + + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2082,6 +2945,10 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2098,6 +2965,11 @@ packages: engines: {node: '>= 0.4'} hasBin: true + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -2191,6 +3063,22 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -2198,6 +3086,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -2234,6 +3126,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2246,6 +3142,11 @@ packages: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2262,9 +3163,23 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -2284,6 +3199,15 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.6: resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -2314,6 +3238,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -2353,6 +3296,10 @@ packages: resolution: {integrity: sha512-PO9AvJLEsNLO+EYhF4zB+v10hOjsJe5kJW+S6tTbRv+TW7gf1Qer4mfjP9h3/y9h8ZiPvOrenxnEgDtFgaM5zw==} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -2369,11 +3316,19 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -2389,16 +3344,26 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url@0.11.0: resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} @@ -2406,6 +3371,9 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2421,6 +3389,10 @@ packages: win-guid@0.2.0: resolution: {integrity: sha512-iekGhWzFQSunvE87ndXxoa6UgyQbkL4MmbYTFhQnk94pVsAW89mWnvW1zI3JMnypUm8jykJaITudspoR8NrRBQ==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -2466,6 +3438,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yerpc@0.6.4: + resolution: {integrity: sha512-f/yBGREg5sR9aa6QG0thon6uL8jKJhQuRO/SwGsnOIIe45wFQrMYBV+A5cCNEgqzSHsz+Jl2F7sEO6h7CJfr5w==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2683,11 +3658,79 @@ snapshots: hashery: 1.4.0 keyv: 5.6.0 + '@deltachat/jsonrpc-client@1.160.0(ws@8.19.0)': + dependencies: + '@deltachat/tiny-emitter': 3.0.0 + isomorphic-ws: 5.0.0(ws@8.19.0) + yerpc: 0.6.4 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - ws + + '@deltachat/stdio-rpc-server-android-arm64@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-android-arm@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-darwin-arm64@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-darwin-x64@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-linux-arm64@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-linux-arm@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-linux-ia32@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-linux-x64@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-win32-ia32@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server-win32-x64@1.160.0': + optional: true + + '@deltachat/stdio-rpc-server@1.160.0(@deltachat/jsonrpc-client@1.160.0(ws@8.19.0))': + dependencies: + '@deltachat/jsonrpc-client': 1.160.0(ws@8.19.0) + optionalDependencies: + '@deltachat/stdio-rpc-server-android-arm': 1.160.0 + '@deltachat/stdio-rpc-server-android-arm64': 1.160.0 + '@deltachat/stdio-rpc-server-darwin-arm64': 1.160.0 + '@deltachat/stdio-rpc-server-darwin-x64': 1.160.0 + '@deltachat/stdio-rpc-server-linux-arm': 1.160.0 + '@deltachat/stdio-rpc-server-linux-arm64': 1.160.0 + '@deltachat/stdio-rpc-server-linux-ia32': 1.160.0 + '@deltachat/stdio-rpc-server-linux-x64': 1.160.0 + '@deltachat/stdio-rpc-server-win32-ia32': 1.160.0 + '@deltachat/stdio-rpc-server-win32-x64': 1.160.0 + + '@deltachat/tiny-emitter@3.0.0': {} + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.2': optional: true @@ -2766,184 +3809,81 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@hapi/accept@6.0.3': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': dependencies: - '@hapi/boom': 10.0.1 - '@hapi/hoek': 11.0.7 + eslint: 9.39.2 + eslint-visitor-keys: 3.4.3 - '@hapi/ammo@6.0.1': - dependencies: - '@hapi/hoek': 11.0.7 + '@eslint-community/regexpp@4.12.2': {} - '@hapi/b64@6.0.1': + '@eslint/config-array@0.21.1': dependencies: - '@hapi/hoek': 11.0.7 + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color - '@hapi/boom@10.0.1': + '@eslint/config-helpers@0.4.2': dependencies: - '@hapi/hoek': 11.0.7 + '@eslint/core': 0.17.0 + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.2.8': + dependencies: + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 '@hapi/boom@9.1.4': dependencies: '@hapi/hoek': 9.3.0 - '@hapi/bounce@3.0.2': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/hoek': 11.0.7 - - '@hapi/bourne@3.0.0': {} - - '@hapi/call@9.0.1': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/hoek': 11.0.7 - - '@hapi/catbox-memory@6.0.2': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/hoek': 11.0.7 - - '@hapi/catbox@12.1.1': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/hoek': 11.0.7 - '@hapi/podium': 5.0.2 - '@hapi/validate': 2.0.1 - - '@hapi/content@6.0.0': - dependencies: - '@hapi/boom': 10.0.1 - - '@hapi/cryptiles@6.0.3': - dependencies: - '@hapi/boom': 10.0.1 - - '@hapi/file@3.0.0': {} - - '@hapi/hapi@21.4.4': - dependencies: - '@hapi/accept': 6.0.3 - '@hapi/ammo': 6.0.1 - '@hapi/boom': 10.0.1 - '@hapi/bounce': 3.0.2 - '@hapi/call': 9.0.1 - '@hapi/catbox': 12.1.1 - '@hapi/catbox-memory': 6.0.2 - '@hapi/heavy': 8.0.1 - '@hapi/hoek': 11.0.7 - '@hapi/mimos': 7.0.1 - '@hapi/podium': 5.0.2 - '@hapi/shot': 6.0.2 - '@hapi/somever': 4.1.1 - '@hapi/statehood': 8.2.1 - '@hapi/subtext': 8.1.1 - '@hapi/teamwork': 6.0.1 - '@hapi/topo': 6.0.2 - '@hapi/validate': 2.0.1 - - '@hapi/heavy@8.0.1': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/hoek': 11.0.7 - '@hapi/validate': 2.0.1 - - '@hapi/hoek@11.0.7': {} - '@hapi/hoek@9.3.0': {} - '@hapi/iron@7.0.1': + '@hono/node-server@1.19.9(hono@4.11.9)': dependencies: - '@hapi/b64': 6.0.1 - '@hapi/boom': 10.0.1 - '@hapi/bourne': 3.0.0 - '@hapi/cryptiles': 6.0.3 - '@hapi/hoek': 11.0.7 + hono: 4.11.9 - '@hapi/mimos@7.0.1': + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': dependencies: - '@hapi/hoek': 11.0.7 - mime-db: 1.54.0 + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 - '@hapi/nigel@5.0.1': - dependencies: - '@hapi/hoek': 11.0.7 - '@hapi/vise': 5.0.1 + '@humanwhocodes/module-importer@1.0.1': {} - '@hapi/pez@6.1.0': - dependencies: - '@hapi/b64': 6.0.1 - '@hapi/boom': 10.0.1 - '@hapi/content': 6.0.0 - '@hapi/hoek': 11.0.7 - '@hapi/nigel': 5.0.1 - - '@hapi/podium@5.0.2': - dependencies: - '@hapi/hoek': 11.0.7 - '@hapi/teamwork': 6.0.1 - '@hapi/validate': 2.0.1 - - '@hapi/shot@6.0.2': - dependencies: - '@hapi/hoek': 11.0.7 - '@hapi/validate': 2.0.1 - - '@hapi/somever@4.1.1': - dependencies: - '@hapi/bounce': 3.0.2 - '@hapi/hoek': 11.0.7 - - '@hapi/statehood@8.2.1': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/bounce': 3.0.2 - '@hapi/bourne': 3.0.0 - '@hapi/cryptiles': 6.0.3 - '@hapi/hoek': 11.0.7 - '@hapi/iron': 7.0.1 - '@hapi/validate': 2.0.1 - - '@hapi/subtext@8.1.1': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/bourne': 3.0.0 - '@hapi/content': 6.0.0 - '@hapi/file': 3.0.0 - '@hapi/hoek': 11.0.7 - '@hapi/pez': 6.1.0 - '@hapi/wreck': 18.1.0 - - '@hapi/teamwork@6.0.1': {} - - '@hapi/topo@6.0.2': - dependencies: - '@hapi/hoek': 11.0.7 - - '@hapi/validate@2.0.1': - dependencies: - '@hapi/hoek': 11.0.7 - '@hapi/topo': 6.0.2 - - '@hapi/vise@5.0.1': - dependencies: - '@hapi/hoek': 11.0.7 - - '@hapi/wreck@18.1.0': - dependencies: - '@hapi/boom': 10.0.1 - '@hapi/bourne': 3.0.0 - '@hapi/hoek': 11.0.7 - - '@hapipal/schmervice@3.0.0(@hapi/hapi@21.4.4)': - dependencies: - '@hapi/hapi': 21.4.4 - - '@hapipal/toys@4.0.0(@hapi/hapi@21.4.4)': - dependencies: - '@hapi/hoek': 11.0.7 - optionalDependencies: - '@hapi/hapi': 21.4.4 + '@humanwhocodes/retry@0.4.3': {} '@img/colour@1.0.0': {} @@ -3069,7 +4009,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -3082,14 +4022,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.10) + jest-config: 29.7.0(@types/node@24.10.9) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3114,7 +4054,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -3132,7 +4072,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.19.10 + '@types/node': 24.10.9 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -3154,7 +4094,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.19.10 + '@types/node': 24.10.9 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -3224,7 +4164,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.10 + '@types/node': 24.10.9 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -3255,6 +4195,13 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@pinojs/redact@0.4.0': {} '@protobufjs/aspromise@1.1.2': {} @@ -3280,6 +4227,81 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + '@sinclair/typebox@0.27.10': {} '@sinonjs/commons@3.0.1': @@ -3299,6 +4321,11 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -3323,15 +4350,17 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.19.10 + '@types/node': 24.10.9 '@types/connect@3.4.38': dependencies: - '@types/node': 22.19.10 + '@types/node': 24.10.9 + + '@types/estree@1.0.8': {} '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 22.19.10 + '@types/node': 24.10.9 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -3344,7 +4373,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.19.10 + '@types/node': 24.10.9 '@types/http-errors@2.0.5': {} @@ -3363,6 +4392,8 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/json-schema@7.0.15': {} + '@types/long@4.0.2': {} '@types/long@5.0.0': @@ -3379,27 +4410,183 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/normalize-package-data@2.4.4': {} + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} '@types/send@1.2.1': dependencies: - '@types/node': 22.19.10 + '@types/node': 24.10.9 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.19.10 + '@types/node': 24.10.9 '@types/stack-utils@2.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.10.9 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': dependencies: '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.55.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/type-utils': 8.55.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + eslint: 9.39.2 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.55.0(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + + '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.55.0(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.55.0': {} + + '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.55.0(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + eslint-visitor-keys: 4.2.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + '@whiskeysockets/baileys@6.7.21(link-preview-js@3.2.0)(sharp@0.34.5)': dependencies: '@cacheable/node-cache': 1.7.6 @@ -3425,13 +4612,24 @@ snapshots: curve25519-js: 0.0.4 protobufjs: 6.8.8 - abstract-logging@2.0.1: {} - accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -3448,6 +4646,8 @@ snapshots: ansi-styles@6.2.3: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -3457,6 +4657,8 @@ snapshots: dependencies: sprintf-js: 1.0.3 + argparse@2.0.1: {} + async-mutex@0.5.0: dependencies: tslib: 2.8.1 @@ -3553,6 +4755,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3575,8 +4781,17 @@ snapshots: buffer-from@1.1.2: {} + builtin-modules@5.0.0: {} + + bundle-require@5.1.0(esbuild@0.27.2): + dependencies: + esbuild: 0.27.2 + load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} + cacheable@2.3.2: dependencies: '@cacheable/memory': 2.0.7 @@ -3630,10 +4845,20 @@ snapshots: parse5-htmlparser2-tree-adapter: 7.1.0 tslib: 2.8.1 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@3.9.0: {} + ci-info@4.4.0: {} + cjs-module-lexer@1.4.3: {} + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -3656,6 +4881,10 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@4.1.1: {} + + comment-parser@1.4.5: {} + concat-map@0.0.1: {} concurrently@9.2.1: @@ -3667,6 +4896,10 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + confbox@0.1.8: {} + + consola@3.4.2: {} + content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -3677,6 +4910,10 @@ snapshots: cookie@0.7.2: {} + core-js-compat@3.48.0: + dependencies: + browserslist: 4.28.1 + create-jest@29.7.0(@types/node@22.19.10): dependencies: '@jest/types': 29.6.3 @@ -3718,6 +4955,8 @@ snapshots: dedent@1.7.1: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} delayed-stream@1.0.0: {} @@ -3854,10 +5093,129 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.2): + dependencies: + eslint: 9.39.2 + + eslint-import-context@0.1.9(unrs-resolver@1.11.1): + dependencies: + get-tsconfig: 4.13.0 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.11.1 + + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.55.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2): + dependencies: + '@typescript-eslint/types': 8.55.0 + comment-parser: 1.4.5 + debug: 4.4.3 + eslint: 9.39.2 + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + is-glob: 4.0.3 + minimatch: 10.1.1 + semver: 7.7.3 + stable-hash-x: 0.2.0 + unrs-resolver: 1.11.1 + optionalDependencies: + '@typescript-eslint/utils': 8.55.0(eslint@9.39.2)(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-unicorn@58.0.0(eslint@9.39.2): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint/plugin-kit': 0.2.8 + ci-info: 4.4.0 + clean-regexp: 1.0.0 + core-js-compat: 3.48.0 + eslint: 9.39.2 + esquery: 1.7.0 + globals: 16.5.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + read-package-up: 11.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.12.0 + semver: 7.7.3 + strip-indent: 4.1.1 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + etag@1.8.1: {} execa@5.1.1: @@ -3917,14 +5275,26 @@ snapshots: fast-copy@4.0.2: {} + fast-deep-equal@3.1.3: {} + fast-json-stable-stringify@2.1.0: {} + fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} fb-watchman@2.0.2: dependencies: bser: 2.1.1 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + file-type@21.3.0: dependencies: '@tokenizer/inflate': 0.4.1 @@ -3949,11 +5319,31 @@ snapshots: transitivePeerDependencies: - supports-color + find-up-simple@1.0.1: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.57.1 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + follow-redirects@1.15.11: {} foreground-child@3.3.1: @@ -4010,6 +5400,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@11.1.0: dependencies: foreground-child: 3.3.1 @@ -4028,6 +5422,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + globals@14.0.0: {} + + globals@16.5.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4041,13 +5439,6 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - hapi-pino@13.0.0: - dependencies: - '@hapi/hoek': 11.0.7 - abstract-logging: 2.0.1 - get-caller-file: 2.0.5 - pino: 9.14.0 - has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4066,8 +5457,14 @@ snapshots: help-me@5.0.0: {} + hono@4.11.9: {} + hookified@1.15.0: {} + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + html-escaper@2.0.2: {} htmlparser2@8.0.2: @@ -4093,6 +5490,15 @@ snapshots: ieee754@1.2.1: {} + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -4100,6 +5506,10 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@5.0.0: {} + + index-to-position@1.2.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -4111,14 +5521,24 @@ snapshots: is-arrayish@0.2.1: {} + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.0.0 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-number@7.0.0: {} is-promise@4.0.0: {} @@ -4127,6 +5547,14 @@ snapshots: isexe@2.0.0: {} + isomorphic-ws@4.0.1(ws@8.19.0): + dependencies: + ws: 8.19.0 + + isomorphic-ws@5.0.0(ws@8.19.0): + dependencies: + ws: 8.19.0 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -4184,7 +5612,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.1 @@ -4253,6 +5681,36 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@24.10.9): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.10.9 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -4277,7 +5735,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4287,7 +5745,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.19.10 + '@types/node': 24.10.9 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -4326,7 +5784,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -4361,7 +5819,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -4389,7 +5847,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -4435,7 +5893,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -4454,7 +5912,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.10 + '@types/node': 24.10.9 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -4463,7 +5921,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.19.10 + '@types/node': 24.10.9 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -4489,12 +5947,28 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 @@ -4503,6 +5977,13 @@ snapshots: leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} link-preview-js@3.2.0: @@ -4510,22 +5991,36 @@ snapshots: cheerio: 1.0.0-rc.11 url: 0.11.0 + load-tsconfig@0.2.5: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + long@4.0.0: {} long@5.3.2: {} + lru-cache@10.4.3: {} + lru-cache@11.2.5: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@4.0.0: dependencies: semver: 7.7.3 @@ -4571,10 +6066,21 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + minimist@1.2.8: {} minipass@7.1.2: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + ms@2.1.3: {} music-metadata@11.11.0: @@ -4592,6 +6098,14 @@ snapshots: transitivePeerDependencies: - supports-color + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + napi-postinstall@0.3.4: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} @@ -4602,6 +6116,12 @@ snapshots: node-releases@2.0.27: {} + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.7.3 + validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} npm-run-path@4.0.1: @@ -4612,6 +6132,8 @@ snapshots: dependencies: boolbase: 1.0.0 + object-assign@4.1.1: {} + object-inspect@1.13.4: {} on-exit-leak-free@2.1.2: {} @@ -4628,6 +6150,15 @@ snapshots: dependencies: mimic-fn: 2.1.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -4640,10 +6171,18 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -4651,6 +6190,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -4677,10 +6222,14 @@ snapshots: path-to-regexp@8.3.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@4.0.3: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -4727,6 +6276,24 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pluralize@8.0.0: {} + + postcss-load-config@6.0.1(tsx@4.21.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + tsx: 4.21.0 + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -4785,6 +6352,8 @@ snapshots: punycode@1.3.2: {} + punycode@2.3.1: {} + pure-rand@6.1.0: {} qified@0.6.0: @@ -4810,14 +6379,38 @@ snapshots: react-is@18.3.1: {} + read-package-up@11.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 9.0.1 + type-fest: 4.41.0 + + read-pkg@9.0.1: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 8.3.0 + type-fest: 4.41.0 + unicorn-magic: 0.1.0 + + readdirp@4.1.2: {} + real-require@0.2.0: {} + regexp-tree@0.1.27: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + require-directory@2.1.1: {} resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4830,6 +6423,37 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -4967,10 +6591,28 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.22 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + split2@4.2.0: {} sprintf-js@1.0.3: {} + stable-hash-x@0.2.0: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -5006,6 +6648,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-indent@4.1.1: {} + strip-json-comments@3.1.1: {} strip-json-comments@5.0.3: {} @@ -5014,6 +6658,16 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5030,10 +6684,25 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thread-stream@3.1.0: dependencies: real-require: 0.2.0 + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -5050,6 +6719,12 @@ snapshots: tree-kill@1.2.2: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.10))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -5072,6 +6747,33 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(tsx@4.21.0)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(tsx@4.21.0) + resolve-from: 5.0.0 + rollup: 4.57.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -5106,6 +6808,10 @@ snapshots: turbo-windows-64: 2.7.6 turbo-windows-arm64: 2.7.6 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-detect@4.0.8: {} type-fest@0.21.3: {} @@ -5118,8 +6824,12 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typescript@4.9.5: {} + typescript@5.9.3: {} + ufo@1.6.3: {} + uglify-js@3.19.3: optional: true @@ -5129,14 +6839,44 @@ snapshots: undici-types@7.16.0: {} + unicorn-magic@0.1.0: {} + unpipe@1.0.0: {} + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + url@0.11.0: dependencies: punycode: 1.3.2 @@ -5148,6 +6888,11 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + vary@1.1.2: {} walker@1.0.8: @@ -5160,6 +6905,8 @@ snapshots: win-guid@0.2.0: {} + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@7.0.0: @@ -5199,4 +6946,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yerpc@0.6.4: + dependencies: + '@types/ws': 8.18.1 + isomorphic-ws: 4.0.1(ws@8.19.0) + typescript: 4.9.5 + optionalDependencies: + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + yocto-queue@0.1.0: {} diff --git a/turbo.json b/turbo.json index 2dccf99..1c4003f 100644 --- a/turbo.json +++ b/turbo.json @@ -4,7 +4,7 @@ "globalEnv": [ "NODE_ENV", "ZAMMAD_URL", - "SIGNAL_CLI_URL" + "BRIDGE_SIGNAL_URL" ], "tasks": { "dev": { @@ -19,6 +19,13 @@ "dist/**", "docker/zammad/addons/**" ] - } + }, + "lint": { + "dependsOn": ["^build"] + }, + "format": { + "cache": false + }, + "format:check": {} } }