diff --git a/.gitignore b/.gitignore index a547bf3..438657a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env # Editor directories and files .vscode/* diff --git a/App.tsx b/App.tsx index 5a922a1..fba9bc8 100644 --- a/App.tsx +++ b/App.tsx @@ -1,16 +1,23 @@ -import React, { useState, useMemo } from 'react'; -import { UploadCloud, File as FileIcon, Loader2, Download, Layers, Users, X, CheckCircle2, FileText, Eye, UserPen } from 'lucide-react'; +import React, { useState, useMemo, useRef, useEffect } from 'react'; +import { UploadCloud, File as FileIcon, Loader2, Download, Layers, Users, X, CheckCircle2, FileText, Eye, UserPen, Save, FolderOpen, AlertTriangle, ArrowLeftRight, Wand2, Package } from 'lucide-react'; import { v4 as uuidv4 } from 'uuid'; import JSZip from 'jszip'; -import { ExtractedSignaturePage, GroupingMode, ProcessedDocument } from './types'; -import { getPageCount, renderPageToImage, generateGroupedPdfs, findSignaturePageCandidates, extractSinglePagePdf } from './services/pdfService'; -import { analyzePage } from './services/geminiService'; +import { ExtractedSignaturePage, GroupingMode, ProcessedDocument, SavedConfiguration, AppMode, ExecutedUpload, ExecutedSignaturePage, AssemblyMatch } from './types'; +import { getPageCount, renderPageToImage, generateGroupedPdfs, findSignaturePageCandidates, extractSinglePagePdf, assembleAllDocuments } from './services/pdfService'; +import { analyzePage, analyzeExecutedPage } from './services/geminiService'; +import { autoMatch, createManualMatch } from './services/matchingService'; +import { convertDocxToPdf } from './services/docxService'; import SignatureCard from './components/SignatureCard'; import PdfPreviewModal from './components/PdfPreviewModal'; import InstructionsModal from './components/InstructionsModal'; +import CompletionChecklist from './components/CompletionChecklist'; +import ExecutedPageCard from './components/ExecutedPageCard'; +import MatchPickerModal from './components/MatchPickerModal'; // Concurrency Constants for AI - Keeping AI limit per doc to avoid rate limits, but unlimited docs const CONCURRENT_AI_REQUESTS_PER_DOC = 5; +type SupportedSourceFormat = 'pdf' | 'docx'; +type NormalizedUpload = { sourceFile: File; pdfFile: File | null; errorMessage?: string }; const App: React.FC = () => { const [documents, setDocuments] = useState([]); @@ -20,6 +27,21 @@ const App: React.FC = () => { // Grouping & Filtering State const [groupingMode, setGroupingMode] = useState('agreement'); + // App Mode State + const [appMode, setAppMode] = useState('extract'); + + // Assembly State + const [executedUploads, setExecutedUploads] = useState([]); + const [assemblyMatches, setAssemblyMatches] = useState([]); + const [isDraggingExecuted, setIsDraggingExecuted] = useState(false); + + // Match Picker Modal State + const [matchPickerState, setMatchPickerState] = useState<{ + isOpen: boolean; + blankPageId: string | null; + currentMatch: AssemblyMatch | null; + }>({ isOpen: false, blankPageId: null, currentMatch: null }); + // Drag & Drop State const [isDragging, setIsDragging] = useState(false); @@ -33,40 +55,179 @@ const App: React.FC = () => { // Instructions Modal State const [isInstructionsOpen, setIsInstructionsOpen] = useState(false); + // Ref for load-config hidden file input + const loadConfigInputRef = useRef(null); + const replaceDocInputRef = useRef(null); + const [replaceTargetDocId, setReplaceTargetDocId] = useState(null); + + // Guard against duplicate restore runs (StrictMode double-invoke, rapid re-uploads) + const restoringIds = useRef>(new Set()); + // --- Handlers --- + const getSupportedSourceFormat = (file: File): SupportedSourceFormat | null => { + const lowerName = file.name.toLowerCase(); + if (file.type === 'application/pdf' || lowerName.endsWith('.pdf')) return 'pdf'; + if ( + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + lowerName.endsWith('.docx') + ) return 'docx'; + return null; + }; + + const normalizeUploadToPdf = async (file: File): Promise => { + const format = getSupportedSourceFormat(file); + if (!format) return null; + if (format === 'pdf') return file; + return convertDocxToPdf(file); + }; + + const getErrorMessage = (error: unknown, fallback: string): string => { + if (error instanceof Error && error.message) return error.message; + if (typeof error === 'string' && error.trim()) return error; + return fallback; + }; + const handleFileUpload = async (files: FileList | null) => { if (!files || files.length === 0) return; - // Check for API Key - if (!process.env.API_KEY) { - alert("API_KEY is missing from environment. Please provide a valid key."); - return; + const uploadedFiles = Array.from(files); + setCurrentStatus('Preparing uploads...'); + + const normalizedUploads: NormalizedUpload[] = await Promise.all(uploadedFiles.map(async (f) => { + try { + const pdfFile = await normalizeUploadToPdf(f); + if (!pdfFile) { + return { sourceFile: f, pdfFile: null, errorMessage: 'Unsupported file type. Please upload PDF or DOCX.' }; + } + return { sourceFile: f, pdfFile }; + } catch (error) { + console.error(`Failed to normalize file ${f.name}`, error); + return { sourceFile: f, pdfFile: null, errorMessage: getErrorMessage(error, 'DOCX conversion failed') }; + } + })); + setCurrentStatus(''); + + // Snapshot current docs before setState to find version updates by filename. + const versionUpdates: ProcessedDocument[] = []; + for (const normalized of normalizedUploads) { + if (!normalized.pdfFile) continue; + const matched = documents.find(d => d.name === normalized.pdfFile.name); + if (matched && !restoringIds.current.has(matched.id)) { + restoringIds.current.add(matched.id); + versionUpdates.push({ + ...matched, + file: normalized.pdfFile, + status: 'pending', + wasRestored: true, + savedPages: matched.extractedPages + }); + } } - const newDocs: ProcessedDocument[] = Array.from(files).map(f => { - // Validation: Strict PDF Check - const isPdf = f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); - - return { + setDocuments(prev => { + const updatedDocs = [...prev]; + const newDocs: ProcessedDocument[] = []; + + for (const normalized of normalizedUploads) { + const file = normalized.pdfFile; + const existingIdx = file ? updatedDocs.findIndex(d => d.name === file.name) : -1; + + if (existingIdx !== -1) { + updatedDocs[existingIdx] = { + ...updatedDocs[existingIdx], + file, + status: 'pending', + errorMessage: undefined, + wasRestored: true, + savedPages: updatedDocs[existingIdx].extractedPages, + }; + } else { + newDocs.push({ id: uuidv4(), - name: f.name, - file: f, + name: file?.name ?? normalized.sourceFile.name, + file, pageCount: 0, - status: isPdf ? 'pending' : 'error', - extractedPages: [] - }; + status: file ? 'pending' : 'error', + errorMessage: normalized.errorMessage, + extractedPages: [], + }); + } + } + + return [...updatedDocs, ...newDocs]; }); - setDocuments(prev => [...prev, ...newDocs]); - - // Process all valid pending docs immediately - // const validDocsToProcess = newDocs.filter(d => d.status === 'pending'); - // processAllDocuments(validDocsToProcess); + if (versionUpdates.length > 0) { + await processVersionUpdatedDocuments(versionUpdates); + versionUpdates.forEach(d => restoringIds.current.delete(d.id)); + } + }; + + const handleReplaceDocumentClick = (docId: string) => { + setReplaceTargetDocId(docId); + replaceDocInputRef.current?.click(); + }; + + const handleReplaceDocumentSelected = async (file: File | null) => { + if (!file || !replaceTargetDocId) { + if (replaceDocInputRef.current) replaceDocInputRef.current.value = ''; + setReplaceTargetDocId(null); + return; + } + + const targetDoc = documents.find(d => d.id === replaceTargetDocId); + if (!targetDoc) { + if (replaceDocInputRef.current) replaceDocInputRef.current.value = ''; + setReplaceTargetDocId(null); + return; + } + + setCurrentStatus(`Preparing replacement for '${targetDoc.name}'...`); + + let normalizedFile: File | null = null; + try { + normalizedFile = await normalizeUploadToPdf(file); + } catch (error) { + setCurrentStatus(getErrorMessage(error, 'Replacement conversion failed')); + setTimeout(() => setCurrentStatus(''), 3500); + if (replaceDocInputRef.current) replaceDocInputRef.current.value = ''; + setReplaceTargetDocId(null); + return; + } + + if (!normalizedFile) { + setCurrentStatus('Unsupported file type. Please upload PDF or DOCX.'); + setTimeout(() => setCurrentStatus(''), 3000); + if (replaceDocInputRef.current) replaceDocInputRef.current.value = ''; + setReplaceTargetDocId(null); + return; + } + + if (!restoringIds.current.has(targetDoc.id)) { + restoringIds.current.add(targetDoc.id); + } + + const versionUpdate: ProcessedDocument = { + ...targetDoc, + file: normalizedFile, + status: 'pending', + errorMessage: undefined, + wasRestored: true, + savedPages: targetDoc.extractedPages, + }; + + setDocuments(prev => prev.map(d => d.id === targetDoc.id ? versionUpdate : d)); + await processVersionUpdatedDocuments([versionUpdate]); + restoringIds.current.delete(targetDoc.id); + + if (replaceDocInputRef.current) replaceDocInputRef.current.value = ''; + setReplaceTargetDocId(null); }; const handleProcessPending = () => { - const pendingDocs = documents.filter(d => d.status === 'pending'); + // Only process truly new pending docs — restored ones auto-rescan via useEffect + const pendingDocs = documents.filter(d => d.status === 'pending' && d.file !== null && !d.wasRestored); processAllDocuments(pendingDocs); }; @@ -75,7 +236,7 @@ const App: React.FC = () => { */ const processAllDocuments = async (docsToProcess: ProcessedDocument[]) => { if (docsToProcess.length === 0) return; - + setIsProcessing(true); setCurrentStatus(`Processing ${docsToProcess.length} documents...`); @@ -86,9 +247,154 @@ const App: React.FC = () => { setCurrentStatus(''); }; + /** + * Re-process an updated version of existing documents while preserving prior + * extracted page edits. Existing extracted pages are retained by pageIndex; + * newly detected signature pages are appended. + */ + const processVersionUpdatedDocuments = async (updatedDocs: ProcessedDocument[]) => { + if (updatedDocs.length === 0) return; + + setIsProcessing(true); + setCurrentStatus(`Updating ${updatedDocs.length} document version${updatedDocs.length > 1 ? 's' : ''}...`); + + await Promise.all(updatedDocs.map(doc => processSingleDocumentWithMerge(doc))); + + setIsProcessing(false); + setCurrentStatus(''); + }; + + /** + * Re-processes a version-updated document: + * 1) preserves prior extracted pages by pageIndex (including user edits) + * 2) refreshes their thumbnails from the new file when page indices still exist + * 3) scans for newly added signature pages and appends them + */ + const processSingleDocumentWithMerge = async (doc: ProcessedDocument) => { + // savedPages was snapshotted onto the doc object at upload time, before we clear extractedPages + const savedPages: ExtractedSignaturePage[] = doc.savedPages ?? doc.extractedPages; + + setDocuments(prev => prev.map(d => d.id === doc.id ? { + ...d, + status: 'processing', + progress: 0, + errorMessage: undefined, + extractedPages: [], + savedPages: undefined + } : d)); + + try { + const file = doc.file!; + const pageCount = await getPageCount(file); + + // First pass: refresh thumbnails for known extracted pages + const uniquePageIndices = Array.from(new Set(savedPages.map(p => p.pageIndex))).sort((a, b) => a - b); + const totalKnown = uniquePageIndices.length; + const freshPages: ExtractedSignaturePage[] = []; + const knownIndexSet = new Set(uniquePageIndices); + + for (let i = 0; i < uniquePageIndices.length; i++) { + const pageIndex = uniquePageIndices[i]; + if (pageIndex >= pageCount) { + // Page no longer exists in latest version; keep prior extraction. + savedPages.filter(sp => sp.pageIndex === pageIndex).forEach(saved => freshPages.push(saved)); + } else { + try { + const { dataUrl, width, height } = await renderPageToImage(file, pageIndex); + const pagesAtIndex = savedPages.filter(sp => sp.pageIndex === pageIndex); + pagesAtIndex.forEach(saved => { + freshPages.push({ ...saved, thumbnailUrl: dataUrl, originalWidth: width, originalHeight: height }); + }); + } catch (err) { + console.error(`Error rendering page ${pageIndex} of ${doc.name}`, err); + // Keep saved page as-is if render fails (stale thumbnail better than nothing) + savedPages.filter(sp => sp.pageIndex === pageIndex).forEach(saved => freshPages.push(saved)); + } + } + const progressKnown = totalKnown === 0 ? 40 : Math.round(((i + 1) / totalKnown) * 40); + setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, progress: progressKnown } : d)); + } + + // Second pass: detect newly added signature pages + const candidateIndices = await findSignaturePageCandidates(file, (curr, total) => { + const progress = 40 + Math.round((curr / total) * 30); + setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, progress } : d)); + }); + const newCandidateIndices = candidateIndices.filter(pageIndex => !knownIndexSet.has(pageIndex)); + + if (newCandidateIndices.length > 0) { + let processedNew = 0; + const totalNew = newCandidateIndices.length; + + for (let i = 0; i < newCandidateIndices.length; i += CONCURRENT_AI_REQUESTS_PER_DOC) { + const chunk = newCandidateIndices.slice(i, i + CONCURRENT_AI_REQUESTS_PER_DOC); + const chunkPromises = chunk.map(async (pageIndex) => { + try { + const { dataUrl, width, height } = await renderPageToImage(file, pageIndex); + const analysis = await analyzePage(dataUrl); + if (analysis.isSignaturePage) { + return analysis.signatures.map(sig => ({ + id: uuidv4(), + documentId: doc.id, + documentName: doc.name, + pageIndex, + pageNumber: pageIndex + 1, + partyName: sig.partyName || "Unknown Party", + signatoryName: sig.signatoryName || "", + capacity: sig.capacity || "Signatory", + copies: 1, + thumbnailUrl: dataUrl, + originalWidth: width, + originalHeight: height + })); + } + } catch (err) { + console.error(`Error analyzing new page ${pageIndex} of ${doc.name}`, err); + } + return []; + }); + + const chunkResults = await Promise.all(chunkPromises); + chunkResults.flat().forEach(p => { if (p) freshPages.push(p); }); + processedNew += chunk.length; + const aiProgress = 70 + Math.round((processedNew / totalNew) * 30); + setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, progress: aiProgress } : d)); + } + } + + setDocuments(prev => prev.map(d => d.id === doc.id ? { + ...d, + status: 'completed', + progress: 100, + errorMessage: undefined, + pageCount, + extractedPages: freshPages, + wasRestored: undefined, + savedPages: undefined, + } : d)); + + restoringIds.current.delete(doc.id); + setCurrentStatus(`Updated '${doc.name}' — kept prior pages, added new detections`); + setTimeout(() => setCurrentStatus(''), 3000); + + } catch (error) { + console.error(`Error restoring ${doc.name}`, error); + restoringIds.current.delete(doc.id); + setDocuments(prev => prev.map(d => d.id === doc.id ? { + ...d, + status: 'error', + errorMessage: getErrorMessage(error, 'Failed to restore this document'), + wasRestored: undefined, + savedPages: undefined + } : d)); + } + }; + const processSingleDocument = async (doc: ProcessedDocument) => { + if (!doc.file) return; // Safety guard — should not happen for normal pending docs + // Update status to processing - setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, status: 'processing', progress: 0 } : d)); + setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, status: 'processing', progress: 0, errorMessage: undefined } : d)); try { const pageCount = await getPageCount(doc.file); @@ -159,13 +465,18 @@ const App: React.FC = () => { ...d, status: 'completed', progress: 100, + errorMessage: undefined, pageCount, extractedPages } : d)); } catch (error) { console.error(`Error processing doc ${doc.name}`, error); - setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, status: 'error' } : d)); + setDocuments(prev => prev.map(d => d.id === doc.id ? { + ...d, + status: 'error', + errorMessage: getErrorMessage(error, 'Failed to process this document') + } : d)); } }; @@ -208,6 +519,397 @@ const App: React.FC = () => { setDocuments(prev => prev.filter(d => d.id !== docId)); }; + // --- Save / Load Configuration --- + + const fileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result as string; + // Strip the data:...;base64, prefix to store raw base64 + resolve(result.split(',')[1]); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + + const handleSaveConfiguration = async () => { + const pages = documents.flatMap(d => d.extractedPages); + if (pages.length === 0) return; + + setIsProcessing(true); + setCurrentStatus('Bundling PDFs into config...'); + + try { + // Convert document PDFs to base64 + const docEntries = await Promise.all(documents.map(async ({ id, name, pageCount, file }) => { + const entry: { id: string; name: string; pageCount: number; pdfBase64?: string } = { id, name, pageCount }; + if (file) { + entry.pdfBase64 = await fileToBase64(file); + } + return entry; + })); + + // Convert executed upload PDFs to base64 + const execEntries = await Promise.all( + executedUploads + .filter(u => u.status === 'completed') + .map(async ({ id, fileName, pageCount, executedPages, file }) => { + const entry: { id: string; fileName: string; pageCount: number; executedPages: typeof executedPages; pdfBase64?: string } = { id, fileName, pageCount, executedPages }; + if (file) { + entry.pdfBase64 = await fileToBase64(file); + } + return entry; + }) + ); + + const config: SavedConfiguration = { + version: 1, + savedAt: new Date().toISOString(), + groupingMode, + documents: docEntries, + extractedPages: pages, + executedUploads: execEntries, + assemblyMatches, + }; + + const blob = new Blob([JSON.stringify(config)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `SignatureConfig_${new Date().toISOString().slice(0, 10)}.json`; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } catch (e) { + console.error('Error saving configuration:', e); + alert('Failed to save configuration.'); + } finally { + setIsProcessing(false); + setCurrentStatus(''); + } + }; + + const base64ToFile = (base64: string, fileName: string): File => { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new File([bytes], fileName, { type: 'application/pdf' }); + }; + + const handleLoadConfiguration = (file: File | null) => { + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const raw = e.target?.result as string; + const config = JSON.parse(raw) as SavedConfiguration; + + // Basic validation + if (config.version !== 1 || !Array.isArray(config.documents) || !Array.isArray(config.extractedPages)) { + alert('Invalid configuration file.'); + return; + } + + // Build restored documents, distributing extractedPages back by documentId + const pagesByDocId = new Map(); + for (const page of config.extractedPages) { + const arr = pagesByDocId.get(page.documentId) ?? []; + arr.push(page); + pagesByDocId.set(page.documentId, arr); + } + + const hasBundledPdfs = config.documents.some(d => !!d.pdfBase64); + + const restoredDocs: ProcessedDocument[] = config.documents.map(d => { + const pdfFile = d.pdfBase64 ? base64ToFile(d.pdfBase64, d.name) : null; + return { + id: d.id, + name: d.name, + file: pdfFile, + pageCount: d.pageCount, + status: pdfFile ? 'completed' as const : 'restored' as const, + extractedPages: pagesByDocId.get(d.id) ?? [], + }; + }); + + setDocuments(restoredDocs); + setGroupingMode(config.groupingMode); + + // Restore assembly state if present + if (config.executedUploads && config.executedUploads.length > 0) { + const restoredUploads: ExecutedUpload[] = config.executedUploads.map(u => ({ + ...u, + file: u.pdfBase64 ? base64ToFile(u.pdfBase64, u.fileName) : null as unknown as File, + status: 'completed' as const, + })); + setExecutedUploads(restoredUploads); + } + if (config.assemblyMatches && config.assemblyMatches.length > 0) { + setAssemblyMatches(config.assemblyMatches); + } + + if (hasBundledPdfs) { + setCurrentStatus('Configuration loaded with bundled PDFs'); + } else { + setCurrentStatus('Configuration loaded — re-upload PDFs to enable pack download'); + } + setTimeout(() => setCurrentStatus(''), 4000); + } catch { + alert('Could not read configuration file. Make sure it is a valid Signature Packet IDE JSON.'); + } + }; + reader.readAsText(file); + + // Reset so the same file can be re-loaded later + if (loadConfigInputRef.current) loadConfigInputRef.current.value = ''; + }; + + // --- Assembly Mode Handlers --- + + const allExecutedPages = useMemo(() => { + return executedUploads.flatMap(u => u.executedPages); + }, [executedUploads]); + + const handleExecutedFileUpload = async (files: FileList | null) => { + if (!files || files.length === 0) return; + + const uploadedFiles = Array.from(files); + setCurrentStatus('Preparing executed uploads...'); + + const normalizedUploads: NormalizedUpload[] = await Promise.all(uploadedFiles.map(async (f) => { + try { + const pdfFile = await normalizeUploadToPdf(f); + if (!pdfFile) { + return { sourceFile: f, pdfFile: null, errorMessage: 'Unsupported file type. Please upload PDF or DOCX.' }; + } + return { sourceFile: f, pdfFile }; + } catch (error) { + console.error(`Failed to normalize executed file ${f.name}`, error); + return { sourceFile: f, pdfFile: null, errorMessage: getErrorMessage(error, 'DOCX conversion failed') }; + } + })); + setCurrentStatus(''); + + // Create ExecutedUpload entries + const newUploads: ExecutedUpload[] = normalizedUploads + .map(item => ({ + id: uuidv4(), + file: item.pdfFile ?? item.sourceFile, + fileName: (item.pdfFile?.name ?? item.sourceFile.name), + pageCount: 0, + status: item.pdfFile ? 'pending' as const : 'error' as const, + errorMessage: item.errorMessage, + executedPages: [], + })) + .filter(u => u.status === 'pending' || u.status === 'error'); + + // Check for duplicate filenames + const existingNames = new Set(executedUploads.map(u => `${u.fileName}_${u.file.size}`)); + const deduped = newUploads.filter(u => { + const key = `${u.fileName}_${u.file.size}`; + if (existingNames.has(key)) { + console.warn(`Skipping duplicate executed upload: ${u.fileName}`); + return false; + } + return true; + }); + + if (deduped.length === 0) return; + + setExecutedUploads(prev => [...prev, ...deduped]); + + // Process each upload + for (const upload of deduped.filter(u => u.status === 'pending')) { + await processExecutedUpload(upload); + } + }; + + const processExecutedUpload = async (upload: ExecutedUpload) => { + setExecutedUploads(prev => prev.map(u => u.id === upload.id ? { ...u, status: 'processing', progress: 0, errorMessage: undefined } : u)); + + try { + const pageCount = await getPageCount(upload.file); + const executedPages: ExecutedSignaturePage[] = []; + + for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) { + try { + const { dataUrl, width, height } = await renderPageToImage(upload.file, pageIndex); + const analysis = await analyzeExecutedPage(dataUrl); + + if (analysis.signatures.length > 0) { + // Create an ExecutedSignaturePage for each signature block found + for (const sig of analysis.signatures) { + executedPages.push({ + id: uuidv4(), + sourceUploadId: upload.id, + sourceFileName: upload.fileName, + pageIndexInSource: pageIndex, + pageNumber: pageIndex + 1, + extractedDocumentName: analysis.documentName || '', + extractedPartyName: sig.partyName || '', + extractedSignatoryName: sig.signatoryName || '', + extractedCapacity: sig.capacity || '', + isConfirmedExecuted: analysis.isExecuted, + thumbnailUrl: dataUrl, + originalWidth: width, + originalHeight: height, + matchedBlankPageId: null, + matchConfidence: null, + }); + } + } else { + // No signatures found, but still create an entry for the page + executedPages.push({ + id: uuidv4(), + sourceUploadId: upload.id, + sourceFileName: upload.fileName, + pageIndexInSource: pageIndex, + pageNumber: pageIndex + 1, + extractedDocumentName: analysis.documentName || '', + extractedPartyName: '', + extractedSignatoryName: '', + extractedCapacity: '', + isConfirmedExecuted: analysis.isExecuted, + thumbnailUrl: dataUrl, + originalWidth: width, + originalHeight: height, + matchedBlankPageId: null, + matchConfidence: null, + }); + } + } catch (err) { + console.error(`Error processing executed page ${pageIndex} of ${upload.fileName}`, err); + } + + // Update progress + const progress = Math.round(((pageIndex + 1) / pageCount) * 100); + setExecutedUploads(prev => prev.map(u => u.id === upload.id ? { ...u, progress } : u)); + } + + setExecutedUploads(prev => prev.map(u => u.id === upload.id ? { + ...u, + status: 'completed', + progress: 100, + errorMessage: undefined, + pageCount, + executedPages, + } : u)); + + } catch (error) { + console.error(`Error processing executed upload ${upload.fileName}`, error); + setExecutedUploads(prev => prev.map(u => u.id === upload.id ? { + ...u, + status: 'error', + errorMessage: getErrorMessage(error, 'Failed to process this executed upload') + } : u)); + } + }; + + const handleAutoMatch = () => { + const newMatches = autoMatch(allPages, allExecutedPages, assemblyMatches); + if (newMatches.length === 0) { + setCurrentStatus('No new matches found'); + setTimeout(() => setCurrentStatus(''), 2000); + return; + } + + // Merge: keep existing confirmed/overridden matches, replace auto-matches, add new ones + setAssemblyMatches(prev => { + const preserved = prev.filter(m => m.status === 'user-confirmed' || m.status === 'user-overridden'); + return [...preserved, ...newMatches]; + }); + + setCurrentStatus(`Auto-matched ${newMatches.length} page${newMatches.length > 1 ? 's' : ''}`); + setTimeout(() => setCurrentStatus(''), 3000); + }; + + const handleManualMatch = (blankPageId: string, executedPageId: string) => { + const blank = allPages.find(p => p.id === blankPageId); + const executed = allExecutedPages.find(p => p.id === executedPageId); + if (!blank || !executed) return; + + const match = createManualMatch(blank, executed); + + setAssemblyMatches(prev => { + // Remove any existing match for this blank page + const filtered = prev.filter(m => m.blankPageId !== blankPageId); + return [...filtered, match]; + }); + }; + + const handleUnmatch = (blankPageId: string) => { + setAssemblyMatches(prev => prev.filter(m => m.blankPageId !== blankPageId)); + }; + + const handleUnmatchByExecutedId = (executedPageId: string) => { + setAssemblyMatches(prev => prev.filter(m => m.executedPageId !== executedPageId)); + }; + + const handleChecklistCellClick = (blankPageId: string, currentMatch: AssemblyMatch | null) => { + setMatchPickerState({ + isOpen: true, + blankPageId, + currentMatch, + }); + }; + + const handleAssembleDocuments = async () => { + if (assemblyMatches.length === 0) return; + + // Warn about unmatched pages + const unmatchedCount = allPages.length - assemblyMatches.length; + if (unmatchedCount > 0) { + const proceed = window.confirm( + `${unmatchedCount} signature page${unmatchedCount > 1 ? 's' : ''} still unmatched. ` + + `Unmatched pages will keep the original (blank) signature page in the assembled document. Continue?` + ); + if (!proceed) return; + } + + setIsProcessing(true); + setCurrentStatus('Assembling documents...'); + + try { + const assembledPdfs = await assembleAllDocuments(documents, assemblyMatches, executedUploads); + + const zip = new JSZip(); + for (const [filename, data] of Object.entries(assembledPdfs)) { + zip.file(filename, data); + } + + const zipContent = await zip.generateAsync({ type: 'blob' }); + const url = window.URL.createObjectURL(zipContent); + const link = document.createElement('a'); + link.href = url; + link.download = `Assembled_Documents_${new Date().toISOString().slice(0, 10)}.zip`; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + + } catch (e) { + console.error('Assembly error:', e); + alert('Failed to assemble documents'); + } finally { + setIsProcessing(false); + setCurrentStatus(''); + } + }; + + const removeExecutedUpload = (uploadId: string) => { + // Also remove any matches that reference pages from this upload + setExecutedUploads(prev => { + const upload = prev.find(u => u.id === uploadId); + if (upload) { + const pageIds = new Set(upload.executedPages.map(p => p.id)); + setAssemblyMatches(matches => matches.filter(m => !pageIds.has(m.executedPageId))); + } + return prev.filter(u => u.id !== uploadId); + }); + }; + // --- Preview Logic --- const openPreview = (url: string, title: string) => { @@ -222,7 +924,7 @@ const App: React.FC = () => { }; const handlePreviewDocument = async (doc: ProcessedDocument) => { - if (doc.status === 'error') return; // Don't preview errored files + if (doc.status === 'error' || doc.status === 'restored' || !doc.file) return; const url = URL.createObjectURL(doc.file); openPreview(url, doc.name); }; @@ -230,7 +932,7 @@ const App: React.FC = () => { const handlePreviewSignaturePage = async (page: ExtractedSignaturePage) => { // Find the original document file const parentDoc = documents.find(d => d.id === page.documentId); - if (!parentDoc) return; + if (!parentDoc || !parentDoc.file) return; try { const pdfBytes = await extractSinglePagePdf(parentDoc.file, page.pageIndex); @@ -353,6 +1055,27 @@ const App: React.FC = () => { pages={displayedPages} /> + {/* Match Picker Modal */} + setMatchPickerState({ isOpen: false, blankPageId: null, currentMatch: null })} + blankPage={allPages.find(p => p.id === matchPickerState.blankPageId) || null} + currentMatch={matchPickerState.currentMatch} + executedPages={allExecutedPages} + allMatches={assemblyMatches} + onConfirmMatch={handleManualMatch} + onUnmatch={handleUnmatch} + /> + + {/* Replace Version Input */} + handleReplaceDocumentSelected(e.target.files?.[0] ?? null)} + /> + {/* Header */}
@@ -368,11 +1091,37 @@ const App: React.FC = () => { {documents.length} Docs {allPages.length} Sig Pages Found
+ {/* Save / Load Config */} +
+ handleLoadConfiguration(e.target.files?.[0] ?? null)} + /> + + +
-
- +
+ {/* Sidebar: Documents */}
@@ -390,7 +1139,7 @@ const App: React.FC = () => { handleFileUpload(e.target.files)} @@ -398,7 +1147,7 @@ const App: React.FC = () => {
@@ -414,43 +1163,76 @@ const App: React.FC = () => {
{documents.map(doc => ( -
-
+
+
-

{doc.name}

+

{doc.name}

{doc.status === 'processing' && <> Processing...} {doc.status === 'completed' && <> {doc.extractedPages.length} sig pages} - {doc.status === 'error' && PDF only} + {doc.status === 'error' && ( + + {doc.errorMessage || 'PDF or DOCX only'} + + )} {doc.status === 'pending' && 'Queued'} + {doc.status === 'restored' && ( + + Needs file + + )}
+ {doc.status === 'restored' && ( + {doc.extractedPages.length} sig pages (saved) + )} {doc.status === 'processing' && doc.progress !== undefined && (
-
)}
- + {/* Document Actions */}
- {doc.status !== 'error' && ( - )} - + )} +
)} + + {/* Executed Uploads Section (Assembly Mode) */} + {appMode === 'assembly' && ( + <> +
+
+

Executed Pages

+
{ e.preventDefault(); setIsDraggingExecuted(true); }} + onDragLeave={() => setIsDraggingExecuted(false)} + onDrop={(e) => { + e.preventDefault(); + setIsDraggingExecuted(false); + handleExecutedFileUpload(e.dataTransfer.files); + }} + > + handleExecutedFileUpload(e.target.files)} + /> + +
+
+ + {/* Executed uploads list */} + {executedUploads.map(upload => ( +
+
+ +
+
+

{upload.fileName}

+
+
+ {upload.status === 'processing' && <> Analyzing...} + {upload.status === 'completed' && <> {upload.executedPages.filter(p => p.isConfirmedExecuted).length} signed pages} + {upload.status === 'error' && ( + + {upload.errorMessage || 'Error'} + + )} + {upload.status === 'pending' && 'Queued'} +
+ {upload.status === 'processing' && upload.progress !== undefined && ( +
+
+
+ )} +
+
+
+ +
+
+ ))} + + )}
{/* Main Content: Review Grid */} -
+
{/* Toolbar */}
+ {/* Mode Toggle */}
- - - + +
+ + {/* Grouping toggle (only in extract mode) */} + {appMode === 'extract' && ( +
+ + + +
+ )}
{/* Content Area with Nav */} -
+
{/* Grid Area */} -
- - {displayedPages.length === 0 ? ( -
-
- -
-

No signature pages found yet

-

Upload agreements (PDF) to begin extraction.

-
+
+ + {appMode === 'extract' ? ( + // --- Extract Mode Content --- + <> + {displayedPages.length === 0 ? ( +
+
+ +
+

No signature pages found yet

+

Upload agreements (PDF or DOCX) to begin extraction.

+
+ ) : ( +
+ {/* Render grouping headers based on current mode */} + {displayedPages.reduce((acc: React.ReactNode[], page, idx, arr) => { + const prev = arr[idx-1]; + let shouldInsertHeader = false; + let headerText = ''; + let HeaderIcon = Layers; + + if (groupingMode === 'agreement') { + shouldInsertHeader = !prev || prev.documentName !== page.documentName; + headerText = page.documentName; + HeaderIcon = FileText; + } else if (groupingMode === 'counterparty') { + shouldInsertHeader = !prev || prev.partyName !== page.partyName; + headerText = page.partyName; + HeaderIcon = Users; + } else { + // Signatory + const currentSig = page.signatoryName || 'Unknown Signatory'; + const prevSig = prev?.signatoryName || 'Unknown Signatory'; + shouldInsertHeader = !prev || prevSig !== currentSig; + headerText = currentSig; + HeaderIcon = UserPen; + } + + if (shouldInsertHeader) { + const headerId = `group-${headerText.replace(/[^a-zA-Z0-9]/g, '_')}`; + acc.push( +
+ +

{headerText}

+
+ ); + } + + acc.push( + p !== 'All')} + onUpdateCopies={handleUpdateCopies} + onUpdateParty={handleUpdateParty} + onUpdateSignatory={handleUpdateSignatory} + onUpdateCapacity={handleUpdateCapacity} + onDelete={handleDeletePage} + onPreview={handlePreviewSignaturePage} + /> + ); + return acc; + }, [])} +
+ )} + ) : ( -
- {/* Render grouping headers based on current mode */} - {displayedPages.reduce((acc: React.ReactNode[], page, idx, arr) => { - const prev = arr[idx-1]; - let shouldInsertHeader = false; - let headerText = ''; - let HeaderIcon = Layers; - - if (groupingMode === 'agreement') { - shouldInsertHeader = !prev || prev.documentName !== page.documentName; - headerText = page.documentName; - HeaderIcon = FileText; - } else if (groupingMode === 'counterparty') { - shouldInsertHeader = !prev || prev.partyName !== page.partyName; - headerText = page.partyName; - HeaderIcon = Users; - } else { - // Signatory - const currentSig = page.signatoryName || 'Unknown Signatory'; - const prevSig = prev?.signatoryName || 'Unknown Signatory'; - shouldInsertHeader = !prev || prevSig !== currentSig; - headerText = currentSig; - HeaderIcon = UserPen; - } - - if (shouldInsertHeader) { - const headerId = `group-${headerText.replace(/[^a-zA-Z0-9]/g, '_')}`; - acc.push( -
- -

{headerText}

-
- ); - } - - acc.push( - p !== 'All')} - onUpdateCopies={handleUpdateCopies} - onUpdateParty={handleUpdateParty} - onUpdateSignatory={handleUpdateSignatory} - onUpdateCapacity={handleUpdateCapacity} - onDelete={handleDeletePage} - onPreview={handlePreviewSignaturePage} - /> - ); - return acc; - }, [])} -
+ // --- Assembly Mode Content --- +
+ {/* Completion Checklist Grid */} + + + {/* Executed Pages Cards */} + {allExecutedPages.length > 0 && ( +
+

+ + Uploaded Executed Pages ({allExecutedPages.filter(p => p.isConfirmedExecuted).length} signed) +

+
+ {allExecutedPages.map(ep => ( + m.executedPageId === ep.id) || null} + onUnmatch={handleUnmatchByExecutedId} + /> + ))} +
+
+ )} + + {allExecutedPages.length === 0 && ( +
+
+ +
+

No executed pages yet

+

Upload signed PDFs or DOCX files in the sidebar to begin matching.

+
+ )} +
)}
- {/* Right Nav Rail */} - {displayedPages.length > 0 && ( + {/* Right Nav Rail (Extract mode only) */} + {appMode === 'extract' && displayedPages.length > 0 && (

Jump to {groupingMode === 'counterparty' ? 'Party' : groupingMode === 'signatory' ? 'Signatory' : 'Agreement'} @@ -576,7 +1506,7 @@ const App: React.FC = () => {
    {navigationGroups.map(g => (
  • -

- {/* Floating Action Bar */} - {displayedPages.length > 0 && ( + {/* Floating Action Bar — Extract Mode */} + {appMode === 'extract' && displayedPages.length > 0 && (
-
-
)} + {/* Floating Action Bar — Assembly Mode */} + {appMode === 'assembly' && allExecutedPages.length > 0 && ( +
+ +
+ +
+ )} + {/* Status Toast */} {currentStatus && (
diff --git a/README.md b/README.md index 7785943..14de804 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ https://github.com/user-attachments/assets/1b2ab8e4-0129-4319-aa13-05d31d714266 ## Features +### Extraction - **AI-Powered Extraction**: Uses Google Gemini 2.5 Flash to visually identify signature pages and extract: - **Party Name** (Entity bound by the contract) - **Signatory Name** (Human signing the document) @@ -18,12 +19,24 @@ https://github.com/user-attachments/assets/1b2ab8e4-0129-4319-aa13-05d31d714266 - **Agreement** (e.g., all pages for the SPA) - **Counterparty** (e.g., all pages for "Acme Corp" across all docs) - **Signatory** (e.g., all pages "Jane Smith" needs to sign) -- **Privacy-First**: Documents are processed in-memory. No file storage persistence. -- **Batch Processing**: Upload multiple transaction documents (PDF) at once. +- **Batch Processing**: Upload multiple transaction documents (PDF or DOCX) at once. +- **DOCX Conversion**: `.docx` uploads are converted to PDF through a server-side converter endpoint backed by Microsoft Graph (M365) to preserve layout fidelity. - **Integrated Preview**: View original PDFs and extracted signature pages instantly. -- **Automatic Instructions**: Generates a clear signing table/instruction sheet for clients. +- **Automatic Instructions**: Generates a per-signatory signing table (with party and capacity) to send to your client. - **Print-Ready Export**: Downloads a ZIP file containing perfectly sorted PDF packets for each party or agreement. +### Document Assembly +- **Executed Page Matching**: Upload signed/executed PDFs and let the AI identify which signature pages they correspond to. +- **Auto-Match**: Automatically matches executed pages to blank signature pages by document name, party, and signatory. +- **Assembly Progress Grid**: Visual checklist organized by signatory (columns) and document (rows) showing match status at a glance. Columns are drag-to-reorder. +- **Manual Override**: Click any cell to manually assign or reassign an executed page. +- **Assemble & Download**: Produces final assembled PDFs with blank signature pages swapped for their executed counterparts. + +### Configuration +- **Save/Load Config**: Save your entire session (extracted pages, edits, assembly matches) to a `.json` file. +- **Bundled PDFs**: Saved configs embed the original PDF files so you can restore a full session without re-uploading anything. +- **Privacy-First**: PDF extraction and matching happen in-browser. If DOCX upload is enabled, DOCX files are sent only to your configured conversion endpoint. + ## Tech Stack - **Frontend**: React 19, Tailwind CSS, Lucide Icons @@ -35,7 +48,7 @@ https://github.com/user-attachments/assets/1b2ab8e4-0129-4319-aa13-05d31d714266 1. **Clone the repository**: ```bash - git clone https://github.com/yourusername/signature-packet-ide.git + git clone https://github.com/jamietso/signature-packet-ide.git cd signature-packet-ide ``` @@ -45,26 +58,70 @@ https://github.com/user-attachments/assets/1b2ab8e4-0129-4319-aa13-05d31d714266 ``` 3. **Environment Configuration**: - Create a `.env` file in the root directory and add your Google Gemini API Key: + Create a `.env` file in the root directory and add your Google Gemini API Key. + For DOCX conversion via M365, add Microsoft Graph app credentials: ```env - API_KEY=your_google_gemini_api_key_here + GEMINI_API_KEY=your_google_gemini_api_key_here + M365_TENANT_ID=your_microsoft_tenant_id + M365_CLIENT_ID=your_app_registration_client_id + M365_CLIENT_SECRET=your_app_registration_client_secret + M365_USER_ID=user-object-id-or-upn-for-conversion-drive + # Optional temporary folder in that user's OneDrive + M365_UPLOAD_FOLDER=SignaturePacketIDE-Temp + # Optional (defaults to /api/docx-to-pdf; works with Vite proxy) + VITE_DOCX_CONVERTER_URL=/api/docx-to-pdf + # Optional backend port (default 8787) + DOCX_CONVERTER_PORT=8787 ``` -4. **Run the application**: +4. **Run the full stack (frontend + DOCX converter backend)**: ```bash - npm start + npm run dev:full ``` ## Usage Guide -1. **Upload**: Drag and drop your transaction documents (PDFs) into the sidebar. -2. **Review**: The AI will extract signature pages. Review the "Party", "Signatory", and "Capacity" fields in the card view. +### DOCX Conversion Endpoint Contract +- Method: `POST` +- URL: `VITE_DOCX_CONVERTER_URL` (defaults to `/api/docx-to-pdf`) +- Request: `multipart/form-data` with a `file` field containing `.docx` +- Response: `200` with `Content-Type: application/pdf` and raw PDF bytes +- Auth: this app sends `credentials: include`, so cookie/session-based auth is supported + +### Local backend +- Backend entrypoint: `backend/server.ts` +- Health check: `GET /api/health` +- Converter route: `POST /api/docx-to-pdf` +- Uses Microsoft Graph conversion (`.../content?format=pdf`) for high-fidelity Office-to-PDF rendering. + +### M365 permissions +- Register an app in Azure/Microsoft Entra and create a client secret. +- Add Microsoft Graph **Application** permissions: + - `Files.ReadWrite.All` (for upload + conversion + cleanup) +- Grant admin consent for your tenant. +- Set `M365_USER_ID` to a user/service account whose OneDrive will hold temporary uploads. + +### Extract Mode +1. **Upload**: Drag and drop your transaction documents (PDFs or DOCX files) into the sidebar, then click **Extract**. +2. **Review**: The AI will identify signature pages. Review the "Party", "Signatory", and "Capacity" fields for each page. 3. **Adjust**: - Use the **Grouping Toggles** (Agreement / Party / Signatory) to change how pages are sorted. - Edit the **Copies** counter if a party needs to sign multiple originals. -4. **Instructions**: Click "Instructions" to view and copy a signing table to send to your client. +4. **Instructions**: Click "Instructions" to view and copy a per-signatory signing table to send to your client. 5. **Download**: Click "Download ZIP" to get the organized PDF packets. +### Assembly Mode +1. Switch to the **Assembly** tab in the toolbar. +2. **Upload Signed Pages**: Drop executed/scanned PDFs or DOCX files into the "Executed Pages" section of the sidebar. +3. **Auto-Match**: Click **Auto-Match** to let the AI match executed pages to their corresponding blank signature pages. +4. **Review**: The Assembly Progress grid shows each document (rows) × signatory (columns). Green = matched, amber = pending. +5. **Manual Override**: Click any cell to manually assign or reassign a page. +6. **Assemble & Download**: Click **Assemble & Download** to generate final PDFs with executed pages inserted. + +### Save & Restore +- Click **Save Config** at any time to export your session (including all PDFs) to a `.json` file. +- Click **Load Config** to restore a previous session instantly — no re-uploading required. + ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. diff --git a/backend/server.ts b/backend/server.ts new file mode 100644 index 0000000..98fa3d6 --- /dev/null +++ b/backend/server.ts @@ -0,0 +1,150 @@ +import 'dotenv/config'; +import express from 'express'; +import multer from 'multer'; +import { randomUUID } from 'node:crypto'; + +const app = express(); +const port = Number(process.env.DOCX_CONVERTER_PORT || 8787); +const graphBaseUrl = process.env.M365_GRAPH_BASE_URL || 'https://graph.microsoft.com/v1.0'; + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 30 * 1024 * 1024 }, +}); + +const requiredEnv = ['M365_TENANT_ID', 'M365_CLIENT_ID', 'M365_CLIENT_SECRET', 'M365_USER_ID'] as const; + +const requireConfig = () => { + const missing = requiredEnv.filter((key) => !process.env[key]?.trim()); + if (missing.length > 0) { + throw new Error(`Missing M365 converter config: ${missing.join(', ')}`); + } + return { + tenantId: process.env.M365_TENANT_ID!, + clientId: process.env.M365_CLIENT_ID!, + clientSecret: process.env.M365_CLIENT_SECRET!, + userId: process.env.M365_USER_ID!, + folder: process.env.M365_UPLOAD_FOLDER || 'SignaturePacketIDE-Temp', + }; +}; + +const getGraphToken = async (): Promise => { + const { tenantId, clientId, clientSecret } = requireConfig(); + const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + const body = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret, + scope: 'https://graph.microsoft.com/.default', + }); + + const response = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }); + + if (!response.ok) { + const detail = await response.text(); + throw new Error(`Token request failed (${response.status}): ${detail}`); + } + + const json = await response.json() as { access_token?: string }; + if (!json.access_token) { + throw new Error('Token response missing access_token'); + } + + return json.access_token; +}; + +const buildUserDrivePathUrl = (userId: string, folder: string, fileName: string): string => { + const safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, '_'); + const fullPath = `${folder}/${randomUUID()}-${safeName}`; + return `${graphBaseUrl}/users/${encodeURIComponent(userId)}/drive/root:/${fullPath}:/content`; +}; + +app.get('/api/health', (_req, res) => { + res.status(200).json({ ok: true }); +}); + +app.post('/api/docx-to-pdf', upload.single('file'), async (req, res) => { + try { + const uploaded = req.file; + if (!uploaded) { + res.status(400).json({ error: 'No file uploaded. Use multipart/form-data with field "file".' }); + return; + } + + const looksLikeDocx = + uploaded.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + uploaded.originalname.toLowerCase().endsWith('.docx'); + + if (!looksLikeDocx) { + res.status(400).json({ error: 'Only .docx files are supported for this endpoint.' }); + return; + } + + const token = await getGraphToken(); + const { userId, folder } = requireConfig(); + const uploadUrl = buildUserDrivePathUrl(userId, folder, uploaded.originalname); + + // 1) Upload DOCX to service account OneDrive + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': uploaded.mimetype || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }, + body: uploaded.buffer, + }); + + if (!uploadResponse.ok) { + const detail = await uploadResponse.text(); + throw new Error(`Graph upload failed (${uploadResponse.status}): ${detail}`); + } + + const uploadedItem = await uploadResponse.json() as { id?: string }; + if (!uploadedItem.id) { + throw new Error('Graph upload response missing item id'); + } + + // 2) Request converted content as PDF + const convertUrl = + `${graphBaseUrl}/users/${encodeURIComponent(userId)}/drive/items/${encodeURIComponent(uploadedItem.id)}/content?format=pdf`; + const pdfResponse = await fetch(convertUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!pdfResponse.ok) { + const detail = await pdfResponse.text(); + throw new Error(`Graph conversion failed (${pdfResponse.status}): ${detail}`); + } + + const pdfBuffer = Buffer.from(await pdfResponse.arrayBuffer()); + + // 3) Best-effort cleanup of temp file + const deleteUrl = `${graphBaseUrl}/users/${encodeURIComponent(userId)}/drive/items/${encodeURIComponent(uploadedItem.id)}`; + void fetch(deleteUrl, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }).catch((cleanupErr) => { + console.warn('Cleanup warning:', cleanupErr); + }); + + const safeName = uploaded.originalname.replace(/\.docx$/i, '.pdf'); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `inline; filename="${safeName}"`); + res.status(200).send(pdfBuffer); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown conversion error'; + console.error('DOCX conversion failed:', message); + res.status(500).json({ error: 'DOCX conversion failed', detail: message }); + } +}); + +app.listen(port, () => { + console.log(`DOCX converter API listening on http://localhost:${port}`); +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..f3ac492 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["./**/*.ts"] +} diff --git a/components/CompletionChecklist.tsx b/components/CompletionChecklist.tsx new file mode 100644 index 0000000..f75d417 --- /dev/null +++ b/components/CompletionChecklist.tsx @@ -0,0 +1,375 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { ExtractedSignaturePage, AssemblyMatch, ExecutedSignaturePage } from '../types'; +import { CheckCircle2, AlertTriangle, Minus, Printer, ChevronLeft, ChevronRight } from 'lucide-react'; + +interface CompletionChecklistProps { + blankPages: ExtractedSignaturePage[]; + matches: AssemblyMatch[]; + executedPages: ExecutedSignaturePage[]; + onCellClick: (blankPageId: string, currentMatch: AssemblyMatch | null) => void; +} + +interface CellData { + blankPage: ExtractedSignaturePage; + match: AssemblyMatch | null; +} + +const CompletionChecklist: React.FC = ({ + blankPages, + matches, + executedPages, + onCellClick, +}) => { + // Build unique document names (rows) + const docNameSet = new Set(); + blankPages.forEach(p => docNameSet.add(p.documentName)); + const documentNames = Array.from(docNameSet).sort(); + + // Build unique signatory columns — one column per signatory regardless of party + const initialSignatoryNames = Array.from(new Set(blankPages.map(p => p.signatoryName || '(Unnamed)'))).sort(); + const [signatoryOrder, setSignatoryOrder] = useState(initialSignatoryNames); + // Sync if the set of signatories changes (e.g. new pages loaded) + const prevSignatories = useRef(JSON.stringify(initialSignatoryNames)); + if (prevSignatories.current !== JSON.stringify(initialSignatoryNames)) { + prevSignatories.current = JSON.stringify(initialSignatoryNames); + setSignatoryOrder(initialSignatoryNames); + } + const signatoryNames = signatoryOrder; + + // Party names per signatory for header sub-labels + const partiesBySignatory = (sigName: string): string[] => + Array.from(new Set(blankPages + .filter(p => (p.signatoryName || '(Unnamed)') === sigName) + .map(p => p.partyName) + )).sort(); + + // Drag state + const dragIndex = useRef(null); + const scrollContainerRef = useRef(null); + const tableSizerRef = useRef(null); + const [maxScrollLeft, setMaxScrollLeft] = useState(0); + const [scrollLeft, setScrollLeft] = useState(0); + const TABLE_DOC_COL_WIDTH = 260; + const TABLE_SIG_COL_WIDTH = 220; + const tablePixelWidth = TABLE_DOC_COL_WIDTH + (signatoryNames.length * TABLE_SIG_COL_WIDTH); + + // Build match lookup: blankPageId → AssemblyMatch + const matchByBlankId = new Map(); + for (const match of matches) { + matchByBlankId.set(match.blankPageId, match); + } + + // For a (document, signatory) cell, return ALL blank pages for that combination (across all parties) + const getCells = (docName: string, signatoryName: string): CellData[] => { + const blanks = blankPages.filter( + p => p.documentName === docName && (p.signatoryName || '(Unnamed)') === signatoryName + ); + return blanks.map(blank => ({ blankPage: blank, match: matchByBlankId.get(blank.id) || null })); + }; + + // Summary counts + const totalRequired = blankPages.length; + const totalMatched = matches.length; + const progressPct = totalRequired > 0 ? Math.round((totalMatched / totalRequired) * 100) : 0; + + const handlePrint = () => { + const printWindow = window.open('', '_blank'); + if (!printWindow) return; + + const now = new Date().toLocaleString(); + + // Build table rows for print + const headerCells = signatoryNames.map(s => { + const parties = partiesBySignatory(s).map(p => `${p}`).join(''); + return `${s}${parties ? `
${parties}` : ''}`; + }).join(''); + + const bodyRows = documentNames.map(docName => { + const cells = signatoryNames.map(sigName => { + const cellData = getCells(docName, sigName); + if (cellData.length === 0) return `—`; + const allMatched = cellData.every(c => !!c.match); + const anyMatched = cellData.some(c => !!c.match); + if (allMatched) { + const label = cellData[0].match!.status === 'auto-matched' ? 'Auto' : 'Manual'; + return `✓ ${label}`; + } + if (anyMatched) return `⚠ Partial`; + return `⚠ Pending`; + }).join(''); + return `${docName}${cells}`; + }).join(''); + + printWindow.document.write(` + + + + Signature Packet Checklist + + + +

Signature Packet Checklist

+
Generated ${now}
+
${totalMatched}/${totalRequired} signature pages matched
+ + ${headerCells} + ${bodyRows} +
Document
+ + + `); + printWindow.document.close(); + printWindow.focus(); + setTimeout(() => printWindow.print(), 300); + }; + + const handleChecklistWheel: React.WheelEventHandler = (event) => { + const container = scrollContainerRef.current; + if (!container) return; + + // Let trackpads handle natural horizontal scroll, but map vertical wheel + // to horizontal movement when this grid can scroll sideways. + const canScrollHorizontally = container.scrollWidth > container.clientWidth; + if (!canScrollHorizontally) return; + + const dominantVertical = Math.abs(event.deltaY) > Math.abs(event.deltaX); + if (dominantVertical && event.deltaY !== 0) { + container.scrollLeft += event.deltaY; + event.preventDefault(); + } + }; + + useEffect(() => { + const localViewport = scrollContainerRef.current; + const sizer = tableSizerRef.current; + if (!localViewport || !sizer) return; + + const updateScrollMetrics = () => { + const width = Math.max(tablePixelWidth, sizer.scrollWidth); + const max = Math.max(0, width - localViewport.clientWidth); + setMaxScrollLeft(max); + setScrollLeft(Math.min(localViewport.scrollLeft, max)); + }; + + updateScrollMetrics(); + const observer = new ResizeObserver(updateScrollMetrics); + observer.observe(localViewport); + observer.observe(sizer); + const onScroll = () => { + setScrollLeft(localViewport.scrollLeft); + }; + localViewport.addEventListener('scroll', onScroll, { passive: true }); + return () => { + observer.disconnect(); + localViewport.removeEventListener('scroll', onScroll); + }; + }, [blankPages.length, matches.length, signatoryNames.length, documentNames.length, tablePixelWidth]); + + const handleSliderChange: React.ChangeEventHandler = (event) => { + const next = Number(event.currentTarget.value); + const host = scrollContainerRef.current; + if (host) host.scrollLeft = next; + setScrollLeft(next); + }; + + const scrollByAmount = (delta: number) => { + const host = scrollContainerRef.current; + if (!host) return; + host.scrollLeft += delta; + setScrollLeft(host.scrollLeft); + }; + + return ( +
+ {/* Summary bar */} +
+
+
+ + Assembly Progress + +
+ 0 ? 'text-green-600' : 'text-slate-600'}`}> + {totalMatched}/{totalRequired} matched + + +
+
+
+
0 + ? 'bg-green-500' + : 'bg-blue-500' + }`} + style={{ width: `${progressPct}%` }} + /> +
+
+
+ + {/* Grid */} +
+
+
+ + +
+ +
+
+ +
+
+ + + + + {signatoryNames.map((sigName, idx) => ( + + ))} + + + + {documentNames.map((docName, rowIdx) => ( + + + {signatoryNames.map(sigName => { + const cellData = getCells(docName, sigName); + + if (cellData.length === 0) { + return ( + + ); + } + + return ( + + ); + })} + + ))} + +
+ Document + { dragIndex.current = idx; }} + onDragOver={e => e.preventDefault()} + onDrop={() => { + const from = dragIndex.current; + if (from === null || from === idx) return; + const next = [...signatoryNames]; + next.splice(idx, 0, next.splice(from, 1)[0]); + setSignatoryOrder(next); + dragIndex.current = null; + }} + className="text-center px-3 py-3 font-medium text-slate-600 min-w-[220px] w-[220px] max-w-[220px] cursor-grab select-none" + > +
{sigName}
+
+ {partiesBySignatory(sigName).map(party => ( + {party} + ))} +
+
+ + {docName} + + +
+ +
+
+
+ {cellData.map(cell => { + const isMatched = !!cell.match; + return ( + + ); + })} +
+
+
+
+
+ ); +}; + +export default CompletionChecklist; diff --git a/components/ExecutedPageCard.tsx b/components/ExecutedPageCard.tsx new file mode 100644 index 0000000..47f9ced --- /dev/null +++ b/components/ExecutedPageCard.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { ExecutedSignaturePage, AssemblyMatch } from '../types'; +import { CheckCircle2, AlertCircle, FileText, Users, UserPen, Briefcase } from 'lucide-react'; + +interface ExecutedPageCardProps { + page: ExecutedSignaturePage; + match: AssemblyMatch | null; + onUnmatch?: (executedPageId: string) => void; +} + +const ExecutedPageCard: React.FC = ({ page, match, onUnmatch }) => { + const isMatched = !!match; + + return ( +
+ {/* Thumbnail */} +
+ {`Executed +
+ Pg {page.pageNumber} +
+ {/* Status badge */} +
+ {isMatched ? 'Matched' : page.isConfirmedExecuted ? 'Unmatched' : 'Not signed'} +
+
+ + {/* Content */} +
+ {/* Source file */} +
+ From: {page.sourceFileName} +
+ + {/* Extracted metadata */} +
+ {page.extractedDocumentName && ( +
+ + + {page.extractedDocumentName} + +
+ )} + {page.extractedPartyName && ( +
+ + {page.extractedPartyName} +
+ )} + {page.extractedSignatoryName && ( +
+ + {page.extractedSignatoryName} +
+ )} + {page.extractedCapacity && ( +
+ + {page.extractedCapacity} +
+ )} +
+ + {/* Match info */} + {isMatched && match && ( +
+
+ + + Matched to {match.documentName} — {match.partyName} + +
+ {onUnmatch && ( + + )} +
+ )} + + {!isMatched && page.isConfirmedExecuted && ( +
+ + Awaiting match — assign manually or run auto-match +
+ )} + + {!page.isConfirmedExecuted && ( +
+ + This page does not appear to be signed +
+ )} +
+
+ ); +}; + +export default ExecutedPageCard; diff --git a/components/InstructionsModal.tsx b/components/InstructionsModal.tsx index 856bd96..066c7d2 100644 --- a/components/InstructionsModal.tsx +++ b/components/InstructionsModal.tsx @@ -13,25 +13,24 @@ const InstructionsModal: React.FC = ({ isOpen, onClose, if (!isOpen) return null; - // Group by Party - const parties: string[] = (Array.from(new Set(pages.map(p => p.partyName))) as string[]).sort(); + // Group by Signatory + const signatories: string[] = (Array.from(new Set(pages.map(p => p.signatoryName || '(No Signatory)'))) as string[]).sort(); const groupedPages: Record = {}; - - parties.forEach(party => { - groupedPages[party] = pages.filter(p => p.partyName === party && p.copies > 0); + + signatories.forEach(signatory => { + groupedPages[signatory] = pages.filter(p => (p.signatoryName || '(No Signatory)') === signatory && p.copies > 0); }); const handleCopy = () => { let text = `SIGNING INSTRUCTIONS\nGenerated by Signature Packet IDE\n\n`; - - Object.entries(groupedPages).forEach(([party, partyPages]) => { - if (partyPages.length === 0) return; - text += `${party.toUpperCase()}\n`; - text += '-'.repeat(party.length) + '\n'; - partyPages.forEach(p => { + + Object.entries(groupedPages).forEach(([signatory, signatoryPages]) => { + if (signatoryPages.length === 0) return; + text += `${signatory.toUpperCase()}\n`; + text += '-'.repeat(signatory.length) + '\n'; + signatoryPages.forEach(p => { text += `• ${p.documentName}\n`; - const sigText = p.signatoryName ? ` by ${p.signatoryName}` : ''; - text += ` Sign as: ${p.capacity}${sigText} (${p.copies} ${p.copies === 1 ? 'copy' : 'copies'})\n`; + text += ` Sign as: ${p.capacity} (${p.copies} ${p.copies === 1 ? 'copy' : 'copies'})\n`; }); text += '\n'; }); @@ -68,30 +67,30 @@ const InstructionsModal: React.FC = ({ isOpen, onClose, {/* Content */}
- {parties.map(party => { - const partyPages = groupedPages[party]; - if (partyPages.length === 0) return null; + {signatories.map(signatory => { + const signatoryPages = groupedPages[signatory]; + if (signatoryPages.length === 0) return null; return ( -
+
- {party} + {signatory}
+ - - {partyPages.map(p => ( + {signatoryPages.map(p => ( + - ))} diff --git a/components/MatchPickerModal.tsx b/components/MatchPickerModal.tsx new file mode 100644 index 0000000..f68533e --- /dev/null +++ b/components/MatchPickerModal.tsx @@ -0,0 +1,226 @@ +import React, { useState } from 'react'; +import { X, CheckCircle2, FileText, Users, UserPen, Briefcase, ArrowRight, Unlink } from 'lucide-react'; +import { ExtractedSignaturePage, ExecutedSignaturePage, AssemblyMatch } from '../types'; + +interface MatchPickerModalProps { + isOpen: boolean; + onClose: () => void; + blankPage: ExtractedSignaturePage | null; + currentMatch: AssemblyMatch | null; + executedPages: ExecutedSignaturePage[]; + allMatches: AssemblyMatch[]; + onConfirmMatch: (blankPageId: string, executedPageId: string) => void; + onUnmatch: (blankPageId: string) => void; +} + +const MatchPickerModal: React.FC = ({ + isOpen, + onClose, + blankPage, + currentMatch, + executedPages, + allMatches, + onConfirmMatch, + onUnmatch, +}) => { + const [selectedExecutedId, setSelectedExecutedId] = useState(null); + + if (!isOpen || !blankPage) return null; + + // Determine which executed pages are available (not matched to other blanks) + const matchedExecutedIds = new Set( + allMatches + .filter(m => m.blankPageId !== blankPage.id) // exclude THIS blank's match + .map(m => m.executedPageId) + ); + + const availableExecuted = executedPages.filter( + ep => ep.isConfirmedExecuted && !matchedExecutedIds.has(ep.id) + ); + + // Also show the currently matched executed page at the top if it exists + const currentMatchedPage = currentMatch + ? executedPages.find(ep => ep.id === currentMatch.executedPageId) + : null; + + const handleConfirm = () => { + if (selectedExecutedId && blankPage) { + onConfirmMatch(blankPage.id, selectedExecutedId); + setSelectedExecutedId(null); + onClose(); + } + }; + + const handleUnmatch = () => { + if (blankPage) { + onUnmatch(blankPage.id); + setSelectedExecutedId(null); + onClose(); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ {currentMatch ? 'Reassign Match' : 'Match Executed Page'} +

+ +
+ + {/* Content */} +
+ {/* Left: Blank signature page info */} +
+

+ Blank Signature Page +

+
+ {`Blank +
+
+
+ + {blankPage.documentName} +
+
+ + {blankPage.partyName} +
+ {blankPage.signatoryName && ( +
+ + {blankPage.signatoryName} +
+ )} + {blankPage.capacity && ( +
+ + {blankPage.capacity} +
+ )} +
+ Page {blankPage.pageNumber} in original document +
+
+ + {/* Current match info */} + {currentMatch && currentMatchedPage && ( +
+
Currently matched to:
+
+ {currentMatchedPage.extractedPartyName} — {currentMatchedPage.sourceFileName} pg {currentMatchedPage.pageNumber} +
+ +
+ )} +
+ + {/* Arrow divider */} +
+ +
+ + {/* Right: Available executed pages */} +
+

+ Select Executed Page ({availableExecuted.length} available) +

+ + {availableExecuted.length === 0 ? ( +
+ No unmatched executed pages available. +
+ Upload more signed pages or unmatch existing ones. +
+ ) : ( +
+ {availableExecuted.map(ep => { + const isSelected = selectedExecutedId === ep.id; + + return ( + + ); + })} +
+ )} +
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +}; + +export default MatchPickerModal; diff --git a/package-lock.json b/package-lock.json index 383cb74..54474bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,26 @@ "version": "0.0.0", "dependencies": { "@google/genai": "^1.30.0", + "dotenv": "^17.3.1", + "express": "^5.2.1", "jszip": "^3.10.1", "lucide-react": "^0.555.0", + "multer": "^2.1.1", "pdf-lib": "^1.17.1", "react": "^19.2.0", "react-dom": "^19.2.0", "uuid": "^13.0.0" }, "devDependencies": { + "@types/express": "^5.0.6", + "@types/multer": "^2.1.0", "@types/node": "^22.14.0", "@vitejs/plugin-react": "^5.0.0", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", "typescript": "~5.8.2", - "vite": "^6.2.0" + "vite": "^6.2.0", + "vitest": "^4.0.18" } }, "node_modules/@babel/code-frame": { @@ -1178,6 +1186,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1223,6 +1238,45 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1230,6 +1284,48 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.19.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", @@ -1240,6 +1336,41 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", @@ -1261,6 +1392,130 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1294,6 +1549,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1339,6 +1610,30 @@ "node": "*" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1388,6 +1683,61 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001757", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", @@ -1409,1041 +1759,2732 @@ ], "license": "CC-BY-4.0" }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=18" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 12" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=8" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", "dependencies": { - "safe-buffer": "^5.0.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", - "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "node": ">=8" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "color-name": "~1.1.4" }, "engines": { - "node": ">=12.20.0" + "node": ">=7.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, - "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "license": "Apache-2.0", + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" } }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=18" + "node": ">= 6" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, "engines": { - "node": ">=18" + "node": ">= 0.6" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, "engines": { - "node": ">= 14" + "node": ">= 0.6" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.6.0" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": ">= 8" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, "engines": { - "node": ">=6" + "node": ">= 12" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "bignumber.js": "^9.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, "engines": { - "node": ">=6" + "node": ">= 0.8" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { - "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/electron-to-chromium": { + "version": "1.5.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", + "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } + "license": "ISC" }, - "node_modules/lucide-react": { - "version": "0.555.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz", - "integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.4" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 0.4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, "license": "MIT" }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "es-errors": "^1.3.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">= 0.4" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=10.5.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "node": ">=6" } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12.0.0" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/pdf-lib": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", - "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { - "@pdf-lib/standard-fonts": "^1.0.0", - "@pdf-lib/upng": "^1.0.1", - "pako": "^1.0.11", - "tslib": "^1.11.1" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "paypal", + "url": "https://paypal.me/jimmywarting" } ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": "^12.20 || >= 14.13" } }, - "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, - "peerDependencies": { - "react": "^19.2.0" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "glob": "^10.3.7" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, - "bin": { - "rimraf": "dist/esm/bin.mjs" + "engines": { + "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "dev": true, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" + "fetch-blob": "^3.1.2" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", - "fsevents": "~2.3.2" + "node": ">=12.20.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" ], - "license": "MIT" + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", "engines": { "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=12" + "node": ">= 0.8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.555.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz", + "integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12.0.0" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" }, "node_modules/typescript": { "version": "5.8.3", @@ -2466,6 +4507,15 @@ "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -2516,6 +4566,15 @@ "uuid": "dist-node/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -2591,6 +4650,84 @@ } } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -2615,6 +4752,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -2706,6 +4860,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -2727,12 +4887,96 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index f5094cd..adb5cf6 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,34 @@ "type": "module", "scripts": { "dev": "vite", + "dev:backend": "tsx backend/server.ts", + "dev:full": "concurrently -n frontend,backend -c blue,green \"npm run dev\" \"npm run dev:backend\"", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { - "lucide-react": "^0.555.0", - "uuid": "^13.0.0", "@google/genai": "^1.30.0", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "jszip": "^3.10.1", + "lucide-react": "^0.555.0", + "multer": "^2.1.1", + "pdf-lib": "^1.17.1", "react": "^19.2.0", "react-dom": "^19.2.0", - "pdf-lib": "^1.17.1", - "jszip": "^3.10.1" + "uuid": "^13.0.0" }, "devDependencies": { + "@types/express": "^5.0.6", + "@types/multer": "^2.1.0", "@types/node": "^22.14.0", "@vitejs/plugin-react": "^5.0.0", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", "typescript": "~5.8.2", - "vite": "^6.2.0" + "vite": "^6.2.0", + "vitest": "^4.0.18" } } diff --git a/services/docxService.ts b/services/docxService.ts new file mode 100644 index 0000000..a9c116b --- /dev/null +++ b/services/docxService.ts @@ -0,0 +1,48 @@ +const DEFAULT_CONVERTER_URL = '/api/docx-to-pdf'; + +const getConverterUrl = (): string => { + const configured = import.meta.env.VITE_DOCX_CONVERTER_URL; + return configured && configured.trim() ? configured.trim() : DEFAULT_CONVERTER_URL; +}; + +/** + * Converts a DOCX file into a PDF via a server-side converter. + * Use a backend that calls Adobe PDF Services (or equivalent) for layout-fidelity. + */ +export const convertDocxToPdf = async (docxFile: File): Promise => { + const converterUrl = getConverterUrl(); + const formData = new FormData(); + formData.append('file', docxFile, docxFile.name); + + const response = await fetch(converterUrl, { + method: 'POST', + body: formData, + credentials: 'include', + }); + + if (!response.ok) { + let detail = ''; + const contentType = response.headers.get('content-type') || ''; + try { + if (contentType.toLowerCase().includes('application/json')) { + const payload = await response.json() as { error?: string; detail?: string }; + detail = [payload.error, payload.detail].filter(Boolean).join(': '); + } else { + detail = (await response.text()).trim(); + } + } catch { + // Ignore parse failure and fall back to status text. + } + const suffix = detail ? ` - ${detail}` : ''; + throw new Error(`DOCX conversion failed (${response.status} ${response.statusText})${suffix}`); + } + + const contentType = response.headers.get('content-type') || ''; + if (!contentType.toLowerCase().includes('application/pdf')) { + throw new Error('DOCX converter returned a non-PDF response'); + } + + const pdfBytes = await response.arrayBuffer(); + const pdfName = docxFile.name.replace(/\.docx$/i, '.pdf'); + return new File([pdfBytes], pdfName, { type: 'application/pdf', lastModified: Date.now() }); +}; diff --git a/services/geminiService.ts b/services/geminiService.ts index b3402b6..8205b36 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -1,25 +1,97 @@ import { GoogleGenAI, Type, Schema } from "@google/genai"; -import { SignatureBlockExtraction } from "../types"; +import { SignatureBlockExtraction, ExecutedPageExtraction } from "../types"; const SYSTEM_INSTRUCTION = ` -You are a specialized legal AI assistant for transaction lawyers. +You are a specialized legal AI assistant for transaction lawyers. Your task is to analyze an image of a document page and identify if it is a "Signature Page" (or "Execution Page"). ### CRITICAL DEFINITIONS FOR EXTRACTION -1. **PARTY**: The legal entity or person entering into the contract (e.g., "ABC Holdings Limited", "XYZ Fund II, L.P."). - - Found in headings like "EXECUTED by ABC HOLDINGS LIMITED". + +1. **PARTY**: The legal entity OR individual person who is a party to the contract. + - For COMPANIES: Found in headings like "EXECUTED by ABC HOLDINGS LIMITED" or "ABC CORP:". The company name is the party. + - For INDIVIDUALS: The label above the signature line (e.g. "KEY HOLDER:", "FOUNDER:", "GUARANTOR:", "INVESTOR:") is a ROLE, NOT the party name. The party is the INDIVIDUAL'S NAME printed below or beside the signature line. + - NEVER use a role label like "Key Holder", "Founder", "Guarantor", "Investor" as the party name when a person's name is present. + - If the only name present is a person's name (e.g. "John Smith"), use that as the party name. + 2. **SIGNATORY**: The human being physically signing the page. - - Found under lines like "Name: Jane Smith" or "Signed by: ___". - - A company CANNOT be a signatory. -3. **CAPACITY**: The role/authority of the signatory (e.g., "Director", "Authorised Signatory", "General Partner"). - - Found under "Title:". + - For companies: the named officer/director signing on behalf of the company (found under "Name:", "By:", or "Signed by:"). + - For individuals signing in their personal capacity: the signatory IS the same person as the party. Use their name for BOTH partyName and signatoryName. + - A company name (e.g. "Acme Corp") can NEVER be a signatory. + +3. **CAPACITY**: The role or authority of the signatory. + - For company signatories: "Director", "CEO", "Authorised Signatory", "General Partner", etc. + - For individuals signing personally: use the label from the block (e.g. "Key Holder", "Founder", "Guarantor") as the capacity, NOT as the party name. + +### COMMON INDIVIDUAL SIGNATURE BLOCK PATTERN (very important): +\`\`\` +KEY HOLDER: + +______________________________ +John Smith +\`\`\` +→ partyName: "John Smith", signatoryName: "John Smith", capacity: "Key Holder" + +### COMMON COMPANY SIGNATURE BLOCK PATTERN: +\`\`\` +ACME CORP: + +By: ______________________________ +Name: Jane Smith +Title: Director +\`\`\` +→ partyName: "Acme Corp", signatoryName: "Jane Smith", capacity: "Director" + +### MULTI-LEVEL ENTITY SIGNATURE BLOCKS (funds, LPs, trusts) +Many entities sign through a chain of intermediaries. The pattern looks like: + +\`\`\` +[ROLE LABEL] (if an entity): +Name of [Role]: [Top-Level Entity], L.P. +By: [Intermediate Entity], L.L.C., its general partner + By: ______________________________ + Name: [Individual Name] + Title: [Title] +\`\`\` + +Rules for multi-level entities: +- The PARTY is always the TOP-LEVEL named entity (the fund, LP, or trust — e.g. "[Fund] IX, L.P.") +- The SIGNATORY is always the INDIVIDUAL PERSON who physically signs (the innermost "Name:" line) +- The CAPACITY should describe the signing chain, e.g. "Member of [GP Entity], L.L.C., its General Partner" +- The role label before the block (e.g. "HOLDER", "INVESTOR") is NOT the party name — it goes in capacity if relevant +- Look for "Name of Holder:", "Name of Investor:", etc. as the source of the party name +- Intermediate entities (the "By: [Entity], its general partner" lines) are NOT the party — they are part of the signing authority chain + +Example: +\`\`\` +HOLDER (if an entity): +Name of Holder: Sequoia Capital Fund XV, L.P. +By: SC XV Management, L.L.C., its general partner + By: ______________________________ + Name: Jane Smith + Title: Managing Member +\`\`\` +→ partyName: "Sequoia Capital Fund XV, L.P.", signatoryName: "Jane Smith", capacity: "Managing Member of SC XV Management, L.L.C., its General Partner" + +Example with deeper nesting: +\`\`\` +INVESTOR: +Name of Investor: Acme Growth Partners III, L.P. +By: Acme Growth GP III, L.L.C., its general partner +By: Acme Capital Holdings, Inc., its managing member + By: ______________________________ + Name: John Doe + Title: President +\`\`\` +→ partyName: "Acme Growth Partners III, L.P.", signatoryName: "John Doe", capacity: "President of Acme Capital Holdings, Inc." ### RULES 1. If this is a signature page, set isSignaturePage to true. 2. Extract ALL signature blocks found on the page. -3. For each block, strictly separate the **Party Name** (Entity), **Signatory Name** (Human), and **Capacity** (Title). -4. If a field is blank (e.g. "Name: _______"), leave the extracted value as empty string or "Unknown". -5. If it is NOT a signature page (e.g. text clauses only), set isSignaturePage to false. +3. For each block, strictly separate the **Party Name** (Entity or Individual), **Signatory Name** (Human), and **Capacity** (Title/Role). +4. When you see nested "By:" lines, always trace to the TOP-LEVEL entity for partyName and the BOTTOM-LEVEL individual for signatoryName. +5. Look for "Name of [Role]:" patterns (e.g. "Name of Holder:", "Name of Investor:") as a reliable indicator of the top-level party name. +6. If a field is blank (e.g. "Name: _______"), leave the extracted value as empty string. +7. If it is NOT a signature page (e.g. text clauses only), set isSignaturePage to false. `; const RESPONSE_SCHEMA: Schema = { @@ -36,15 +108,15 @@ const RESPONSE_SCHEMA: Schema = { properties: { partyName: { type: Type.STRING, - description: "The legal entity bound by the contract (The PARTY)." + description: "The legal entity or individual who is a party to the contract. For companies: the company name. For multi-level entities (LPs, funds): the TOP-LEVEL entity name from 'Name of Holder/Investor:' lines, NOT intermediate entities. For individuals: the person's actual name (e.g. 'John Smith'), NOT their role label (e.g. NOT 'Key Holder' or 'Founder')." }, signatoryName: { type: Type.STRING, - description: "The human name of the person signing (The SIGNATORY)." + description: "The human name of the person physically signing. For multi-level entity chains, this is the individual at the bottom of the 'By:' chain. For individuals signing personally, this is the same as partyName. Never use a company name here." }, capacity: { type: Type.STRING, - description: "The title or role of the person signing (The CAPACITY)." + description: "The title or role of the signatory. For multi-level entity chains, include the signing authority context (e.g. 'Managing Member of [GP], its General Partner'). For company signatories: 'Director', 'CEO', etc. For individuals signing personally: use their block label, e.g. 'Key Holder', 'Founder', 'Guarantor'." } } } @@ -53,6 +125,9 @@ const RESPONSE_SCHEMA: Schema = { required: ["isSignaturePage", "signatures"] }; +const needsRetry = (signatures: Array<{ partyName: string; signatoryName: string; capacity: string }>): boolean => + signatures.some(s => !s.partyName || !s.signatoryName); + export const analyzePage = async ( base64Image: string, modelName: string = 'gemini-2.5-flash' @@ -60,22 +135,14 @@ export const analyzePage = async ( // Remove data:image/jpeg;base64, prefix if present const cleanBase64 = base64Image.split(',')[1]; - try { + const callAI = async (promptText: string): Promise => { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); - const response = await ai.models.generateContent({ model: modelName, contents: { parts: [ - { - inlineData: { - mimeType: 'image/jpeg', - data: cleanBase64 - } - }, - { - text: "Analyze this page. Is it a signature page? Extract the Party, Signatory, and Capacity according to the definitions." - } + { inlineData: { mimeType: 'image/jpeg', data: cleanBase64 } }, + { text: promptText } ] }, config: { @@ -84,18 +151,221 @@ export const analyzePage = async ( responseSchema: RESPONSE_SCHEMA } }); - const text = response.text; if (!text) throw new Error("No response from AI"); - return JSON.parse(text) as SignatureBlockExtraction; + }; + + try { + const result = await callAI( + "Analyze this page. Is it a signature page? Extract the Party, Signatory, and Capacity according to the definitions." + ); + + // Log raw result for debugging + if (result.isSignaturePage) { + console.log("[analyzePage] Raw extraction:", JSON.stringify(result.signatures, null, 2)); + } + + // Retry with a more forceful prompt if any block is missing party or signatory + if (result.isSignaturePage && needsRetry(result.signatures)) { + console.warn("[analyzePage] Missing party/signatory — retrying with enhanced prompt. Problematic blocks:", + result.signatures.filter(s => !s.partyName || !s.signatoryName)); + try { + const retry = await callAI( + "IMPORTANT: One or more signature blocks on this page returned an empty partyName or signatoryName. " + + "Look very carefully at the FULL signature block structure. " + + "If you see a pattern like 'Name of Holder: [Fund Name]' or 'Name of Investor: [Fund Name]', " + + "that IS the partyName — use it even if it's a long entity name with L.P., L.L.C., etc. " + + "Trace all nested 'By:' lines to find the individual's name at the bottom — that is the signatoryName. " + + "Re-extract ALL signature blocks, ensuring partyName and signatoryName are never empty." + ); + console.log("[analyzePage] Retry result:", JSON.stringify(retry.signatures, null, 2)); + if (retry.isSignaturePage && retry.signatures.length > 0) { + return retry; + } + } catch (retryErr) { + console.error("analyzePage retry failed:", retryErr); + } + } + + return result; } catch (error) { console.error("Gemini Analysis Error:", error); - // Fallback safe return - return { - isSignaturePage: false, - signatures: [] - }; + return { isSignaturePage: false, signatures: [] }; + } +}; + +// --- Executed (Signed) Page Analysis --- + +const EXECUTED_PAGE_SYSTEM_INSTRUCTION = ` +You are a specialized legal AI assistant for transaction lawyers. +Your task is to analyze an image of an EXECUTED (signed) signature page from a legal document. + +### YOUR TASK +1. Determine if this page contains an actual signature (handwritten ink, electronic signature, DocuSign stamp, or similar). +2. Extract the following information: + +**documentName**: The name of the agreement/contract this signature page belongs to. +- Look for text like "Signature Page to [Agreement Name]" or "[Signature Page]" headers/footers. +- Look for running headers, footers, or watermarks that reference the agreement name. +- Common patterns: "Signature Page to Amended and Restated Investors' Rights Agreement" +- If you find it, extract ONLY the agreement name (e.g. "Amended and Restated Investors' Rights Agreement"), not the "Signature Page to" prefix. +- If you cannot determine the document name, return an empty string. + +**partyName**: The legal entity or individual who signed. +- Apply the same rules as for blank pages: company name for companies, individual's actual name for individuals. +- NEVER use role labels ("Key Holder", "Founder") as the party name. +- For multi-level entities (LPs, funds, trusts): use the TOP-LEVEL entity name from "Name of Holder/Investor:" lines, NOT intermediate entities. + +**signatoryName**: The human being who physically signed. +- For individuals signing personally, this is the same as partyName. +- For multi-level entity chains, this is the individual at the bottom of the "By:" chain. + +**capacity**: The role/title of the signatory. +- For individuals signing personally, use their role label (e.g. "Key Holder", "Founder"). +- For multi-level entity chains, include the signing authority context (e.g. "Managing Member of [GP Entity], its General Partner"). + +**isExecuted**: Whether this page appears to actually be signed/executed. +- true if there is a visible signature (ink, electronic, stamp, DocuSign completion marker). +- false if the signature line is blank/unsigned. + +### MULTI-LEVEL ENTITY SIGNATURE BLOCKS (funds, LPs, trusts) +Many entities sign through a chain of intermediaries: + +\`\`\` +[ROLE LABEL] (if an entity): +Name of [Role]: [Top-Level Entity], L.P. +By: [Intermediate Entity], L.L.C., its general partner + By: ______________________________ + Name: [Individual Name] + Title: [Title] +\`\`\` + +- The PARTY is the TOP-LEVEL named entity (the fund/LP/trust) +- The SIGNATORY is the INDIVIDUAL who physically signed (innermost "Name:" line) +- The CAPACITY describes the signing chain +- "Name of Holder:", "Name of Investor:" etc. indicate the top-level party name +- Intermediate "By: [Entity], its general partner" lines are NOT the party + +Example: +\`\`\` +HOLDER (if an entity): +Name of Holder: Sequoia Capital Fund XV, L.P. +By: SC XV Management, L.L.C., its general partner + By: [signature] + Name: Jane Smith + Title: Managing Member +\`\`\` +→ partyName: "Sequoia Capital Fund XV, L.P.", signatoryName: "Jane Smith", capacity: "Managing Member of SC XV Management, L.L.C., its General Partner" + +### RULES +1. The documentName is CRITICAL for matching this executed page to its agreement. Look carefully for it. +2. If this is a scanned page, OCR the text to extract all information. +3. If multiple signature blocks appear on one page, extract all of them. +4. Apply the same Party vs Signatory vs Capacity distinction rules as for blank signature pages. +5. When you see nested "By:" lines, always trace to the TOP-LEVEL entity for partyName and the BOTTOM-LEVEL individual for signatoryName. +6. Look for "Name of [Role]:" patterns as a reliable indicator of the top-level party name. +`; + +const EXECUTED_PAGE_RESPONSE_SCHEMA: Schema = { + type: Type.OBJECT, + properties: { + isExecuted: { + type: Type.BOOLEAN, + description: "True if the page appears to contain an actual signature (not blank/unsigned)." + }, + documentName: { + type: Type.STRING, + description: "The name of the agreement this signature page belongs to, extracted from page text (e.g. 'Amended and Restated Investors Rights Agreement'). Empty string if not determinable." + }, + signatures: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + partyName: { + type: Type.STRING, + description: "The legal entity or individual party. For multi-level entities (LPs, funds): the TOP-LEVEL entity name from 'Name of Holder/Investor:' lines, NOT intermediate entities. For individuals: their actual name, NOT their role label." + }, + signatoryName: { + type: Type.STRING, + description: "The human name of the person who signed. For multi-level entity chains, this is the individual at the bottom of the 'By:' chain. For individuals signing personally, same as partyName." + }, + capacity: { + type: Type.STRING, + description: "The title or role of the signatory. For multi-level entity chains, include the signing authority context (e.g. 'Managing Member of [GP], its General Partner'). For individuals: use their block label (e.g. 'Key Holder')." + } + } + } + } + }, + required: ["isExecuted", "documentName", "signatures"] +}; + +export const analyzeExecutedPage = async ( + base64Image: string, + modelName: string = 'gemini-2.5-flash' +): Promise => { + const cleanBase64 = base64Image.split(',')[1]; + + const callAI = async (promptText: string): Promise => { + const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); + const response = await ai.models.generateContent({ + model: modelName, + contents: { + parts: [ + { inlineData: { mimeType: 'image/jpeg', data: cleanBase64 } }, + { text: promptText } + ] + }, + config: { + systemInstruction: EXECUTED_PAGE_SYSTEM_INSTRUCTION, + responseMimeType: "application/json", + responseSchema: EXECUTED_PAGE_RESPONSE_SCHEMA + } + }); + const text = response.text; + if (!text) throw new Error("No response from AI"); + return JSON.parse(text) as ExecutedPageExtraction; + }; + + try { + const result = await callAI( + "Analyze this executed signature page. Is it actually signed? Extract the document name, party, signatory, and capacity." + ); + + // Log raw result for debugging + if (result.isExecuted) { + console.log("[analyzeExecutedPage] Raw extraction:", JSON.stringify({ documentName: result.documentName, signatures: result.signatures }, null, 2)); + } + + // Retry with a more forceful prompt if any block is missing party or signatory + if (result.isExecuted && needsRetry(result.signatures)) { + console.warn("[analyzeExecutedPage] Missing party/signatory — retrying. Problematic blocks:", + result.signatures.filter(s => !s.partyName || !s.signatoryName)); + try { + const retry = await callAI( + "IMPORTANT: One or more signature blocks returned an empty partyName or signatoryName. " + + "Look very carefully at the full signature block. " + + "If you see 'Name of Holder:', 'Name of Investor:', or similar, that fund/entity name IS the partyName. " + + "Trace all nested 'By:' lines to the individual at the bottom — that name is the signatoryName. " + + "Also look for the agreement name in headers or footers (e.g. 'Signature Page to [Agreement Name]'). " + + "Re-extract everything, ensuring partyName and signatoryName are never empty." + ); + console.log("[analyzeExecutedPage] Retry result:", JSON.stringify(retry.signatures, null, 2)); + if (retry.isExecuted && retry.signatures.length > 0) { + return retry; + } + } catch (retryErr) { + console.error("analyzeExecutedPage retry failed:", retryErr); + } + } + + return result; + + } catch (error) { + console.error("Gemini Executed Page Analysis Error:", error); + return { isExecuted: false, documentName: '', signatures: [] }; } }; \ No newline at end of file diff --git a/services/matchingService.test.ts b/services/matchingService.test.ts new file mode 100644 index 0000000..df14d9a --- /dev/null +++ b/services/matchingService.test.ts @@ -0,0 +1,226 @@ +import { describe, it, expect } from 'vitest'; +import { autoMatch, createManualMatch } from './matchingService'; +import { ExtractedSignaturePage, ExecutedSignaturePage, AssemblyMatch } from '../types'; + +// --- Fixtures --- + +function makeBlank(overrides: Partial & { id: string }): ExtractedSignaturePage { + return { + id: overrides.id, + documentId: overrides.documentId ?? 'doc-1', + documentName: overrides.documentName ?? 'Investors Rights Agreement', + pageIndex: overrides.pageIndex ?? 0, + pageNumber: overrides.pageNumber ?? 1, + partyName: overrides.partyName ?? 'Acme Corp', + signatoryName: overrides.signatoryName ?? 'Jane Smith', + capacity: overrides.capacity ?? 'Chief Executive Officer', + copies: overrides.copies ?? 1, + thumbnailUrl: '', + originalWidth: 612, + originalHeight: 792, + }; +} + +function makeExecuted(overrides: Partial & { id: string }): ExecutedSignaturePage { + return { + id: overrides.id, + sourceUploadId: overrides.sourceUploadId ?? 'upload-1', + sourceFileName: overrides.sourceFileName ?? 'executed.pdf', + pageIndexInSource: overrides.pageIndexInSource ?? 0, + pageNumber: overrides.pageNumber ?? 1, + extractedDocumentName: overrides.extractedDocumentName ?? 'Investors Rights Agreement', + extractedPartyName: overrides.extractedPartyName ?? 'Acme Corp', + extractedSignatoryName: overrides.extractedSignatoryName ?? 'Jane Smith', + extractedCapacity: overrides.extractedCapacity ?? 'Chief Executive Officer', + isConfirmedExecuted: overrides.isConfirmedExecuted ?? true, + thumbnailUrl: '', + originalWidth: 612, + originalHeight: 792, + matchedBlankPageId: null, + matchConfidence: null, + }; +} + +// --- Tests --- + +describe('autoMatch', () => { + it('matches identical blank and executed pages', () => { + const blank = makeBlank({ id: 'b1' }); + const executed = makeExecuted({ id: 'e1' }); + + const matches = autoMatch([blank], [executed]); + + expect(matches).toHaveLength(1); + expect(matches[0].blankPageId).toBe('b1'); + expect(matches[0].executedPageId).toBe('e1'); + expect(matches[0].status).toBe('auto-matched'); + }); + + it('does not match executed pages with isConfirmedExecuted = false', () => { + const blank = makeBlank({ id: 'b1' }); + const executed = makeExecuted({ id: 'e1', isConfirmedExecuted: false }); + + const matches = autoMatch([blank], [executed]); + + expect(matches).toHaveLength(0); + }); + + it('does not match pairs below the 0.4 threshold', () => { + const blank = makeBlank({ id: 'b1', documentName: 'Stock Purchase Agreement', partyName: 'Acme Corp', signatoryName: 'Jane Smith' }); + const executed = makeExecuted({ id: 'e1', extractedDocumentName: 'Completely Different Document', extractedPartyName: 'Other LLC', extractedSignatoryName: 'Bob Jones' }); + + const matches = autoMatch([blank], [executed]); + + expect(matches).toHaveLength(0); + }); + + it('each blank page is matched at most once', () => { + const blank = makeBlank({ id: 'b1' }); + const exec1 = makeExecuted({ id: 'e1' }); + const exec2 = makeExecuted({ id: 'e2' }); + + const matches = autoMatch([blank], [exec1, exec2]); + + expect(matches).toHaveLength(1); + expect(matches[0].blankPageId).toBe('b1'); + }); + + it('each executed page is matched at most once', () => { + const blank1 = makeBlank({ id: 'b1' }); + const blank2 = makeBlank({ id: 'b2' }); + const executed = makeExecuted({ id: 'e1' }); + + const matches = autoMatch([blank1, blank2], [executed]); + + expect(matches).toHaveLength(1); + expect(matches[0].executedPageId).toBe('e1'); + }); + + it('prefers the higher-scoring pair in greedy assignment', () => { + const blank1 = makeBlank({ id: 'b1', documentName: 'Investors Rights Agreement', partyName: 'Acme Corp', signatoryName: 'Jane Smith' }); + const blank2 = makeBlank({ id: 'b2', documentName: 'Stock Purchase Agreement', partyName: 'Beta LLC', signatoryName: 'John Doe' }); + // exec1 is a perfect match for blank1 + const exec1 = makeExecuted({ id: 'e1', extractedDocumentName: 'Investors Rights Agreement', extractedPartyName: 'Acme Corp', extractedSignatoryName: 'Jane Smith' }); + // exec2 is a decent but lower match for blank1 + const exec2 = makeExecuted({ id: 'e2', extractedDocumentName: 'Investors Rights Agreement', extractedPartyName: 'Beta LLC', extractedSignatoryName: 'John Doe' }); + + const matches = autoMatch([blank1, blank2], [exec1, exec2]); + + expect(matches).toHaveLength(2); + const match1 = matches.find(m => m.blankPageId === 'b1'); + const match2 = matches.find(m => m.blankPageId === 'b2'); + expect(match1?.executedPageId).toBe('e1'); + expect(match2?.executedPageId).toBe('e2'); + }); + + it('preserves user-confirmed matches and excludes their pages from auto-matching', () => { + const blank1 = makeBlank({ id: 'b1' }); + const blank2 = makeBlank({ id: 'b2', documentName: 'NVCA Side Letter', partyName: 'Beta LLC', signatoryName: 'John Doe' }); + const exec1 = makeExecuted({ id: 'e1' }); + const exec2 = makeExecuted({ id: 'e2', extractedDocumentName: 'NVCA Side Letter', extractedPartyName: 'Beta LLC', extractedSignatoryName: 'John Doe' }); + + const existingConfirmed: AssemblyMatch = { + blankPageId: 'b1', + executedPageId: 'e1', + documentId: 'doc-1', + documentName: 'Investors Rights Agreement', + pageIndex: 0, + partyName: 'Acme Corp', + signatoryName: 'Jane Smith', + status: 'user-confirmed', + }; + + const matches = autoMatch([blank1, blank2], [exec1, exec2], [existingConfirmed]); + + // Should only auto-match b2/e2; b1/e1 are already user-confirmed (preserved externally) + expect(matches).toHaveLength(1); + expect(matches[0].blankPageId).toBe('b2'); + expect(matches[0].executedPageId).toBe('e2'); + }); + + it('preserves user-overridden matches the same way as user-confirmed', () => { + const blank = makeBlank({ id: 'b1' }); + const executed = makeExecuted({ id: 'e1' }); + + const existingOverride: AssemblyMatch = { + blankPageId: 'b1', + executedPageId: 'e1', + documentId: 'doc-1', + documentName: 'Investors Rights Agreement', + pageIndex: 0, + partyName: 'Acme Corp', + signatoryName: 'Jane Smith', + status: 'user-overridden', + }; + + const matches = autoMatch([blank], [executed], [existingOverride]); + + // b1 and e1 are already consumed by the override — nothing new to match + expect(matches).toHaveLength(0); + }); + + it('does not preserve auto-matched entries from existingMatches (they are re-computed)', () => { + const blank = makeBlank({ id: 'b1' }); + const executed = makeExecuted({ id: 'e1' }); + + const staleAutoMatch: AssemblyMatch = { + blankPageId: 'b1', + executedPageId: 'e1', + documentId: 'doc-1', + documentName: 'Investors Rights Agreement', + pageIndex: 0, + partyName: 'Acme Corp', + signatoryName: 'Jane Smith', + status: 'auto-matched', + }; + + // auto-matched existing entries are NOT preserved — pages become available again + const matches = autoMatch([blank], [executed], [staleAutoMatch]); + + expect(matches).toHaveLength(1); + expect(matches[0].blankPageId).toBe('b1'); + expect(matches[0].executedPageId).toBe('e1'); + }); + + it('handles empty inputs gracefully', () => { + expect(autoMatch([], [])).toEqual([]); + expect(autoMatch([makeBlank({ id: 'b1' })], [])).toEqual([]); + expect(autoMatch([], [makeExecuted({ id: 'e1' })])).toEqual([]); + }); + + it('matches with minor whitespace / case differences in names', () => { + const blank = makeBlank({ id: 'b1', documentName: 'Investors Rights Agreement', signatoryName: 'Jane Smith' }); + const executed = makeExecuted({ id: 'e1', extractedDocumentName: ' investors rights agreement ', extractedSignatoryName: 'JANE SMITH' }); + + const matches = autoMatch([blank], [executed]); + + expect(matches).toHaveLength(1); + }); + + it('matches when executed document name contains the blank document name (containment)', () => { + const blank = makeBlank({ id: 'b1', documentName: 'Rights Agreement' }); + const executed = makeExecuted({ id: 'e1', extractedDocumentName: 'Investors Rights Agreement' }); + + const matches = autoMatch([blank], [executed]); + + expect(matches).toHaveLength(1); + }); +}); + +describe('createManualMatch', () => { + it('creates a user-overridden match with correct fields', () => { + const blank = makeBlank({ id: 'b1', documentId: 'doc-42', documentName: 'SPA', partyName: 'Acme Corp', signatoryName: 'Jane Smith', pageIndex: 3 }); + const executed = makeExecuted({ id: 'e1' }); + + const match = createManualMatch(blank, executed); + + expect(match.blankPageId).toBe('b1'); + expect(match.executedPageId).toBe('e1'); + expect(match.documentId).toBe('doc-42'); + expect(match.documentName).toBe('SPA'); + expect(match.partyName).toBe('Acme Corp'); + expect(match.signatoryName).toBe('Jane Smith'); + expect(match.pageIndex).toBe(3); + expect(match.status).toBe('user-overridden'); + }); +}); diff --git a/services/matchingService.ts b/services/matchingService.ts new file mode 100644 index 0000000..e7cb1a6 --- /dev/null +++ b/services/matchingService.ts @@ -0,0 +1,163 @@ +import { ExtractedSignaturePage, ExecutedSignaturePage, AssemblyMatch } from '../types'; + +/** + * Normalizes a string for comparison: lowercase, trim, collapse whitespace. + */ +const normalize = (s: string): string => + s.toLowerCase().trim().replace(/\s+/g, ' '); + +/** + * Tokenizes a normalized string into a set of words. + */ +const tokenize = (s: string): Set => + new Set(normalize(s).split(' ').filter(t => t.length > 0)); + +/** + * Computes similarity between two strings using a tiered approach: + * 1. Exact match (after normalization) → 1.0 + * 2. One contains the other → 0.8 + * 3. Jaccard token overlap → 0.0–1.0 + */ +const similarity = (a: string, b: string): number => { + if (!a || !b) return 0; + + const na = normalize(a); + const nb = normalize(b); + + // Exact match + if (na === nb) return 1.0; + + // Containment + if (na.includes(nb) || nb.includes(na)) return 0.8; + + // Jaccard token overlap + const tokensA = tokenize(a); + const tokensB = tokenize(b); + + if (tokensA.size === 0 || tokensB.size === 0) return 0; + + let intersection = 0; + for (const t of tokensA) { + if (tokensB.has(t)) intersection++; + } + + const union = tokensA.size + tokensB.size - intersection; + return union > 0 ? intersection / union : 0; +}; + +/** + * Scores a (blank, executed) pair using weighted similarity. + * - Document name: 50% weight + * - Party name: 35% weight + * - Signatory name: 15% weight + */ +const scorePair = ( + blank: ExtractedSignaturePage, + executed: ExecutedSignaturePage +): number => { + const docScore = similarity(blank.documentName, executed.extractedDocumentName); + const partyScore = similarity(blank.partyName, executed.extractedPartyName); + const sigScore = similarity(blank.signatoryName, executed.extractedSignatoryName); + + return docScore * 0.5 + partyScore * 0.35 + sigScore * 0.15; +}; + +const MATCH_THRESHOLD = 0.4; + +/** + * Auto-matches executed pages to blank signature pages. + * + * Algorithm: + * 1. Score every possible (blank, executed) pair + * 2. Filter pairs below threshold (0.4) + * 3. Greedy assignment: sort by score descending, assign best pairs first + * (each blank and each executed page used only once) + * 4. Return AssemblyMatch[] with status 'auto-matched' + * + * @param blankPages - The extracted blank signature pages from the original documents + * @param executedPages - The uploaded executed (signed) signature pages + * @param existingMatches - Any existing matches to preserve (user-confirmed or user-overridden) + * @returns New auto-matched AssemblyMatch entries (does not include preserved existing matches) + */ +export const autoMatch = ( + blankPages: ExtractedSignaturePage[], + executedPages: ExecutedSignaturePage[], + existingMatches: AssemblyMatch[] = [] +): AssemblyMatch[] => { + // Determine which blanks and executed pages are already matched + // (preserve user-confirmed and user-overridden matches) + const preservedMatches = existingMatches.filter( + m => m.status === 'user-confirmed' || m.status === 'user-overridden' + ); + const usedBlankIds = new Set(preservedMatches.map(m => m.blankPageId)); + const usedExecutedIds = new Set(preservedMatches.map(m => m.executedPageId)); + + // Filter to only unmatched pages + const availableBlanks = blankPages.filter(b => !usedBlankIds.has(b.id)); + const availableExecuted = executedPages.filter( + e => !usedExecutedIds.has(e.id) && e.isConfirmedExecuted + ); + + // Score all possible pairs + const candidates: Array<{ + blank: ExtractedSignaturePage; + executed: ExecutedSignaturePage; + score: number; + }> = []; + + for (const blank of availableBlanks) { + for (const executed of availableExecuted) { + const score = scorePair(blank, executed); + if (score >= MATCH_THRESHOLD) { + candidates.push({ blank, executed, score }); + } + } + } + + // Sort by score descending (greedy assignment) + candidates.sort((a, b) => b.score - a.score); + + // Greedy: assign best-scoring pairs, each page used only once + const matchedBlankIds = new Set(); + const matchedExecutedIds = new Set(); + const newMatches: AssemblyMatch[] = []; + + for (const { blank, executed } of candidates) { + if (matchedBlankIds.has(blank.id) || matchedExecutedIds.has(executed.id)) { + continue; + } + + matchedBlankIds.add(blank.id); + matchedExecutedIds.add(executed.id); + + newMatches.push({ + blankPageId: blank.id, + executedPageId: executed.id, + documentId: blank.documentId, + documentName: blank.documentName, + pageIndex: blank.pageIndex, + partyName: blank.partyName, + signatoryName: blank.signatoryName, + status: 'auto-matched', + }); + } + + return newMatches; +}; + +/** + * Creates a manual match between a blank page and an executed page. + */ +export const createManualMatch = ( + blank: ExtractedSignaturePage, + executed: ExecutedSignaturePage +): AssemblyMatch => ({ + blankPageId: blank.id, + executedPageId: executed.id, + documentId: blank.documentId, + documentName: blank.documentName, + pageIndex: blank.pageIndex, + partyName: blank.partyName, + signatoryName: blank.signatoryName, + status: 'user-overridden', +}); diff --git a/services/pdfService.ts b/services/pdfService.ts index 5828edc..bc0dbbf 100644 --- a/services/pdfService.ts +++ b/services/pdfService.ts @@ -1,5 +1,5 @@ import { PDFDocument } from 'pdf-lib'; -import { ExtractedSignaturePage, ProcessedDocument, GroupingMode } from '../types'; +import { ExtractedSignaturePage, ProcessedDocument, GroupingMode, AssemblyMatch, ExecutedUpload } from '../types'; /** * Reads a file and returns its ArrayBuffer @@ -218,5 +218,126 @@ export const generateGroupedPdfs = async ( results[safeName] = pdfBytes; } + return results; +}; + +// --- Document Assembly Functions --- + +/** + * Assembles a single document by replacing blank signature pages with executed pages. + * Every page from the original document is copied; pages that have a replacement + * in the `replacements` map get swapped with the executed version. + * + * @param originalFile - The original agreement PDF file + * @param replacements - Map of 0-based page index → { executedFile, pageIndexInExecuted } + * @returns The assembled PDF as Uint8Array + */ +export const assembleDocument = async ( + originalFile: File, + replacements: Map +): Promise => { + const originalBuffer = await readFileAsArrayBuffer(originalFile); + const originalDoc = await PDFDocument.load(originalBuffer); + const assembledPdf = await PDFDocument.create(); + + // Cache loaded executed PDFDocuments to avoid reloading the same file + const executedDocCache = new Map(); + + const totalPages = originalDoc.getPageCount(); + + for (let i = 0; i < totalPages; i++) { + const replacement = replacements.get(i); + + if (replacement) { + // Swap in the executed page + let executedDoc = executedDocCache.get(replacement.executedFile); + if (!executedDoc) { + const execBuffer = await readFileAsArrayBuffer(replacement.executedFile); + executedDoc = await PDFDocument.load(execBuffer); + executedDocCache.set(replacement.executedFile, executedDoc); + } + + const [copiedPage] = await assembledPdf.copyPages(executedDoc, [replacement.pageIndexInExecuted]); + assembledPdf.addPage(copiedPage); + } else { + // Keep the original page + const [copiedPage] = await assembledPdf.copyPages(originalDoc, [i]); + assembledPdf.addPage(copiedPage); + } + } + + return await assembledPdf.save(); +}; + +/** + * Assembles all documents that have at least one matched executed page. + * Groups matches by documentId, builds replacement maps, calls assembleDocument for each. + * + * @param documents - All processed documents (must have non-null file) + * @param matches - All assembly matches (auto + manual) + * @param executedUploads - All uploaded executed files + * @returns Map of sanitized filename → assembled PDF bytes + */ +export const assembleAllDocuments = async ( + documents: ProcessedDocument[], + matches: AssemblyMatch[], + executedUploads: ExecutedUpload[] +): Promise> => { + const results: Record = {}; + + // Build a lookup: executedPageId → { file, pageIndexInSource } + const executedPageLookup = new Map(); + for (const upload of executedUploads) { + for (const execPage of upload.executedPages) { + executedPageLookup.set(execPage.id, { + file: upload.file, + pageIndexInSource: execPage.pageIndexInSource, + }); + } + } + + // Group matches by documentId + const matchesByDoc: Record = {}; + for (const match of matches) { + if (!matchesByDoc[match.documentId]) matchesByDoc[match.documentId] = []; + matchesByDoc[match.documentId].push(match); + } + + // Assemble each document + for (const [docId, docMatches] of Object.entries(matchesByDoc)) { + const doc = documents.find(d => d.id === docId); + if (!doc || !doc.file) continue; + + // Build the replacement map: pageIndex → executed source + const replacements = new Map(); + + for (const match of docMatches) { + const execInfo = executedPageLookup.get(match.executedPageId); + if (execInfo) { + replacements.set(match.pageIndex, { + executedFile: execInfo.file, + pageIndexInExecuted: execInfo.pageIndexInSource, + }); + } + } + + if (replacements.size === 0) continue; + + const assembledBytes = await assembleDocument(doc.file, replacements); + + // Sanitize filename + let safeName = doc.name.replace(/[/\\?%*:|"<>]/g, '').trim(); + if (!safeName) safeName = 'Assembled_Document'; + + // Add "_assembled" suffix before .pdf + if (safeName.toLowerCase().endsWith('.pdf')) { + safeName = safeName.slice(0, -4) + '_assembled.pdf'; + } else { + safeName += '_assembled.pdf'; + } + + results[safeName] = assembledBytes; + } + return results; }; \ No newline at end of file diff --git a/types.ts b/types.ts index 89361a4..55775e6 100644 --- a/types.ts +++ b/types.ts @@ -1,10 +1,13 @@ export interface ProcessedDocument { id: string; name: string; - file: File; + file: File | null; // null when restored from config (awaiting re-upload) pageCount: number; - status: 'pending' | 'processing' | 'completed' | 'error'; + status: 'pending' | 'processing' | 'completed' | 'error' | 'restored'; + wasRestored?: boolean; // true if this doc came from a saved config and needs merge-rescan + savedPages?: ExtractedSignaturePage[]; // holds the saved pages during rescan so they survive state clearing progress?: number; // 0 to 100 + errorMessage?: string; extractedPages: ExtractedSignaturePage[]; } @@ -25,6 +28,86 @@ export interface ExtractedSignaturePage { export type GroupingMode = 'agreement' | 'counterparty' | 'signatory'; +export type AppMode = 'extract' | 'assembly'; + +// --- Document Assembly Types --- + +export interface ExecutedPageExtraction { + isExecuted: boolean; + documentName: string; + signatures: Array<{ + partyName: string; + signatoryName: string; + capacity: string; + }>; +} + +export interface ExecutedSignaturePage { + id: string; + sourceUploadId: string; // References ExecutedUpload.id + sourceFileName: string; + pageIndexInSource: number; // 0-based within the uploaded file + pageNumber: number; // 1-based display + + // AI-extracted metadata + extractedDocumentName: string; // "Investors' Rights Agreement" etc. + extractedPartyName: string; + extractedSignatoryName: string; + extractedCapacity: string; + isConfirmedExecuted: boolean; // AI says it's actually signed + + thumbnailUrl: string; + originalWidth: number; + originalHeight: number; + + matchedBlankPageId: string | null; + matchConfidence: 'auto' | 'manual' | null; +} + +export interface ExecutedUpload { + id: string; + file: File; + fileName: string; + pageCount: number; + status: 'pending' | 'processing' | 'completed' | 'error'; + progress?: number; + errorMessage?: string; + executedPages: ExecutedSignaturePage[]; +} + +export interface AssemblyMatch { + blankPageId: string; // ExtractedSignaturePage.id + executedPageId: string; // ExecutedSignaturePage.id + documentId: string; // ProcessedDocument.id + documentName: string; + pageIndex: number; // Exact page position in original PDF + partyName: string; + signatoryName: string; + status: 'auto-matched' | 'user-confirmed' | 'user-overridden'; +} + +export interface SavedConfiguration { + version: 1; + savedAt: string; + groupingMode: GroupingMode; + documents: Array<{ + id: string; + name: string; + pageCount: number; + pdfBase64?: string; // Embedded PDF file data + }>; + extractedPages: ExtractedSignaturePage[]; + // Assembly state (optional for backward compatibility with older configs) + executedUploads?: Array<{ + id: string; + fileName: string; + pageCount: number; + executedPages: ExecutedSignaturePage[]; + pdfBase64?: string; // Embedded PDF file data + }>; + assemblyMatches?: AssemblyMatch[]; +} + export interface SignatureBlockExtraction { isSignaturePage: boolean; signatures: Array<{ diff --git a/vite.config.ts b/vite.config.ts index ee5fb8d..8187777 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,12 @@ export default defineConfig(({ mode }) => { server: { port: 3000, host: '0.0.0.0', + proxy: { + '/api': { + target: 'http://localhost:8787', + changeOrigin: true, + }, + }, }, plugins: [react()], define: {
DocumentParty CapacitySignatory Copies
{p.documentName}{p.partyName} {p.capacity}{p.signatoryName || '-'} {p.copies}