diff --git a/.gitignore b/.gitignore index a031dba..6811d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,12 @@ npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* +.sfu*.log +.web*.log + +# local tooling +.corepack/ +.tmp/ # env files (can opt-in for committing if needed) .env* diff --git a/apps/web/package.json b/apps/web/package.json index ea1bb8b..fe7da16 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,7 @@ "dependencies": { "@conclave/apps-sdk": "workspace:*", "@conclave/meeting-core": "workspace:*", - "@tanstack/react-hotkeys": "^0.1.1", + "@tanstack/react-hotkeys": "^0.1.3", "better-auth": "^1.4.10", "jose": "^6.1.3", "jsonwebtoken": "^9.0.3", @@ -21,7 +21,8 @@ "next": "^16.1.6", "react": "^19.2.1", "react-dom": "^19.2.1", - "socket.io-client": "^4.8.3" + "socket.io-client": "^4.8.3", + "ws": "^8.18.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -29,6 +30,7 @@ "@types/node": "^20", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@types/ws": "^8.5.10", "babel-plugin-react-compiler": "^1.0.0", "tailwindcss": "^4", "typescript": "^5.9.3" diff --git a/apps/web/src/app/api/minutes/route.ts b/apps/web/src/app/api/minutes/route.ts new file mode 100644 index 0000000..5a84543 --- /dev/null +++ b/apps/web/src/app/api/minutes/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; + +const SFU_URL = process.env.SFU_URL || "http://localhost:3031"; +const SFU_SECRET = process.env.SFU_SECRET || "development-secret"; +const SFU_CLIENT_ID = process.env.SFU_CLIENT_ID || "default"; + +export async function POST(request: Request) { + const { roomId } = await request.json().catch(() => ({ roomId: undefined })); + if (!roomId || typeof roomId !== "string") { + return NextResponse.json({ error: "roomId required" }, { status: 400 }); + } + + const res = await fetch(`${SFU_URL}/minutes`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-sfu-secret": SFU_SECRET, + }, + body: JSON.stringify({ roomId, clientId: SFU_CLIENT_ID }), + }); + + if (!res.ok) { + const text = await res.text(); + return NextResponse.json({ error: text || "Failed to generate" }, { status: res.status }); + } + + const buffer = Buffer.from(await res.arrayBuffer()); + return new NextResponse(buffer, { + status: 200, + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="minutes-${roomId}.pdf"`, + }, + }); +} diff --git a/apps/web/src/app/components/DevMeetToolsPanel.tsx b/apps/web/src/app/components/DevMeetToolsPanel.tsx index 8c5d495..e23b78e 100644 --- a/apps/web/src/app/components/DevMeetToolsPanel.tsx +++ b/apps/web/src/app/components/DevMeetToolsPanel.tsx @@ -159,7 +159,8 @@ export default function DevMeetToolsPanel({ roomId }: DevMeetToolsPanelProps) { }; const { io } = await import("socket.io-client"); const socket = io(data.sfuUrl, { - transports: ["websocket", "polling"], + transports: ["polling", "websocket"], + tryAllTransports: true, timeout: 10000, reconnection: false, forceNew: true, diff --git a/apps/web/src/app/hooks/useMeetSocket.ts b/apps/web/src/app/hooks/useMeetSocket.ts index 508d1a2..110d174 100644 --- a/apps/web/src/app/hooks/useMeetSocket.ts +++ b/apps/web/src/app/hooks/useMeetSocket.ts @@ -1133,6 +1133,7 @@ export function useMeetSocket({ for (const [producerId, consumer] of consumersRef.current.entries()) { if (consumer.closed || consumer.track?.readyState === "ended") { staleConsumerIds.push(producerId); + } } @@ -1473,7 +1474,8 @@ export function useMeetSocket({ } const socket = io(sfuUrl, { - transports: ["websocket", "polling"], + transports: ["polling", "websocket"], + tryAllTransports: true, timeout: SOCKET_TIMEOUT_MS, reconnection: false, auth: { token }, diff --git a/apps/web/src/app/meets-client.tsx b/apps/web/src/app/meets-client.tsx index fdfdaf7..1c44eab 100644 --- a/apps/web/src/app/meets-client.tsx +++ b/apps/web/src/app/meets-client.tsx @@ -820,10 +820,59 @@ export default function MeetsClient({ } }, [clearGuestStorage, currentUser, isSigningOut]); + const [showMinutesPrompt, setShowMinutesPrompt] = useState(false); + const [minutesBusy, setMinutesBusy] = useState(false); + const [minutesError, setMinutesError] = useState(null); + const leaveRoom = useCallback(() => { playNotificationSoundForEvents("leave"); socket.cleanup(); - }, [playNotificationSoundForEvents, socket.cleanup]); + }, [playNotificationSound, socket.cleanup]); + + const requestLeaveRoom = useCallback(() => { + if (isAdminFlag) { + setShowMinutesPrompt(true); + return; + } + leaveRoom(); + }, [isAdminFlag, leaveRoom]); + + const requestMinutesAndLeave = useCallback(async () => { + if (!isAdminFlag) { + leaveRoom(); + return; + } + setMinutesError(null); + setMinutesBusy(true); + try { + const res = await fetch("/api/minutes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ roomId }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || "failed to generate minutes"); + } + + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + const ts = new Date().toISOString().replace(/[:.]/g, "-"); + link.download = `minutes-${roomId}-${ts}.pdf`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + leaveRoom(); + } catch (err) { + setMinutesError((err as Error).message); + } finally { + setMinutesBusy(false); + } + }, [isAdminFlag, roomId, leaveRoom]); useEffect(() => { leaveRoomCommandRef.current = leaveRoom; @@ -967,6 +1016,43 @@ export default function MeetsClient({ connectionState === "reconnecting" || connectionState === "waiting"; // Waiting is a kind of loading state visually, or handled separately + const renderMinutesPrompt = showMinutesPrompt && isAdminFlag && ( +
+
+

+ Generate meeting minutes? +

+

+ Before ending the meet, do you want to generate minutes (PDF) with the transcript summary? +

+ {minutesError && ( +
+ {minutesError} +
+ )} +
+ + +
+
+
+ ); + const renderWithApps = (content: React.ReactNode) => ( + {renderMinutesPrompt} {isJoined && meetError && ( + {renderMinutesPrompt} { + console.log('✅ Connected! Sending config...'); + + // 1. Send Config + ws.send(JSON.stringify({ config: { sample_rate: 16000 } })); + + // 2. Send some "silence" to keep it alive (1 second of 16-bit PCM) + console.log('Sending 1 second of silence to keep connection open...'); + const silence = Buffer.alloc(32000); // 16000 samples * 2 bytes + ws.send(silence); +}); + +ws.on('message', (data) => { + console.log('📝 Received from Vosk:', data.toString()); +}); + +ws.on('close', (code, reason) => { + console.log(`🔌 Connection closed (Code: ${code}, Reason: ${reason || 'None'})`); +}); + +ws.on('error', (err) => console.error('❌ Error:', err)); \ No newline at end of file diff --git a/packages/sfu/server/http/createApp.ts b/packages/sfu/server/http/createApp.ts index ba64c51..fca06ee 100644 --- a/packages/sfu/server/http/createApp.ts +++ b/packages/sfu/server/http/createApp.ts @@ -5,6 +5,19 @@ import type { Server as SocketIOServer } from "socket.io"; import { config as defaultConfig } from "../../config/config.js"; import { Logger } from "../../utilities/loggers.js"; import type { SfuState } from "../state.js"; +import { + getCachedMinutes, + getCachedTranscript, + getRoomChannelId, + setCachedMinutes, + setCachedTranscript, +} from "../rooms.js"; +import { + getRoomTranscriptSnapshot, + stopRoomTranscriber, +} from "../recording/roomTranscriber.js"; +import { summarizeTranscript } from "../recording/summarizeTranscript.js"; +import { buildMinutesPdf } from "../recording/minutesPdf.js"; export type CreateSfuAppOptions = { state: SfuState; @@ -46,6 +59,7 @@ export const createSfuApp = ({ getIo, }: CreateSfuAppOptions): Express => { const app = express(); + const minutesGenerationInFlight = new Map>(); app.use(cors()); app.use(express.json()); @@ -195,5 +209,122 @@ export const createSfuApp = ({ }); }); + app.post("/minutes", async (req, res) => { + if (!hasValidSecret(req, config.sfuSecret)) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const { roomId, clientId = "default" } = req.body ?? {}; + if (!roomId || typeof roomId !== "string") { + return res.status(400).json({ error: "roomId required" }); + } + + const channelId = getRoomChannelId(clientId, roomId); + const activeRoom = state.rooms.get(channelId); + const isRoomActive = Boolean(activeRoom && !activeRoom.isEmpty()); + + // For finalized rooms, serve pre-generated cache. + if (!isRoomActive) { + const cached = getCachedMinutes(channelId); + if (cached) { + Logger.info(`Minutes cache hit for channel=${channelId}`); + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="minutes-${roomId}.pdf"`, + ); + return res.end(cached); + } + } + + const inFlight = minutesGenerationInFlight.get(channelId); + if (inFlight) { + Logger.info( + `Minutes request joined existing generation for channel=${channelId}`, + ); + try { + const pdf = await inFlight; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="minutes-${roomId}.pdf"`, + ); + return res.end(pdf); + } catch (err) { + Logger.warn("Joined minutes generation failed", err); + return res.status(500).json({ error: "Failed to generate minutes" }); + } + } + + const generation = (async (): Promise => { + let transcript = isRoomActive + ? getRoomTranscriptSnapshot(channelId) + : stopRoomTranscriber(channelId); + Logger.info( + `Minutes request: channel=${channelId} transcriptAfter${ + isRoomActive ? "Snapshot" : "Stop" + }=${transcript.length}`, + ); + if (!transcript.length && !isRoomActive) { + transcript = getCachedTranscript(channelId); + Logger.info( + `Minutes request: channel=${channelId} transcriptAfterCache=${transcript.length}`, + ); + } + + if (!transcript.length) { + const summary = + "No transcript available. Speech-to-text was not configured or no audio was captured."; + Logger.warn( + `Minutes request has empty transcript for channel=${channelId}`, + ); + const pdf = await buildMinutesPdf({ roomId, summary, transcript: [] }); + if (!isRoomActive) { + setCachedMinutes(channelId, pdf); + } + return pdf; + } + + const summary = await summarizeTranscript(transcript); + const pdf = await buildMinutesPdf({ roomId, summary, transcript }); + if (!isRoomActive) { + setCachedTranscript(channelId, transcript); + setCachedMinutes(channelId, pdf); + } + Logger.info( + `Minutes generated for channel=${channelId} transcriptChunks=${transcript.length}`, + ); + return pdf; + })(); + + minutesGenerationInFlight.set(channelId, generation); + + try { + const pdf = await generation; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="minutes-${roomId}.pdf"`, + ); + return res.end(pdf); + } catch (err) { + const fallbackPdf = isRoomActive ? null : getCachedMinutes(channelId); + if (fallbackPdf) { + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="minutes-${roomId}.pdf"`, + ); + return res.end(fallbackPdf); + } + Logger.warn("Failed to build minutes PDF", err); + return res.status(500).json({ error: "Failed to generate minutes" }); + } finally { + if (minutesGenerationInFlight.get(channelId) === generation) { + minutesGenerationInFlight.delete(channelId); + } + } + }); + return app; }; diff --git a/packages/sfu/server/recording/minutesPdf.ts b/packages/sfu/server/recording/minutesPdf.ts new file mode 100644 index 0000000..649f781 --- /dev/null +++ b/packages/sfu/server/recording/minutesPdf.ts @@ -0,0 +1,112 @@ +import PDFDocument from "pdfkit"; +import type { TranscriptChunk } from "./roomTranscriber.js"; + +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +const formatTimestamp = (ms: number): string => + new Date(ms).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "medium", + }); + +const normalizeSpeakerLabel = (value?: string): string => { + const next = (value || "").trim().replace(/\s+/g, " "); + if (!next || /^unknown$/i.test(next) || UUID_PATTERN.test(next)) { + return "Speaker"; + } + if (next.length <= 36) return next; + if (next.includes("@")) { + const first = next.split("@")[0]?.trim(); + if (first) return first; + } + return next.slice(0, 36); +}; + +const ensurePageSpace = (doc: PDFKit.PDFDocument, estimatedLineCount = 2): void => { + const minimumY = doc.page.height - doc.page.margins.bottom - estimatedLineCount * 14; + if (doc.y > minimumY) { + doc.addPage(); + } +}; + +const drawSectionTitle = (doc: PDFKit.PDFDocument, title: string): void => { + ensurePageSpace(doc, 3); + doc.font("Helvetica-Bold").fontSize(14).text(title); + doc.moveDown(0.2); + const startX = doc.page.margins.left; + const endX = doc.page.width - doc.page.margins.right; + const y = doc.y; + doc.moveTo(startX, y).lineTo(endX, y).strokeColor("#C7CDD3").lineWidth(1).stroke(); + doc.moveDown(0.4); + doc.strokeColor("black"); +}; + +export function buildMinutesPdf(options: { + roomId: string; + summary: string; + transcript: TranscriptChunk[]; +}): Promise { + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ margin: 50 }); + const chunks: Buffer[] = []; + + doc.on("data", (c) => chunks.push(c)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", (err) => reject(err)); + + doc.font("Helvetica-Bold").fontSize(21).text(`Meeting Minutes`, { align: "center" }); + doc.moveDown(0.5); + doc.font("Helvetica").fontSize(12).text(`Room: ${options.roomId}`); + doc.text(`Generated: ${formatTimestamp(Date.now())}`); + doc.moveDown(); + + drawSectionTitle(doc, "Summary"); + const summarySections = (options.summary || "No summary available.") + .split(/\n{2,}/) + .map((section) => section.trim()) + .filter(Boolean); + doc.font("Helvetica").fontSize(12); + for (const section of summarySections) { + ensurePageSpace(doc, 3); + doc.text(`- ${section}`, { + lineGap: 2, + }); + doc.moveDown(0.35); + } + doc.moveDown(); + + drawSectionTitle(doc, "Transcript"); + doc.font("Helvetica"); + + if (!options.transcript.length) { + doc.fontSize(11).text("No transcript captured.", { oblique: true }); + doc.end(); + return; + } + + for (const entry of options.transcript) { + ensurePageSpace(doc, 5); + const start = formatTimestamp(entry.startMs); + const speaker = normalizeSpeakerLabel(entry.speaker); + + doc.font("Helvetica-Bold").fontSize(10).text(`${speaker} - ${start}`); + doc.font("Helvetica").fontSize(11).text(entry.text, { + indent: 12, + lineGap: 1.5, + }); + doc.moveDown(0.55); + } + + doc.end(); + }); +} + + +//gameolan- +//use whisper ai as stt +//ONLY SEND active users(We already capture that info in sfu state) +//OKAY NVM this takes too much cpu and also needs gpu even w v little users +//switching to vosk for now +//SWITCH TO BETTER STUFF IF NEEDED LATER +//USING VOSK diff --git a/packages/sfu/server/recording/roomTranscriber.ts b/packages/sfu/server/recording/roomTranscriber.ts new file mode 100644 index 0000000..fecb438 --- /dev/null +++ b/packages/sfu/server/recording/roomTranscriber.ts @@ -0,0 +1,611 @@ +import dgram from "dgram"; +import { spawn } from "child_process"; +import WebSocket from "ws"; +import type { + Producer, + Router, + RtpCapabilities, + PlainTransport, + Consumer, +} from "mediasoup/types"; +import { Logger } from "../../utilities/loggers.js"; + +const DEFAULT_STT_SAMPLE_RATE = Number(process.env.STT_SAMPLE_RATE || 16000); +const FFMPEG_BIN = process.env.FFMPEG_PATH || "ffmpeg"; +const FFMPEG_FORCE_KILL_TIMEOUT_MS = Number( + process.env.FFMPEG_FORCE_KILL_TIMEOUT_MS || 2000, +); +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +const normalizeSpeakerLabel = (value?: string): string => { + const next = (value || "").trim().replace(/\s+/g, " "); + if (!next) return ""; + if (/^unknown$/i.test(next)) return ""; + if (UUID_PATTERN.test(next)) return ""; + return next.length > 48 ? next.slice(0, 48) : next; +}; + +type VoskWord = { start?: number; end?: number }; +type VoskMessage = { + text?: string; + partial?: string; + speaker?: string; + channel?: string; + start?: number; + end?: number; + result?: VoskWord[]; + alternatives?: Array<{ text?: string }>; +}; + +type RtpCodecLike = { + payloadType: number; + mimeType: string; + clockRate: number; + channels?: number; + parameters?: Record; +}; + +export type TranscriptChunk = { + startMs: number; + endMs: number; + text: string; + speaker?: string; +}; + +const allocateUdpPort = (): Promise => + new Promise((resolve, reject) => { + const socket = dgram.createSocket("udp4"); + socket.once("error", (err) => { + try { + socket.close(); + } catch {} + reject(err); + }); + socket.bind(0, "127.0.0.1", () => { + const addr = socket.address(); + if (typeof addr === "string") { + try { + socket.close(); + } catch {} + reject(new Error("Failed to allocate UDP port")); + return; + } + const port = addr.port; + try { + socket.close(); + } catch (err) { + reject(err); + return; + } + resolve(port); + }); + }); + +const encodeFmtp = (parameters?: Record): string => { + if (!parameters) return ""; + const entries = Object.entries(parameters).filter( + ([, value]) => value !== undefined && value !== null, + ); + return entries + .map(([key, value]) => `${key}=${String(value)}`) + .join(";"); +}; + +const buildConsumerSdp = (rtpPort: number, consumer: Consumer): string => { + const codec = (consumer.rtpParameters.codecs?.[0] || null) as + | RtpCodecLike + | null; + if (!codec) { + throw new Error("No RTP codec found for room transcriber consumer"); + } + const codecName = codec.mimeType.split("/")[1] || "opus"; + const channels = codec.channels && codec.channels > 1 ? `/${codec.channels}` : ""; + const fmtp = encodeFmtp(codec.parameters); + const lines = [ + "v=0", + "o=- 0 0 IN IP4 127.0.0.1", + "s=ConclaveRoomAudio", + "c=IN IP4 127.0.0.1", + "t=0 0", + `m=audio ${rtpPort} RTP/AVP ${codec.payloadType}`, + "a=rtcp-mux", + `a=rtpmap:${codec.payloadType} ${codecName}/${codec.clockRate}${channels}`, + ]; + if (fmtp) { + lines.push(`a=fmtp:${codec.payloadType} ${fmtp}`); + } + for (const ext of consumer.rtpParameters.headerExtensions || []) { + lines.push(`a=extmap:${ext.id} ${ext.uri}`); + } + lines.push("a=recvonly"); + return `${lines.join("\n")}\n`; +}; + +class RoomTranscriber { + private router: Router; + private onStop?: (transcript: TranscriptChunk[]) => void; + private ffmpeg?: ReturnType; + private sttSocket?: WebSocket; + private transport?: PlainTransport; + private consumer?: Consumer; + private producerId?: string; + private transcript: TranscriptChunk[] = []; + private lastPartialText = ""; + private sessionStartedAtMs = Date.now(); + private speakerLabel = "Speaker"; + private stopSubscriptions: Array<() => void> = []; + private ffmpegForceKillTimer?: ReturnType; + private stopped = true; + private forwardedAudioBytes = 0; + private sttMessageCount = 0; + + private ffmpegOnError?: (err: Error) => void; + private ffmpegOnExit?: (code: number | null, signal: NodeJS.Signals | null) => void; + private ffmpegStdoutOnData?: (chunk: Buffer) => void; + private ffmpegStderrOnData?: (chunk: Buffer) => void; + private sttOnOpen?: () => void; + private sttOnError?: (err: Error) => void; + private sttOnMessage?: (data: WebSocket.RawData) => void; + + constructor( + router: Router, + onStop?: (transcript: TranscriptChunk[]) => void, + ) { + this.router = router; + this.onStop = onStop; + } + + async start( + producer: Producer, + opts: { + sttUrl: string; + sttHeaders?: Record; + speakerLabel?: string; + }, + ): Promise { + if (!opts.sttUrl) { + Logger.warn("STT_WS_URL not set; transcriber not started"); + return; + } + if (this.transport || this.consumer || this.ffmpeg || this.sttSocket) { + Logger.info("Transcriber already active for this producer; skipping"); + return; + } + + this.stopped = false; + this.producerId = producer.id; + this.speakerLabel = + normalizeSpeakerLabel(opts.speakerLabel) || + normalizeSpeakerLabel(producer.id) || + "Speaker"; + this.sessionStartedAtMs = Date.now(); + this.forwardedAudioBytes = 0; + this.sttMessageCount = 0; + + try { + const transport = await this.router.createPlainTransport({ + listenIp: { ip: "127.0.0.1", announcedIp: undefined }, + rtcpMux: true, + comedia: false, + }); + this.transport = transport; + + const rtpPort = await allocateUdpPort(); + await transport.connect({ ip: "127.0.0.1", port: rtpPort }); + + const consumer = await transport.consume({ + producerId: producer.id, + rtpCapabilities: this.router.rtpCapabilities as RtpCapabilities, + paused: true, + }); + this.consumer = consumer; + + const sdp = buildConsumerSdp(rtpPort, consumer); + + this.ffmpeg = spawn( + FFMPEG_BIN, + [ + "-hide_banner", + "-loglevel", + "warning", + "-protocol_whitelist", + "file,pipe,udp,rtp", + "-f", + "sdp", + "-i", + "pipe:0", + "-ac", + "1", + "-ar", + `${DEFAULT_STT_SAMPLE_RATE}`, + "-f", + "s16le", + "pipe:1", + ], + { stdio: ["pipe", "pipe", "pipe"] }, + ); + + Logger.info( + `Transcriber starting: producer=${producer.id} speaker=${this.speakerLabel} stt=${opts.sttUrl} ffmpeg=${FFMPEG_BIN} rtpPort=${rtpPort}`, + ); + + this.ffmpegOnError = (err) => { + Logger.warn("Failed to start ffmpeg for STT pipeline", { + ffmpeg: FFMPEG_BIN, + err, + }); + this.stop(); + }; + this.ffmpeg.on("error", this.ffmpegOnError); + + this.ffmpegOnExit = (code, signal) => { + if (this.ffmpegForceKillTimer) { + clearTimeout(this.ffmpegForceKillTimer); + this.ffmpegForceKillTimer = undefined; + } + if (!this.stopped && code !== 0) { + Logger.warn("ffmpeg exited unexpectedly in STT pipeline", { + ffmpeg: FFMPEG_BIN, + code, + signal, + }); + this.stop(); + } + }; + this.ffmpeg.on("exit", this.ffmpegOnExit); + + this.ffmpegStderrOnData = (chunk) => { + const line = chunk.toString("utf8").trim(); + if (line) { + Logger.warn(`ffmpeg/stt stderr: ${line}`); + } + }; + this.ffmpeg.stderr?.on("data", this.ffmpegStderrOnData); + + if (this.ffmpeg.stdin?.writable) { + this.ffmpeg.stdin.write(sdp); + this.ffmpeg.stdin.end(); + } + + this.sttSocket = new WebSocket(opts.sttUrl, { headers: opts.sttHeaders }); + + this.sttOnOpen = () => { + Logger.info(`STT websocket open for producer=${producer.id}`); + this.sttSocket?.send( + JSON.stringify({ config: { sample_rate: DEFAULT_STT_SAMPLE_RATE } }), + ); + }; + this.sttSocket.on("open", this.sttOnOpen); + + this.sttOnError = (err) => { + Logger.warn("STT websocket error", err); + }; + this.sttSocket.on("error", this.sttOnError); + + this.sttOnMessage = (data) => { + this.sttMessageCount += 1; + this.handleSttMessage(this.toUtf8(data), producer.id); + }; + this.sttSocket.on("message", this.sttOnMessage); + + this.ffmpegStdoutOnData = (chunk: Buffer) => { + this.forwardedAudioBytes += chunk.length; + if (this.forwardedAudioBytes === chunk.length) { + Logger.info(`Transcriber audio flow started for producer=${producer.id}`); + } + if (this.sttSocket?.readyState === WebSocket.OPEN) { + this.sttSocket.send(chunk); + } + }; + this.ffmpeg.stdout?.on("data", this.ffmpegStdoutOnData); + + const stop = () => this.stop(); + consumer.on("producerclose", stop); + consumer.on("transportclose", stop); + transport.on("routerclose", stop); + this.stopSubscriptions.push(() => consumer.off("producerclose", stop)); + this.stopSubscriptions.push(() => consumer.off("transportclose", stop)); + this.stopSubscriptions.push(() => transport.off("routerclose", stop)); + + await consumer.resume(); + } catch (err) { + Logger.warn("Failed to start room transcriber", err); + this.stop(); + } + } + + private toUtf8(data: WebSocket.RawData): string { + if (typeof data === "string") return data; + if (Buffer.isBuffer(data)) return data.toString("utf8"); + if (Array.isArray(data)) { + return Buffer.concat(data).toString("utf8"); + } + return data.toString(); + } + + private handleSttMessage(raw: string, producerId: string): void { + try { + const msg = JSON.parse(raw) as VoskMessage; + const finalText = this.extractFinalText(msg); + if (finalText) { + const { startMs, endMs } = this.getTimestampRangeMs(msg); + this.appendTranscript({ + startMs, + endMs, + text: finalText, + speaker: this.resolveSpeakerLabel(msg, producerId), + }); + this.lastPartialText = ""; + return; + } + + const partial = typeof msg.partial === "string" ? msg.partial.trim() : ""; + if (partial) { + this.lastPartialText = partial; + } + } catch (err) { + Logger.warn("STT parse error", err); + } + } + + private resolveSpeakerLabel(msg: VoskMessage, producerId: string): string { + const fromMessage = normalizeSpeakerLabel(msg.speaker || msg.channel); + if (fromMessage) return fromMessage; + return ( + this.speakerLabel || + normalizeSpeakerLabel(producerId) || + "Speaker" + ); + } + + private extractFinalText(msg: VoskMessage): string { + const text = typeof msg.text === "string" ? msg.text.trim() : ""; + if (text) return text; + const altText = Array.isArray(msg.alternatives) + ? (msg.alternatives[0]?.text || "").trim() + : ""; + return altText; + } + + private getTimestampRangeMs(msg: VoskMessage): { + startMs: number; + endMs: number; + } { + const now = Date.now(); + let startSeconds: number | undefined; + let endSeconds: number | undefined; + + if (Array.isArray(msg.result) && msg.result.length) { + const first = msg.result[0]; + const last = msg.result[msg.result.length - 1]; + if (Number.isFinite(Number(first.start))) { + startSeconds = Number(first.start); + } + if (Number.isFinite(Number(last.end))) { + endSeconds = Number(last.end); + } + } + + if (startSeconds === undefined && Number.isFinite(Number(msg.start))) { + startSeconds = Number(msg.start); + } + if (endSeconds === undefined && Number.isFinite(Number(msg.end))) { + endSeconds = Number(msg.end); + } + + const startMs = + startSeconds !== undefined + ? this.sessionStartedAtMs + Math.round(startSeconds * 1000) + : now; + const endMs = + endSeconds !== undefined + ? this.sessionStartedAtMs + Math.round(endSeconds * 1000) + : startMs; + + return { startMs, endMs: Math.max(endMs, startMs) }; + } + + private appendTranscript(chunk: TranscriptChunk): void { + const text = chunk.text.replace(/\s+/g, " ").trim(); + if (!text) return; + const last = this.transcript[this.transcript.length - 1]; + if ( + last && + last.text === text && + Math.abs(last.endMs - chunk.endMs) < 1500 && + (last.speaker || "") === (chunk.speaker || "") + ) { + return; + } + this.transcript.push({ ...chunk, text }); + } + + private removeListeners(): void { + while (this.stopSubscriptions.length) { + const unsubscribe = this.stopSubscriptions.pop(); + try { + unsubscribe?.(); + } catch {} + } + + if (this.ffmpeg && this.ffmpegOnError) { + this.ffmpeg.off("error", this.ffmpegOnError); + } + if (this.ffmpeg && this.ffmpegOnExit) { + this.ffmpeg.off("exit", this.ffmpegOnExit); + } + if (this.ffmpeg?.stdout && this.ffmpegStdoutOnData) { + this.ffmpeg.stdout.off("data", this.ffmpegStdoutOnData); + } + if (this.ffmpeg?.stderr && this.ffmpegStderrOnData) { + this.ffmpeg.stderr.off("data", this.ffmpegStderrOnData); + } + + if (this.sttSocket && this.sttOnOpen) { + this.sttSocket.off("open", this.sttOnOpen); + } + if (this.sttSocket && this.sttOnError) { + this.sttSocket.off("error", this.sttOnError); + } + if (this.sttSocket && this.sttOnMessage) { + this.sttSocket.off("message", this.sttOnMessage); + } + + this.ffmpegOnError = undefined; + this.ffmpegOnExit = undefined; + this.ffmpegStdoutOnData = undefined; + this.ffmpegStderrOnData = undefined; + this.sttOnOpen = undefined; + this.sttOnError = undefined; + this.sttOnMessage = undefined; + } + + getTranscript(): TranscriptChunk[] { + return this.transcript.filter((chunk) => Boolean(chunk.text.trim())); + } + + stop(): void { + if (this.stopped) return; + this.stopped = true; + + if (this.lastPartialText) { + const now = Date.now(); + this.appendTranscript({ + startMs: now, + endMs: now, + text: this.lastPartialText, + speaker: this.speakerLabel || "Speaker", + }); + this.lastPartialText = ""; + } + + Logger.info( + `Stopping transcriber: producer=${this.producerId || "unknown"} chunks=${this.transcript.length} sttMessages=${this.sttMessageCount} audioBytes=${this.forwardedAudioBytes}`, + ); + + if (this.sttSocket?.readyState === WebSocket.OPEN) { + try { + this.sttSocket.send(JSON.stringify({ eof: 1 })); + } catch {} + } + + this.removeListeners(); + this.sttSocket?.close(); + + if (this.ffmpeg) { + const ffmpegProcess = this.ffmpeg; + try { + ffmpegProcess.kill("SIGTERM"); + } catch {} + this.ffmpegForceKillTimer = setTimeout(() => { + if (ffmpegProcess.exitCode === null && !ffmpegProcess.killed) { + try { + ffmpegProcess.kill("SIGKILL"); + } catch {} + } + }, FFMPEG_FORCE_KILL_TIMEOUT_MS); + ffmpegProcess.once("exit", () => { + if (this.ffmpegForceKillTimer) { + clearTimeout(this.ffmpegForceKillTimer); + this.ffmpegForceKillTimer = undefined; + } + }); + } + + try { + this.consumer?.close(); + } catch {} + try { + this.transport?.close(); + } catch {} + + this.consumer = undefined; + this.transport = undefined; + this.sttSocket = undefined; + this.ffmpeg = undefined; + this.producerId = undefined; + this.onStop?.(this.getTranscript()); + } +} + +const transcribers = new Map(); +const TRANSCRIBER_KEY_SEPARATOR = "::"; +const roomTranscriptHistory = new Map(); + +const cloneTranscript = (chunks: TranscriptChunk[]): TranscriptChunk[] => + chunks.map((chunk) => ({ ...chunk })); + +const sortTranscript = (chunks: TranscriptChunk[]): TranscriptChunk[] => + cloneTranscript(chunks).sort((a, b) => { + if (a.startMs !== b.startMs) return a.startMs - b.startMs; + return a.endMs - b.endMs; + }); + +const mergeRoomTranscript = ( + channelId: string, + chunks: TranscriptChunk[], +): void => { + if (!chunks.length) return; + const existing = roomTranscriptHistory.get(channelId); + if (!existing) { + roomTranscriptHistory.set(channelId, cloneTranscript(chunks)); + return; + } + existing.push(...cloneTranscript(chunks)); +}; + +const getTranscriberKey = (channelId: string, producerId: string): string => + `${channelId}${TRANSCRIBER_KEY_SEPARATOR}${producerId}`; + +const getChannelPrefix = (channelId: string): string => + `${channelId}${TRANSCRIBER_KEY_SEPARATOR}`; + +export const ensureProducerTranscriber = ( + channelId: string, + producerId: string, + router: Router, +): RoomTranscriber => { + const key = getTranscriberKey(channelId, producerId); + let transcriber = transcribers.get(key); + if (!transcriber) { + let createdTranscriber: RoomTranscriber; + createdTranscriber = new RoomTranscriber(router, (transcript) => { + mergeRoomTranscript(channelId, transcript); + if (transcribers.get(key) === createdTranscriber) { + transcribers.delete(key); + } + }); + transcriber = createdTranscriber; + transcribers.set(key, transcriber); + } + return transcriber; +}; + +export const getRoomTranscriptSnapshot = (channelId: string): TranscriptChunk[] => { + const channelPrefix = getChannelPrefix(channelId); + const combined: TranscriptChunk[] = []; + const history = roomTranscriptHistory.get(channelId); + if (history?.length) { + combined.push(...cloneTranscript(history)); + } + + for (const [key, transcriber] of transcribers.entries()) { + if (!key.startsWith(channelPrefix)) continue; + combined.push(...transcriber.getTranscript()); + } + + return sortTranscript(combined); +}; + +export const stopRoomTranscriber = (channelId: string): TranscriptChunk[] => { + const channelPrefix = getChannelPrefix(channelId); + for (const [key, transcriber] of transcribers.entries()) { + if (!key.startsWith(channelPrefix)) continue; + transcriber.stop(); + transcribers.delete(key); + } + const history = roomTranscriptHistory.get(channelId) || []; + roomTranscriptHistory.delete(channelId); + return sortTranscript(history); +}; diff --git a/packages/sfu/server/recording/summarizeTranscript.ts b/packages/sfu/server/recording/summarizeTranscript.ts new file mode 100644 index 0000000..c9db41a --- /dev/null +++ b/packages/sfu/server/recording/summarizeTranscript.ts @@ -0,0 +1,155 @@ +import { Logger } from "../../utilities/loggers.js"; +import type { TranscriptChunk } from "./roomTranscriber.js"; + +const DEFAULT_MODEL_URL = + process.env.HF_SUMMARY_URL || + "https://api-inference.huggingface.co/models/sshleifer/distilbart-cnn-12-6"; + +const HUGGINGFACE_TOKEN = process.env.HUGGINGFACE_TOKEN; + +const MAX_TEXT_LENGTH = 6000; +const FALLBACK_SENTENCE_LIMIT = 4; +const TOPIC_LIMIT = 6; +const STOPWORDS = new Set([ + "a", + "an", + "and", + "are", + "as", + "at", + "be", + "by", + "for", + "from", + "has", + "have", + "i", + "in", + "is", + "it", + "its", + "of", + "on", + "or", + "that", + "the", + "their", + "this", + "to", + "was", + "we", + "were", + "will", + "with", + "you", + "your", +]); +const ACTION_TERMS = /\b(action|owner|deadline|due|follow[- ]?up|next step|todo|decide|decision|approve|ship|deliver)\b/i; + +const buildPrompt = (chunks: TranscriptChunk[]): string => { + const lines = chunks.map((c) => c.text.trim()).filter(Boolean); + const text = lines.join("\n"); + return text.length > MAX_TEXT_LENGTH + ? text.slice(0, MAX_TEXT_LENGTH) + : text; +}; + +const tokenize = (text: string): string[] => + (text.toLowerCase().match(/[a-z]{3,}/g) || []).filter( + (token) => !STOPWORDS.has(token), + ); + +const stripMetadata = (sentence: string): string => + sentence + .replace(/\[[^\]]+\]\s*/g, "") + .replace(/^[A-Za-z0-9._@#-]{2,64}:\s*/, "") + .trim(); + +const localFallbackSummary = (text: string): string => { + const normalized = text.replace(/\s+/g, " ").trim(); + const rawSentences = normalized + .split(/(?<=[.!?])\s+|\n+/) + .map((sentence) => stripMetadata(sentence)) + .filter(Boolean); + + if (!rawSentences.length) return "No content"; + + const frequency = new Map(); + for (const token of tokenize(normalized)) { + frequency.set(token, (frequency.get(token) || 0) + 1); + } + + const scored = rawSentences.map((sentence, index) => { + const tokens = tokenize(sentence); + const tokenScore = tokens.reduce( + (sum, token) => sum + (frequency.get(token) || 0), + 0, + ); + const density = tokens.length ? tokenScore / tokens.length : 0; + const actionBoost = ACTION_TERMS.test(sentence) ? 1.4 : 1; + return { index, sentence, score: density * actionBoost }; + }); + + const selected = scored + .sort((a, b) => b.score - a.score) + .slice(0, Math.min(FALLBACK_SENTENCE_LIMIT, rawSentences.length)) + .sort((a, b) => a.index - b.index) + .map((entry) => entry.sentence); + + const topTopics = [...frequency.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, TOPIC_LIMIT) + .map(([topic]) => topic); + + const actionItems = rawSentences.filter((sentence) => ACTION_TERMS.test(sentence)); + + const sections = [ + `Key discussion points: ${selected.join(" ")}`, + topTopics.length ? `Top themes: ${topTopics.join(", ")}.` : "", + actionItems.length + ? `Action items noted: ${actionItems.slice(0, 3).join(" ")}` + : "Action items noted: none captured explicitly.", + ].filter(Boolean); + + return sections.join("\n\n"); +}; + +export async function summarizeTranscript( + chunks: TranscriptChunk[], +): Promise { + if (!chunks.length) return "No transcript available."; + const text = buildPrompt(chunks); + + if (HUGGINGFACE_TOKEN) { + try { + const res = await fetch(DEFAULT_MODEL_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${HUGGINGFACE_TOKEN}`, + }, + body: JSON.stringify({ + inputs: text, + parameters: { max_length: 220, min_length: 60 }, + }), + }); + + if (!res.ok) { + const body = await res.text(); + Logger.warn("HF summary request failed", { status: res.status, body }); + return localFallbackSummary(text); + } + + const data = (await res.json()) as any; + const summary = Array.isArray(data) + ? data[0]?.summary_text + : (data as any)?.summary_text; + return summary || localFallbackSummary(text); + } catch (err) { + Logger.warn("HF summary request errored", err); + return localFallbackSummary(text); + } + } + + return localFallbackSummary(text); +} diff --git a/packages/sfu/server/rooms.ts b/packages/sfu/server/rooms.ts index dae9550..903ac0b 100644 --- a/packages/sfu/server/rooms.ts +++ b/packages/sfu/server/rooms.ts @@ -3,12 +3,21 @@ import { Room } from "../config/classes/Room.js"; import { config } from "../config/config.js"; import getWorker from "../utilities/getWorker.js"; import { Logger } from "../utilities/loggers.js"; +import { + stopRoomTranscriber, + type TranscriptChunk, +} from "./recording/roomTranscriber.js"; +import { summarizeTranscript } from "./recording/summarizeTranscript.js"; +import { buildMinutesPdf } from "./recording/minutesPdf.js"; import { cleanupRoomBrowser } from "./socket/handlers/sharedBrowserHandlers.js"; import type { SfuState } from "./state.js"; import { clearWebinarLinkSlug } from "./webinar.js"; export const getRoomChannelId = (clientId: string, roomId: string): string => `${clientId}:${roomId}`; +const MINUTES_CACHE_TTL_MS = Number( + process.env.MINUTES_CACHE_TTL_MS || 30 * 60 * 1000, +); export const getOrCreateRoom = async ( state: SfuState, @@ -20,6 +29,7 @@ export const getOrCreateRoom = async ( if (room) { return room; } + clearCachedMinutesArtifacts(channelId); const worker = await getWorker(state.workers as Worker[]); @@ -37,6 +47,29 @@ export const getOrCreateRoom = async ( export const cleanupRoom = (state: SfuState, channelId: string): boolean => { const room = state.rooms.get(channelId); if (room && room.isEmpty()) { + const transcript = stopRoomTranscriber(channelId); + if (transcript.length) { + setCachedTranscript(channelId, transcript); + void summarizeTranscript(transcript) + .then(async (summary) => { + Logger.info(`Room ${room.id} summary`, summary); + // Cache PDF in memory for quick retrieval if needed shortly after cleanup + // add feature that allows access to earlier MOMs later REMEMVERRR + try { + const pdf = await buildMinutesPdf({ + roomId: room.id, + summary, + transcript, + }); + setCachedMinutes(channelId, pdf); + } catch (err) { + Logger.warn("Failed to build minutes PDF", err); + } + }) + .catch((err) => { + Logger.warn("Failed to summarize transcript", err); + }); + } const webinarConfig = state.webinarConfigs.get(channelId); if (webinarConfig) { clearWebinarLinkSlug({ @@ -54,3 +87,53 @@ export const cleanupRoom = (state: SfuState, channelId: string): boolean => { } return false; }; + +type CachedMinutes = { pdf: Buffer; createdAt: number }; +const RoomMinutesCache = new Map(); +type CachedTranscript = { transcript: TranscriptChunk[]; createdAt: number }; +const RoomTranscriptCache = new Map(); + +const isFreshEntry = (createdAt: number): boolean => + Date.now() - createdAt <= MINUTES_CACHE_TTL_MS; + +const cloneTranscript = (transcript: TranscriptChunk[]): TranscriptChunk[] => + transcript.map((chunk) => ({ ...chunk })); + +export const setCachedMinutes = (channelId: string, pdf: Buffer): void => { + RoomMinutesCache.set(channelId, { pdf: Buffer.from(pdf), createdAt: Date.now() }); +}; + +export const setCachedTranscript = ( + channelId: string, + transcript: TranscriptChunk[], +): void => { + RoomTranscriptCache.set(channelId, { + transcript: cloneTranscript(transcript), + createdAt: Date.now(), + }); +}; + +export const clearCachedMinutesArtifacts = (channelId: string): void => { + RoomMinutesCache.delete(channelId); + RoomTranscriptCache.delete(channelId); +}; + +export const getCachedMinutes = (channelId: string): Buffer | null => { + const entry = RoomMinutesCache.get(channelId); + if (!entry) return null; + if (!isFreshEntry(entry.createdAt)) { + RoomMinutesCache.delete(channelId); + return null; + } + return Buffer.from(entry.pdf); +}; + +export const getCachedTranscript = (channelId: string): TranscriptChunk[] => { + const entry = RoomTranscriptCache.get(channelId); + if (!entry) return []; + if (!isFreshEntry(entry.createdAt)) { + RoomTranscriptCache.delete(channelId); + return []; + } + return cloneTranscript(entry.transcript); +}; diff --git a/packages/sfu/server/socket/handlers/mediaHandlers.ts b/packages/sfu/server/socket/handlers/mediaHandlers.ts index bd69119..e78705d 100644 --- a/packages/sfu/server/socket/handlers/mediaHandlers.ts +++ b/packages/sfu/server/socket/handlers/mediaHandlers.ts @@ -7,10 +7,13 @@ import type { ToggleMediaData, } from "../../../types.js"; import { Logger } from "../../../utilities/loggers.js"; +import { ensureProducerTranscriber } from "../../recording/roomTranscriber.js"; import { emitWebinarFeedChanged } from "../../webinarNotifications.js"; import type { ConnectionContext } from "../context.js"; import { respond } from "./ack.js"; +const DEFAULT_LOCAL_VOSK_WS_URL = "ws://127.0.0.1:2800"; + export const registerMediaHandlers = (context: ConnectionContext): void => { const { socket, state, io } = context; @@ -65,6 +68,39 @@ export const registerMediaHandlers = (context: ConnectionContext): void => { paused, }); + if (kind === "audio" && type === "webcam") { + const channelId = room.channelId; + const speakerLabel = + room.getDisplayNameForUser(currentClient.id) || currentClient.id; + const sttUrl = + process.env.STT_WS_URL || + process.env.VOSK_WS_URL || + (process.env.NODE_ENV === "production" + ? "" + : DEFAULT_LOCAL_VOSK_WS_URL); + if (!sttUrl) { + Logger.warn( + `Skipping transcriber start for channel=${channelId} producer=${producer.id}: STT URL is not configured`, + ); + } else { + const transcriber = ensureProducerTranscriber( + channelId, + producer.id, + room.router, + ); + Logger.info( + `Starting room transcriber for channel=${channelId} producer=${producer.id} stt=${sttUrl}`, + ); + void transcriber.start(producer, { + sttUrl, + sttHeaders: process.env.STT_API_KEY + ? { Authorization: `Bearer ${process.env.STT_API_KEY}` } + : undefined, + speakerLabel, + }); + } + } + const roomChannelId = room.channelId; const clientId = currentClient.id; let producerClosed = false; diff --git a/packages/sfu/utilities/loggers.ts b/packages/sfu/utilities/loggers.ts index a4f1d6e..5792221 100644 --- a/packages/sfu/utilities/loggers.ts +++ b/packages/sfu/utilities/loggers.ts @@ -1,3 +1,7 @@ +import fs from "fs"; +import path from "path"; +import { inspect } from "util"; + export const colors = { reset: "\x1b[0m", bright: "\x1b[1m", @@ -35,6 +39,29 @@ const getTimestamp = () => { }; const PREFIX = `${colors.fg.magenta}[SFU]${colors.reset}`; +const LOG_FILE_PATH = process.env.SFU_LOG_FILE + ? path.resolve(process.env.SFU_LOG_FILE) + : path.resolve(process.cwd(), ".sfu.runtime.log"); + +const stripAnsi = (value: string): string => + value.replace(/\x1b\[[0-9;]*m/g, ""); + +const writeLogFile = (level: LogLevel, message: string, args: any[]): void => { + const argsText = args + .map((arg) => + typeof arg === "string" + ? arg + : inspect(arg, { depth: 5, breakLength: 120, compact: true }), + ) + .join(" "); + const suffix = argsText ? ` ${argsText}` : ""; + const line = `${new Date().toISOString()} [${level.toUpperCase()}] ${stripAnsi(message)}${suffix}\n`; + try { + fs.appendFileSync(LOG_FILE_PATH, line, "utf8"); + } catch { + // File logging should never break runtime logging. + } +}; type LogLevel = "error" | "warn" | "info" | "debug"; @@ -66,6 +93,7 @@ const shouldLog = (level: LogLevel) => { export const Logger = { info: (message: string, ...args: any[]) => { if (!shouldLog("info")) return; + writeLogFile("info", message, args); console.log( `${colors.dim}${getTimestamp()}${colors.reset} ${PREFIX} ${ colors.fg.cyan @@ -76,6 +104,7 @@ export const Logger = { success: (message: string, ...args: any[]) => { if (!shouldLog("info")) return; + writeLogFile("info", message, args); console.log( `${colors.dim}${getTimestamp()}${colors.reset} ${PREFIX} ${ colors.fg.green @@ -86,6 +115,7 @@ export const Logger = { warn: (message: string, ...args: any[]) => { if (!shouldLog("warn")) return; + writeLogFile("warn", message, args); console.warn( `${colors.dim}${getTimestamp()}${colors.reset} ${PREFIX} ${ colors.fg.yellow @@ -96,6 +126,7 @@ export const Logger = { error: (message: string, ...args: any[]) => { if (!shouldLog("error")) return; + writeLogFile("error", message, args); console.error( `${colors.dim}${getTimestamp()}${colors.reset} ${PREFIX} ${ colors.fg.red @@ -106,6 +137,7 @@ export const Logger = { debug: (message: string, ...args: any[]) => { if (!shouldLog("debug")) return; + writeLogFile("debug", message, args); console.log( `${colors.dim}${getTimestamp()}${colors.reset} ${PREFIX} ${ colors.fg.gray diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4dd4c4..5497769 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,8 +17,8 @@ importers: specifier: workspace:* version: link:../../packages/meeting-core '@tanstack/react-hotkeys': - specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^0.1.3 + version: 0.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) better-auth: specifier: ^1.4.10 version: 1.4.10(next@16.1.6(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -46,6 +46,9 @@ importers: socket.io-client: specifier: ^4.8.3 version: 4.8.3 + ws: + specifier: ^8.18.0 + version: 8.18.3 devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -62,6 +65,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.8) + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -128,9 +134,15 @@ importers: mediasoup: specifier: ^3.19.17 version: 3.19.17 + pdfkit: + specifier: ^0.15.0 + version: 0.15.2 socket.io: specifier: ^4.8.3 version: 4.8.3 + ws: + specifier: ^8.18.0 + version: 8.18.3 y-protocols: specifier: ^1.0.7 version: 1.0.7(yjs@13.6.29) @@ -150,6 +162,12 @@ importers: '@types/node': specifier: ^20 version: 20.19.28 + '@types/pdfkit': + specifier: ^0.13.6 + version: 0.13.9 + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 tsx: specifier: ^4.20.6 version: 4.21.0 @@ -932,6 +950,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.3.17': + resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1023,12 +1044,12 @@ packages: '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} - '@tanstack/hotkeys@0.1.1': - resolution: {integrity: sha512-l2sUODo27m3CI6G83J+PlW4EE4aK5cqcWAUSMY4khJS37P3tKEJReqTfSUT1xi+q5z6BRWI9fsudt2Fg4ugFiA==} + '@tanstack/hotkeys@0.1.3': + resolution: {integrity: sha512-16T+rq9niDG0QcMkG4wIDD2xBCl+dVLEiq5wvNeRpSvndb8jRSATRj1IydipTAA0t0+GT+zdQRTqgXU0Yp54mw==} engines: {node: '>=18'} - '@tanstack/react-hotkeys@0.1.1': - resolution: {integrity: sha512-NWWbowLbZO6vOD0QJYtgewuHy0wTU+CMZpcNojZRxNDau0elcebT+F1OYcstkp3H9cq/1GewpR1SehiyIjttpA==} + '@tanstack/react-hotkeys@0.1.3': + resolution: {integrity: sha512-UnMd2o/MEJB0QtvVVTqUK+6Klp/W3gTqWplg9GyFOpXb7LyHXKp2oyVQDvltezFUl+ugaf3U07ou4hAffcumKQ==} engines: {node: '>=18'} peerDependencies: react: '>=16.8' @@ -1109,6 +1130,9 @@ packages: '@types/node@20.19.28': resolution: {integrity: sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==} + '@types/pdfkit@0.13.9': + resolution: {integrity: sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1135,6 +1159,9 @@ 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==} @@ -1187,12 +1214,20 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + awaitqueue@3.3.0: resolution: {integrity: sha512-zLxDhzQbzHmOyvxi7g3OlfR7jLrcmElStPxfLPpJkrFSDm71RSrY/MvsDA8Btlx8X1nOHUzGhQvc6bdUjL2f2w==} engines: {node: '>=20'} @@ -1231,6 +1266,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1329,6 +1368,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1358,6 +1400,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -1409,6 +1455,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1465,6 +1515,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -1500,6 +1553,18 @@ packages: supports-color: optional: true + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -1515,6 +1580,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + docker-modem@5.0.6: resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} engines: {node: '>= 8.0'} @@ -1597,6 +1665,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1686,6 +1757,13 @@ packages: flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} + fontkit@1.9.0: + resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1716,6 +1794,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1741,7 +1822,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + 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 gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} @@ -1754,14 +1835,25 @@ packages: resolution: {integrity: sha512-hnq1UDlw7WGJV6GCr/g7wnkHYUjdAY2bis9rgn2JqSdQS2WfVvnt1ZE9g8nTguracodf5LLKZOwURsDN49YtBQ==} engines: {node: '>=20'} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1806,6 +1898,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -1813,6 +1909,30 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -1822,6 +1942,14 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1829,10 +1957,41 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1890,6 +2049,10 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jpeg-exif@1.1.4: + resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2006,6 +2169,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -2283,6 +2449,18 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -2310,6 +2488,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2329,6 +2510,9 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pdfkit@0.15.2: + resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2340,6 +2524,13 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + png-js@1.0.0: + resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -2430,6 +2621,10 @@ packages: regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2441,6 +2636,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restructure@2.0.1: + resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -2456,6 +2654,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2501,6 +2703,14 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2602,6 +2812,10 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2669,6 +2883,9 @@ packages: throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -2714,6 +2931,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2760,6 +2983,18 @@ packages: whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3494,6 +3729,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.3.17': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3567,13 +3806,13 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tanstack/hotkeys@0.1.1': + '@tanstack/hotkeys@0.1.3': dependencies: '@tanstack/store': 0.8.1 - '@tanstack/react-hotkeys@0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-hotkeys@0.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/hotkeys': 0.1.1 + '@tanstack/hotkeys': 0.1.3 '@tanstack/react-store': 0.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -3682,6 +3921,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pdfkit@0.13.9': + dependencies: + '@types/node': 20.19.28 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -3709,6 +3952,10 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.28 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -3754,12 +4001,21 @@ snapshots: dependencies: sprintf-js: 1.0.3 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + asap@2.0.6: {} asn1@0.2.6: dependencies: safer-buffer: 2.1.2 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + awaitqueue@3.3.0(supports-color@10.2.2): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -3792,7 +4048,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.28.6 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 @@ -3831,6 +4087,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} base64id@2.0.0: {} @@ -3900,6 +4158,10 @@ snapshots: dependencies: fill-range: 7.1.1 + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.14 @@ -3931,6 +4193,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3987,6 +4256,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@2.1.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4040,6 +4311,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -4069,6 +4342,39 @@ snapshots: optionalDependencies: supports-color: 10.2.2 + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + defu@6.1.4: {} depd@2.0.0: {} @@ -4077,6 +4383,8 @@ snapshots: detect-libc@2.1.2: {} + dfa@1.2.0: {} + docker-modem@5.0.6: dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -4187,6 +4495,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -4324,6 +4644,22 @@ snapshots: flow-enums-runtime@0.0.6: {} + fontkit@1.9.0: + dependencies: + '@swc/helpers': 0.3.17 + brotli: 1.3.3 + clone: 2.1.2 + deep-equal: 2.2.3 + dfa: 1.2.0 + restructure: 2.0.1 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -4343,6 +4679,8 @@ snapshots: function-bind@1.1.2: {} + functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -4390,10 +4728,20 @@ snapshots: transitivePeerDependencies: - supports-color + has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -4440,24 +4788,97 @@ snapshots: inherits@2.0.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + invariant@2.2.4: dependencies: loose-envify: 1.4.0 ipaddr.js@1.9.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-docker@2.2.1: {} is-fullwidth-code-point@3.0.0: {} + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-promise@4.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 + isarray@2.0.5: {} + isexe@2.0.0: {} isomorphic.js@0.2.5: {} @@ -4550,6 +4971,8 @@ snapshots: jose@6.1.3: {} + jpeg-exif@1.1.4: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -4651,6 +5074,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -4849,7 +5277,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/generator': 7.29.0 '@babel/parser': 7.29.0 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 metro: 0.83.3 metro-babel-transformer: 0.83.3 @@ -5012,6 +5440,22 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -5039,6 +5483,8 @@ snapshots: p-try@2.2.0: {} + pako@0.2.9: {} + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -5049,12 +5495,24 @@ snapshots: path-to-regexp@8.3.0: {} + pdfkit@0.15.2: + dependencies: + crypto-js: 4.2.0 + fontkit: 1.9.0 + jpeg-exif: 1.1.4 + linebreak: 1.1.0 + png-js: 1.0.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} pirates@4.0.7: {} + png-js@1.0.0: {} + + possible-typed-array-names@1.1.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -5207,12 +5665,23 @@ snapshots: regenerator-runtime@0.13.11: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + require-directory@2.1.1: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + restructure@2.0.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -5231,6 +5700,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} scheduler@0.25.0: {} @@ -5299,6 +5774,22 @@ snapshots: set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} sharp@0.34.5: @@ -5451,6 +5942,11 @@ snapshots: statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5522,6 +6018,8 @@ snapshots: throat@5.0.0: {} + tiny-inflate@1.0.3: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -5557,6 +6055,16 @@ snapshots: undici-types@6.21.0: {} + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unpipe@1.0.0: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -5589,6 +6097,31 @@ snapshots: whatwg-fetch@3.6.20: {} + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0