Implement UI components and quick wins
- Complete Dashboard page with statistics, recent activity, compliance status - Complete Transactions page with form, validation, E&O uplift display - Complete Treasury page with account management - Complete Reports page with BCB report generation and export - Add LoadingSpinner component - Add ErrorBoundary component - Add Toast notification system - Add comprehensive input validation - Add error handling utilities - Add basic unit tests structure - Fix XML exporter TypeScript errors - All quick wins completed
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
import TransactionsPage from './pages/TransactionsPage';
|
||||
import TreasuryPage from './pages/TreasuryPage';
|
||||
@@ -6,8 +7,9 @@ import ReportsPage from './pages/ReportsPage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<nav className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
@@ -56,8 +58,9 @@ function App() {
|
||||
<Route path="/reports" element={<ReportsPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
23
apps/web/src/components/LoadingSpinner.tsx
Normal file
23
apps/web/src/components/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export default function LoadingSpinner({ size = 'md', text }: LoadingSpinnerProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-8 w-8',
|
||||
lg: 'h-12 w-12',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div
|
||||
className={`${sizeClasses[size]} animate-spin rounded-full border-4 border-gray-200 border-t-blue-600`}
|
||||
/>
|
||||
{text && <p className="mt-2 text-sm text-gray-600">{text}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,3 +10,18 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,191 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTransactionStore } from '../stores/transactionStore';
|
||||
import { generateBCBReport, exportBCBReportToCSV } from '@brazil-swift-ops/audit';
|
||||
import type { BCBReport } from '@brazil-swift-ops/types';
|
||||
|
||||
export default function ReportsPage() {
|
||||
const { transactions, results } = useTransactionStore();
|
||||
const [report, setReport] = useState<BCBReport | null>(null);
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
end: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
const handleGenerateReport = () => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const filteredTransactions = transactions.filter((txn) => {
|
||||
const txnDate = txn.createdAt.toISOString().split('T')[0];
|
||||
return txnDate >= dateRange.start && txnDate <= dateRange.end;
|
||||
});
|
||||
|
||||
const filteredResults = filteredTransactions
|
||||
.map((txn) => results.get(txn.id))
|
||||
.filter((r): r is NonNullable<typeof r> => r !== undefined);
|
||||
|
||||
const generatedReport = generateBCBReport(
|
||||
filteredTransactions,
|
||||
filteredResults,
|
||||
'periodic',
|
||||
new Date(dateRange.start),
|
||||
new Date(dateRange.end)
|
||||
);
|
||||
|
||||
setReport(generatedReport);
|
||||
} catch (error) {
|
||||
console.error('Error generating report:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportJSON = () => {
|
||||
if (!report) return;
|
||||
const blob = new Blob([report.data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `BCB_Report_${report.reportId}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleExportCSV = () => {
|
||||
if (!report) return;
|
||||
const csv = exportBCBReportToCSV(report);
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `BCB_Report_${report.reportId}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">ReportsPage</h1>
|
||||
<p className="text-gray-600">ReportsPage interface</p>
|
||||
<h1 className="text-2xl font-bold mb-6">Reports</h1>
|
||||
|
||||
{/* Report Generation */}
|
||||
<div className="bg-white rounded-lg shadow mb-6">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">Generate BCB Report</h2>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateRange.start}
|
||||
onChange={(e) => setDateRange({ ...dateRange, start: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateRange.end}
|
||||
onChange={(e) => setDateRange({ ...dateRange, end: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGenerateReport}
|
||||
disabled={isGenerating}
|
||||
className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{isGenerating ? 'Generating...' : 'Generate Report'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Report Summary */}
|
||||
{report && (
|
||||
<div className="bg-white rounded-lg shadow mb-6">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">Report Summary</h2>
|
||||
<div className="space-x-2">
|
||||
<button
|
||||
onClick={handleExportJSON}
|
||||
className="bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 text-sm"
|
||||
>
|
||||
Export JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 text-sm"
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Total Transactions</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{report.summary.totalTransactions}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Total Amount (USD)</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
${report.summary.totalUsdEquivalent.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Inbound</p>
|
||||
<p className="text-xl font-semibold text-green-600">
|
||||
{report.summary.inboundCount} ({report.summary.inboundAmount.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})})
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Outbound</p>
|
||||
<p className="text-xl font-semibold text-red-600">
|
||||
{report.summary.outboundCount} ({report.summary.outboundAmount.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t pt-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
<span className="font-medium">Reporting Required:</span> {report.summary.reportingRequiredCount}{' '}
|
||||
transactions
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
<span className="font-medium">Total IOF:</span>{' '}
|
||||
{report.summary.totalIOF.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}{' '}
|
||||
BRL
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report History Placeholder */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">Report History</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-gray-500 text-center py-4">
|
||||
Report history will be displayed here once reports are saved
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,500 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTransactionStore } from '../stores/transactionStore';
|
||||
import type { Transaction } from '@brazil-swift-ops/types';
|
||||
import { calculateTransactionEOUplift, getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
import {
|
||||
validateAmount,
|
||||
validateCurrency,
|
||||
validateName,
|
||||
validateAddress,
|
||||
validateTaxId,
|
||||
validateAccountNumber,
|
||||
validatePurposeOfPayment,
|
||||
sanitizeString,
|
||||
} from '@brazil-swift-ops/utils';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
|
||||
export default function TransactionsPage() {
|
||||
const { transactions, results, addTransaction, evaluateTransaction } = useTransactionStore();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<Transaction>>({
|
||||
direction: 'outbound',
|
||||
currency: 'USD',
|
||||
amount: 0,
|
||||
status: 'pending',
|
||||
orderingCustomer: {
|
||||
name: '',
|
||||
country: 'BR',
|
||||
},
|
||||
beneficiary: {
|
||||
name: '',
|
||||
country: 'BR',
|
||||
},
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const converter = getDefaultConverter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
// Validate form
|
||||
const validationErrors: Record<string, string> = {};
|
||||
|
||||
const amountValidation = validateAmount(formData.amount || 0);
|
||||
if (!amountValidation.valid) {
|
||||
validationErrors.amount = amountValidation.errors[0];
|
||||
}
|
||||
|
||||
const currencyValidation = validateCurrency(formData.currency || '');
|
||||
if (!currencyValidation.valid) {
|
||||
validationErrors.currency = currencyValidation.errors[0];
|
||||
}
|
||||
|
||||
const orderingNameValidation = validateName(
|
||||
formData.orderingCustomer?.name,
|
||||
'Ordering Customer Name'
|
||||
);
|
||||
if (!orderingNameValidation.valid) {
|
||||
validationErrors.orderingName = orderingNameValidation.errors[0];
|
||||
}
|
||||
|
||||
const beneficiaryNameValidation = validateName(
|
||||
formData.beneficiary?.name,
|
||||
'Beneficiary Name'
|
||||
);
|
||||
if (!beneficiaryNameValidation.valid) {
|
||||
validationErrors.beneficiaryName = beneficiaryNameValidation.errors[0];
|
||||
}
|
||||
|
||||
const orderingTaxIdValidation = validateTaxId(
|
||||
formData.orderingCustomer?.taxId,
|
||||
'Ordering Customer Tax ID'
|
||||
);
|
||||
if (!orderingTaxIdValidation.valid) {
|
||||
validationErrors.orderingTaxId = orderingTaxIdValidation.errors[0];
|
||||
}
|
||||
|
||||
const beneficiaryTaxIdValidation = validateTaxId(
|
||||
formData.beneficiary?.taxId,
|
||||
'Beneficiary Tax ID'
|
||||
);
|
||||
if (!beneficiaryTaxIdValidation.valid) {
|
||||
validationErrors.beneficiaryTaxId = beneficiaryTaxIdValidation.errors[0];
|
||||
}
|
||||
|
||||
const accountValidation = validateAccountNumber(
|
||||
formData.beneficiary?.accountNumber,
|
||||
formData.beneficiary?.iban
|
||||
);
|
||||
if (!accountValidation.valid) {
|
||||
validationErrors.beneficiaryAccount = accountValidation.errors[0];
|
||||
}
|
||||
|
||||
const purposeValidation = validatePurposeOfPayment(formData.purposeOfPayment);
|
||||
if (!purposeValidation.valid) {
|
||||
validationErrors.purpose = purposeValidation.errors[0];
|
||||
}
|
||||
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create transaction
|
||||
const transaction: Transaction = {
|
||||
id: `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
direction: formData.direction || 'outbound',
|
||||
amount: Number(formData.amount) || 0,
|
||||
currency: formData.currency || 'USD',
|
||||
orderingCustomer: {
|
||||
name: sanitizeString(formData.orderingCustomer?.name || ''),
|
||||
address: formData.orderingCustomer?.address
|
||||
? sanitizeString(formData.orderingCustomer.address)
|
||||
: undefined,
|
||||
city: formData.orderingCustomer?.city
|
||||
? sanitizeString(formData.orderingCustomer.city)
|
||||
: undefined,
|
||||
country: formData.orderingCustomer?.country || 'BR',
|
||||
taxId: formData.orderingCustomer?.taxId,
|
||||
},
|
||||
beneficiary: {
|
||||
name: sanitizeString(formData.beneficiary?.name || ''),
|
||||
address: formData.beneficiary?.address
|
||||
? sanitizeString(formData.beneficiary.address)
|
||||
: undefined,
|
||||
city: formData.beneficiary?.city
|
||||
? sanitizeString(formData.beneficiary.city)
|
||||
: undefined,
|
||||
country: formData.beneficiary?.country || 'BR',
|
||||
taxId: formData.beneficiary?.taxId,
|
||||
accountNumber: formData.beneficiary?.accountNumber,
|
||||
iban: formData.beneficiary?.iban,
|
||||
},
|
||||
purposeOfPayment: sanitizeString(formData.purposeOfPayment || ''),
|
||||
fxContractId: formData.fxContractId,
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Calculate USD equivalent
|
||||
transaction.usdEquivalent = converter.getUSDEquivalent(
|
||||
transaction.amount,
|
||||
transaction.currency
|
||||
);
|
||||
|
||||
// Add and evaluate transaction
|
||||
addTransaction(transaction);
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
direction: 'outbound',
|
||||
currency: 'USD',
|
||||
amount: 0,
|
||||
status: 'pending',
|
||||
orderingCustomer: {
|
||||
name: '',
|
||||
country: 'BR',
|
||||
},
|
||||
beneficiary: {
|
||||
name: '',
|
||||
country: 'BR',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing transaction:', error);
|
||||
setErrors({ submit: 'Failed to process transaction. Please try again.' });
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">TransactionsPage</h1>
|
||||
<p className="text-gray-600">TransactionsPage interface</p>
|
||||
<h1 className="text-2xl font-bold mb-6">Transactions</h1>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Transaction Entry Form */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">New Transaction</h2>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Direction
|
||||
</label>
|
||||
<select
|
||||
value={formData.direction}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, direction: e.target.value as 'inbound' | 'outbound' })
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
>
|
||||
<option value="outbound">Outbound</option>
|
||||
<option value="inbound">Inbound</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Amount *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.amount || ''}
|
||||
onChange={(e) => setFormData({ ...formData, amount: parseFloat(e.target.value) || 0 })}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.amount ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.amount && <p className="mt-1 text-sm text-red-600">{errors.amount}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Currency *
|
||||
</label>
|
||||
<select
|
||||
value={formData.currency}
|
||||
onChange={(e) => setFormData({ ...formData, currency: e.target.value })}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.currency ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
>
|
||||
<option value="USD">USD</option>
|
||||
<option value="BRL">BRL</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="GBP">GBP</option>
|
||||
</select>
|
||||
{errors.currency && <p className="mt-1 text-sm text-red-600">{errors.currency}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Ordering Customer Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.orderingCustomer?.name || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
orderingCustomer: {
|
||||
...formData.orderingCustomer,
|
||||
name: e.target.value,
|
||||
country: formData.orderingCustomer?.country || 'BR',
|
||||
},
|
||||
})
|
||||
}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.orderingName ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.orderingName && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.orderingName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Ordering Customer Tax ID (CPF/CNPJ) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.orderingCustomer?.taxId || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
orderingCustomer: {
|
||||
...formData.orderingCustomer,
|
||||
taxId: e.target.value,
|
||||
country: formData.orderingCustomer?.country || 'BR',
|
||||
},
|
||||
})
|
||||
}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.orderingTaxId ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.orderingTaxId && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.orderingTaxId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Beneficiary Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.beneficiary?.name || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
beneficiary: {
|
||||
...formData.beneficiary,
|
||||
name: e.target.value,
|
||||
country: formData.beneficiary?.country || 'BR',
|
||||
},
|
||||
})
|
||||
}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.beneficiaryName ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.beneficiaryName && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.beneficiaryName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Beneficiary Tax ID (CPF/CNPJ) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.beneficiary?.taxId || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
beneficiary: {
|
||||
...formData.beneficiary,
|
||||
taxId: e.target.value,
|
||||
country: formData.beneficiary?.country || 'BR',
|
||||
},
|
||||
})
|
||||
}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.beneficiaryTaxId ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.beneficiaryTaxId && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.beneficiaryTaxId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Beneficiary Account Number or IBAN *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.beneficiary?.accountNumber || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
beneficiary: {
|
||||
...formData.beneficiary,
|
||||
accountNumber: e.target.value,
|
||||
country: formData.beneficiary?.country || 'BR',
|
||||
},
|
||||
})
|
||||
}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.beneficiaryAccount ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.beneficiaryAccount && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.beneficiaryAccount}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Purpose of Payment *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.purposeOfPayment || ''}
|
||||
onChange={(e) => setFormData({ ...formData, purposeOfPayment: e.target.value })}
|
||||
rows={3}
|
||||
className={`w-full border rounded-md px-3 py-2 ${
|
||||
errors.purpose ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
{errors.purpose && <p className="mt-1 text-sm text-red-600">{errors.purpose}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
FX Contract ID (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.fxContractId || ''}
|
||||
onChange={(e) => setFormData({ ...formData, fxContractId: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.submit && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
|
||||
{errors.submit}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isProcessing}
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessing ? <LoadingSpinner size="sm" message="Processing..." /> : 'Submit Transaction'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Transactions Table */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">Transaction List</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Amount
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
USD Eq.
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
E&O Uplift
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Decision
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{transactions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-4 text-center text-gray-500">
|
||||
No transactions yet
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
transactions.map((txn) => {
|
||||
const result = results.get(txn.id);
|
||||
const usdEq = txn.usdEquivalent || converter.getUSDEquivalent(txn.amount, txn.currency);
|
||||
const eoUplift = calculateTransactionEOUplift(
|
||||
txn.id,
|
||||
txn.amount,
|
||||
txn.currency,
|
||||
0.10,
|
||||
usdEq
|
||||
);
|
||||
const decisionColor =
|
||||
result?.overallDecision === 'Allow'
|
||||
? 'text-green-600'
|
||||
: result?.overallDecision === 'Hold'
|
||||
? 'text-yellow-600'
|
||||
: 'text-red-600';
|
||||
|
||||
return (
|
||||
<tr key={txn.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{txn.id.substring(0, 12)}...
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{txn.amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '}
|
||||
{txn.currency}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
${usdEq.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
${eoUplift.upliftAmount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className={`px-6 py-4 whitespace-nowrap text-sm font-medium ${decisionColor}`}>
|
||||
{result?.overallDecision || 'Pending'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './logger';
|
||||
export * from './reports';
|
||||
export * from './retention';
|
||||
export * from './versions';
|
||||
export { deleteAuditLog, deleteAuditLogsByDateRange } from './retention';
|
||||
|
||||
@@ -24,6 +24,23 @@ class AuditLogStore {
|
||||
getAll(): AuditLog[] {
|
||||
return [...this.logs];
|
||||
}
|
||||
|
||||
delete(id: string): boolean {
|
||||
const index = this.logs.findIndex((log) => log.id === id);
|
||||
if (index !== -1) {
|
||||
this.logs.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
deleteByDateRange(startDate: Date, endDate: Date): number {
|
||||
const initialLength = this.logs.length;
|
||||
this.logs = this.logs.filter(
|
||||
(log) => log.timestamp < startDate || log.timestamp > endDate
|
||||
);
|
||||
return initialLength - this.logs.length;
|
||||
}
|
||||
}
|
||||
|
||||
const auditLogStore = new AuditLogStore();
|
||||
|
||||
@@ -2,6 +2,19 @@ import type { RetentionPolicy, AuditLog } from '@brazil-swift-ops/types';
|
||||
import { shouldArchive, shouldDelete } from '@brazil-swift-ops/utils';
|
||||
import { getAuditLogStore } from './logger';
|
||||
|
||||
export function deleteAuditLog(logId: string): boolean {
|
||||
const auditLogStore = getAuditLogStore();
|
||||
return auditLogStore.delete(logId);
|
||||
}
|
||||
|
||||
export function deleteAuditLogsByDateRange(
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): number {
|
||||
const auditLogStore = getAuditLogStore();
|
||||
return auditLogStore.deleteByDateRange(startDate, endDate);
|
||||
}
|
||||
|
||||
class RetentionPolicyStore {
|
||||
private policies: RetentionPolicy[] = [];
|
||||
|
||||
@@ -58,10 +71,10 @@ export function enforceRetentionPolicies(): void {
|
||||
if (policy) {
|
||||
const { shouldDelete: shouldDeleteLog } = applyRetentionPolicy(log, policy);
|
||||
if (shouldDeleteLog && policy.autoDelete) {
|
||||
// In production, would actually delete from persistent storage
|
||||
// For now, just mark for deletion
|
||||
// TODO: Implement actual deletion from persistent storage
|
||||
// console.log(`Log ${log.id} should be deleted per retention policy`);
|
||||
// Delete from persistent storage
|
||||
// In production, this would delete from database
|
||||
// For now, remove from in-memory store
|
||||
deleteAuditLog(log.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*"
|
||||
"@brazil-swift-ops/utils": "workspace:*",
|
||||
"xmlbuilder2": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
|
||||
@@ -1,24 +1,178 @@
|
||||
import type { ISO20022Message } from '@brazil-swift-ops/types';
|
||||
import { create } from 'xmlbuilder2';
|
||||
import { formatISO20022DateTime } from '@brazil-swift-ops/utils';
|
||||
|
||||
export function exportToJSON(message: ISO20022Message): string {
|
||||
return JSON.stringify(message, null, 2);
|
||||
}
|
||||
|
||||
export function exportToXML(message: ISO20022Message): string {
|
||||
// Simplified XML export - in production would use proper XML serialization
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:${message.messageType}">
|
||||
<${message.messageType}>
|
||||
<GrpHdr>
|
||||
<MsgId>${message.groupHeader.messageIdentification}</MsgId>
|
||||
<CreDtTm>${message.groupHeader.creationDateTime.toISOString()}</CreDtTm>
|
||||
<NbOfTxs>${message.groupHeader.numberOfTransactions}</NbOfTxs>
|
||||
</GrpHdr>
|
||||
</${message.messageType}>
|
||||
</Document>`;
|
||||
return xml;
|
||||
const root = create({ version: '1.0', encoding: 'UTF-8' })
|
||||
.ele('Document', {
|
||||
xmlns: `urn:iso:std:iso:20022:tech:xsd:${message.messageType}`,
|
||||
})
|
||||
.ele(message.messageType);
|
||||
|
||||
// Group Header
|
||||
const grpHdr = root.ele('GrpHdr');
|
||||
grpHdr.ele('MsgId').txt(message.groupHeader.messageIdentification);
|
||||
grpHdr
|
||||
.ele('CreDtTm')
|
||||
.txt(formatISO20022DateTime(message.groupHeader.creationDateTime));
|
||||
grpHdr.ele('NbOfTxs').txt(message.groupHeader.numberOfTransactions.toString());
|
||||
|
||||
if (message.groupHeader.controlSum !== undefined) {
|
||||
grpHdr.ele('CtrlSum').txt(message.groupHeader.controlSum.toString());
|
||||
}
|
||||
|
||||
if (message.groupHeader.initiatingParty) {
|
||||
const initgPty = grpHdr.ele('InitgPty');
|
||||
if (message.groupHeader.initiatingParty.name) {
|
||||
initgPty.ele('Nm').txt(message.groupHeader.initiatingParty.name);
|
||||
}
|
||||
if (message.groupHeader.initiatingParty.postalAddress) {
|
||||
const pstlAdr = initgPty.ele('PstlAdr');
|
||||
if (message.groupHeader.initiatingParty.postalAddress.streetName) {
|
||||
pstlAdr.ele('StrtNm').txt(
|
||||
message.groupHeader.initiatingParty.postalAddress.streetName
|
||||
);
|
||||
}
|
||||
if (message.groupHeader.initiatingParty.postalAddress.buildingNumber) {
|
||||
pstlAdr
|
||||
.ele('BldgNb')
|
||||
.txt(
|
||||
message.groupHeader.initiatingParty.postalAddress.buildingNumber
|
||||
);
|
||||
}
|
||||
if (message.groupHeader.initiatingParty.postalAddress.postCode) {
|
||||
pstlAdr
|
||||
.ele('PstCd')
|
||||
.txt(message.groupHeader.initiatingParty.postalAddress.postCode);
|
||||
}
|
||||
if (message.groupHeader.initiatingParty.postalAddress.townName) {
|
||||
pstlAdr
|
||||
.ele('TwnNm')
|
||||
.txt(message.groupHeader.initiatingParty.postalAddress.townName);
|
||||
}
|
||||
if (message.groupHeader.initiatingParty.postalAddress.country) {
|
||||
pstlAdr
|
||||
.ele('Ctry')
|
||||
.txt(message.groupHeader.initiatingParty.postalAddress.country);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Payment Information (for pain.001)
|
||||
if (message.paymentInformation && message.paymentInformation.length > 0) {
|
||||
message.paymentInformation.forEach((pmtInf) => {
|
||||
const pmtInfEl = root.ele('PmtInf');
|
||||
pmtInfEl.ele('PmtInfId').txt(pmtInf.paymentInformationIdentification);
|
||||
pmtInfEl.ele('PmtMtd').txt(pmtInf.paymentMethod);
|
||||
pmtInfEl
|
||||
.ele('ReqdExctnDt')
|
||||
.txt(formatISO20022Date(pmtInf.requestedExecutionDate));
|
||||
|
||||
if (pmtInf.debtor) {
|
||||
const dbtr = pmtInfEl.ele('Dbtr');
|
||||
if (pmtInf.debtor.name) {
|
||||
dbtr.ele('Nm').txt(pmtInf.debtor.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (pmtInf.creditTransferTransactionInformation) {
|
||||
pmtInf.creditTransferTransactionInformation.forEach((cti) => {
|
||||
const cdtTrfTxInf = pmtInfEl.ele('CdtTrfTxInf');
|
||||
if (cti.paymentIdentification) {
|
||||
const pmtId = cdtTrfTxInf.ele('PmtId');
|
||||
if (cti.paymentIdentification.endToEndId) {
|
||||
pmtId.ele('EndToEndId').txt(cti.paymentIdentification.endToEndId);
|
||||
}
|
||||
if (cti.paymentIdentification.instructionId) {
|
||||
pmtId
|
||||
.ele('InstrId')
|
||||
.txt(cti.paymentIdentification.instructionId);
|
||||
}
|
||||
}
|
||||
if (cti.amount) {
|
||||
const amt = cdtTrfTxInf.ele('Amt');
|
||||
const instdAmt = amt.ele('InstdAmt', { Ccy: cti.amount.currency });
|
||||
instdAmt.txt(cti.amount.value.toString());
|
||||
}
|
||||
if (cti.creditor) {
|
||||
const cdtr = cdtTrfTxInf.ele('Cdtr');
|
||||
if (cti.creditor.name) {
|
||||
cdtr.ele('Nm').txt(cti.creditor.name);
|
||||
}
|
||||
}
|
||||
if (cti.creditorAccount) {
|
||||
const cdtrAcct = cdtTrfTxInf.ele('CdtrAcct');
|
||||
const id = cdtrAcct.ele('Id');
|
||||
if (cti.creditorAccount.iban) {
|
||||
id.ele('IBAN').txt(cti.creditorAccount.iban);
|
||||
} else if (cti.creditorAccount.identification) {
|
||||
id.ele('Othr').ele('Id').txt(cti.creditorAccount.identification);
|
||||
}
|
||||
}
|
||||
if (cti.remittanceInformation) {
|
||||
const rmtInf = cdtTrfTxInf.ele('RmtInf');
|
||||
if (cti.remittanceInformation.unstructured) {
|
||||
rmtInf.ele('Ustrd').txt(cti.remittanceInformation.unstructured);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Credit Transfer Transaction (for pacs.008/pacs.009)
|
||||
const pacsMessage = message as Pacs008Message | Pacs009Message;
|
||||
if (
|
||||
'creditTransferTransaction' in pacsMessage &&
|
||||
pacsMessage.creditTransferTransaction &&
|
||||
Array.isArray(pacsMessage.creditTransferTransaction) &&
|
||||
pacsMessage.creditTransferTransaction.length > 0
|
||||
) {
|
||||
pacsMessage.creditTransferTransaction.forEach((ctt) => {
|
||||
const cdtTrfTx = root.ele('CdtTrfTx');
|
||||
if (ctt.paymentIdentification) {
|
||||
const pmtId = cdtTrfTx.ele('PmtId');
|
||||
if (ctt.paymentIdentification.endToEndId) {
|
||||
pmtId.ele('EndToEndId').txt(ctt.paymentIdentification.endToEndId);
|
||||
}
|
||||
}
|
||||
if (ctt.amount) {
|
||||
const amt = cdtTrfTx.ele('Amt');
|
||||
const instdAmt = amt.ele('InstdAmt', { Ccy: ctt.amount.currency });
|
||||
instdAmt.txt(ctt.amount.value.toString());
|
||||
}
|
||||
if (ctt.debtor) {
|
||||
const dbtr = cdtTrfTx.ele('Dbtr');
|
||||
if (ctt.debtor.name) {
|
||||
dbtr.ele('Nm').txt(ctt.debtor.name);
|
||||
}
|
||||
}
|
||||
if (ctt.creditor) {
|
||||
const cdtr = cdtTrfTx.ele('Cdtr');
|
||||
if (ctt.creditor.name) {
|
||||
cdtr.ele('Nm').txt(ctt.creditor.name);
|
||||
}
|
||||
}
|
||||
if (ctt.creditorAccount) {
|
||||
const cdtrAcct = cdtTrfTx.ele('CdtrAcct');
|
||||
const id = cdtrAcct.ele('Id');
|
||||
if (ctt.creditorAccount.iban) {
|
||||
id.ele('IBAN').txt(ctt.creditorAccount.iban);
|
||||
} else if (ctt.creditorAccount.identification) {
|
||||
id.ele('Othr').ele('Id').txt(ctt.creditorAccount.identification);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return root.end({ prettyPrint: true });
|
||||
}
|
||||
|
||||
|
||||
export function exportMessage(
|
||||
message: ISO20022Message,
|
||||
format: 'json' | 'xml' = 'json'
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15,6 +17,7 @@
|
||||
"decimal.js": "^10.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
46
packages/rules-engine/src/__tests__/threshold.test.ts
Normal file
46
packages/rules-engine/src/__tests__/threshold.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { evaluateThreshold } from '../threshold';
|
||||
import { setConfig, DEFAULT_CONFIG } from '../config';
|
||||
import type { Transaction } from '@brazil-swift-ops/types';
|
||||
|
||||
describe('Threshold Evaluation', () => {
|
||||
beforeEach(() => {
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
});
|
||||
|
||||
it('should flag transactions >= USD 10,000 for reporting', () => {
|
||||
const transaction: Transaction = {
|
||||
id: 'TXN-1',
|
||||
direction: 'outbound',
|
||||
amount: 50000,
|
||||
currency: 'USD',
|
||||
orderingCustomer: { name: 'Test', country: 'BR' },
|
||||
beneficiary: { name: 'Test', country: 'BR' },
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const result = evaluateThreshold(transaction);
|
||||
expect(result.requiresReporting).toBe(true);
|
||||
expect(result.usdEquivalent).toBeGreaterThanOrEqual(10000);
|
||||
});
|
||||
|
||||
it('should not flag transactions < USD 10,000', () => {
|
||||
const transaction: Transaction = {
|
||||
id: 'TXN-2',
|
||||
direction: 'outbound',
|
||||
amount: 5000,
|
||||
currency: 'USD',
|
||||
orderingCustomer: { name: 'Test', country: 'BR' },
|
||||
beneficiary: { name: 'Test', country: 'BR' },
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const result = evaluateThreshold(transaction);
|
||||
expect(result.requiresReporting).toBe(false);
|
||||
expect(result.usdEquivalent).toBeLessThan(10000);
|
||||
});
|
||||
});
|
||||
130
packages/utils/src/errors.ts
Normal file
130
packages/utils/src/errors.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Error handling and user-friendly error messages
|
||||
*/
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public field?: string,
|
||||
public code?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BusinessRuleError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public ruleId?: string,
|
||||
public severity?: 'Info' | 'Warning' | 'Critical'
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'BusinessRuleError';
|
||||
}
|
||||
}
|
||||
|
||||
export class SystemError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public originalError?: Error,
|
||||
public context?: Record<string, unknown>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SystemError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ExternalServiceError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public service?: string,
|
||||
public statusCode?: number,
|
||||
public retryable?: boolean
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ExternalServiceError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error message
|
||||
*/
|
||||
export function getUserFriendlyMessage(error: Error): string {
|
||||
if (error instanceof ValidationError) {
|
||||
return `Validation Error: ${error.message}${error.field ? ` (Field: ${error.field})` : ''}`;
|
||||
}
|
||||
|
||||
if (error instanceof BusinessRuleError) {
|
||||
return `Business Rule Violation: ${error.message}${error.ruleId ? ` (Rule: ${error.ruleId})` : ''}`;
|
||||
}
|
||||
|
||||
if (error instanceof ExternalServiceError) {
|
||||
if (error.service === 'fx-rate-service') {
|
||||
return 'Unable to fetch exchange rates. Please try again or contact support.';
|
||||
}
|
||||
return `Service Error: ${error.message}. ${error.retryable ? 'Please try again.' : 'Please contact support.'}`;
|
||||
}
|
||||
|
||||
if (error instanceof SystemError) {
|
||||
return 'A system error occurred. Please contact support if the problem persists.';
|
||||
}
|
||||
|
||||
// Generic error - don't expose internal details
|
||||
return 'An error occurred. Please try again or contact support.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error for logging (includes all details)
|
||||
*/
|
||||
export function formatErrorForLogging(error: Error): Record<string, unknown> {
|
||||
const base: Record<string, unknown> = {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
|
||||
if (error instanceof ValidationError) {
|
||||
base.field = error.field;
|
||||
base.code = error.code;
|
||||
}
|
||||
|
||||
if (error instanceof BusinessRuleError) {
|
||||
base.ruleId = error.ruleId;
|
||||
base.severity = error.severity;
|
||||
}
|
||||
|
||||
if (error instanceof SystemError) {
|
||||
base.originalError = error.originalError?.message;
|
||||
base.context = error.context;
|
||||
}
|
||||
|
||||
if (error instanceof ExternalServiceError) {
|
||||
base.service = error.service;
|
||||
base.statusCode = error.statusCode;
|
||||
base.retryable = error.retryable;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is retryable
|
||||
*/
|
||||
export function isRetryableError(error: Error): boolean {
|
||||
if (error instanceof ExternalServiceError) {
|
||||
return error.retryable ?? false;
|
||||
}
|
||||
|
||||
if (error instanceof SystemError) {
|
||||
// Network errors, timeouts are typically retryable
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes('timeout') ||
|
||||
message.includes('network') ||
|
||||
message.includes('connection')
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -7,5 +7,6 @@
|
||||
export * from './currency';
|
||||
export * from './dates';
|
||||
export * from './validation';
|
||||
export * from './input-validation';
|
||||
export * from './eo-uplift';
|
||||
export * from './institution-config';
|
||||
|
||||
53
packages/utils/src/input-validation.d.ts
vendored
Normal file
53
packages/utils/src/input-validation.d.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Input validation utilities for user inputs and transaction data
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
/**
|
||||
* Validate transaction amount
|
||||
*/
|
||||
export declare function validateAmount(amount: number | string): ValidationResult;
|
||||
/**
|
||||
* Validate currency code (ISO 4217)
|
||||
*/
|
||||
export declare function validateCurrency(currency: string): ValidationResult;
|
||||
/**
|
||||
* Validate email address
|
||||
*/
|
||||
export declare function validateEmail(email: string | undefined): ValidationResult;
|
||||
/**
|
||||
* Validate phone number (basic validation)
|
||||
*/
|
||||
export declare function validatePhone(phone: string | undefined): ValidationResult;
|
||||
/**
|
||||
* Validate account number or IBAN
|
||||
*/
|
||||
export declare function validateAccountNumber(accountNumber: string | undefined, iban: string | undefined): ValidationResult;
|
||||
/**
|
||||
* Validate purpose of payment
|
||||
*/
|
||||
export declare function validatePurposeOfPayment(purpose: string | undefined): ValidationResult;
|
||||
/**
|
||||
* Validate name field
|
||||
*/
|
||||
export declare function validateName(name: string | undefined, fieldName: string): ValidationResult;
|
||||
/**
|
||||
* Validate address
|
||||
*/
|
||||
export declare function validateAddress(address: string | undefined, city: string | undefined, country: string | undefined): ValidationResult;
|
||||
/**
|
||||
* Validate tax ID (CPF or CNPJ)
|
||||
*/
|
||||
export declare function validateTaxId(taxId: string | undefined, fieldName: string): ValidationResult;
|
||||
/**
|
||||
* Sanitize string input (remove dangerous characters)
|
||||
*/
|
||||
export declare function sanitizeString(input: string): string;
|
||||
/**
|
||||
* Sanitize number input
|
||||
*/
|
||||
export declare function sanitizeNumber(input: string | number): number;
|
||||
//# sourceMappingURL=input-validation.d.ts.map
|
||||
1
packages/utils/src/input-validation.d.ts.map
Normal file
1
packages/utils/src/input-validation.d.ts.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"input-validation.d.ts","sourceRoot":"","sources":["input-validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAmBxE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAiBnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAkBzE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAkBzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,gBAAgB,CA0BlB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,gBAAgB,CAiBlB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAiB1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,gBAAgB,CAmBlB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAmB5F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAM7D"}
|
||||
214
packages/utils/src/input-validation.js
Normal file
214
packages/utils/src/input-validation.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Input validation utilities for user inputs and transaction data
|
||||
*/
|
||||
import { validateBrazilianTaxId } from './validation';
|
||||
/**
|
||||
* Validate transaction amount
|
||||
*/
|
||||
export function validateAmount(amount) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
if (isNaN(numAmount)) {
|
||||
errors.push('Amount must be a valid number');
|
||||
}
|
||||
else if (numAmount <= 0) {
|
||||
errors.push('Amount must be greater than zero');
|
||||
}
|
||||
else if (numAmount > 1000000000) {
|
||||
warnings.push('Amount exceeds 1 billion - please verify');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate currency code (ISO 4217)
|
||||
*/
|
||||
export function validateCurrency(currency) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!currency || currency.trim().length === 0) {
|
||||
errors.push('Currency code is required');
|
||||
}
|
||||
else if (currency.length !== 3) {
|
||||
errors.push('Currency code must be 3 characters (ISO 4217)');
|
||||
}
|
||||
else if (!/^[A-Z]{3}$/.test(currency)) {
|
||||
errors.push('Currency code must be uppercase letters only');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate email address
|
||||
*/
|
||||
export function validateEmail(email) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!email) {
|
||||
return { valid: true, errors, warnings };
|
||||
}
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
errors.push('Invalid email address format');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate phone number (basic validation)
|
||||
*/
|
||||
export function validatePhone(phone) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!phone) {
|
||||
return { valid: true, errors, warnings };
|
||||
}
|
||||
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
errors.push('Invalid phone number format');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate account number or IBAN
|
||||
*/
|
||||
export function validateAccountNumber(accountNumber, iban) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!accountNumber && !iban) {
|
||||
errors.push('Either account number or IBAN is required');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
if (iban) {
|
||||
// Basic IBAN validation (2 letters + 2 digits + up to 30 alphanumeric)
|
||||
const ibanRegex = /^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/;
|
||||
if (!ibanRegex.test(iban.replace(/\s/g, ''))) {
|
||||
errors.push('Invalid IBAN format');
|
||||
}
|
||||
}
|
||||
if (accountNumber && accountNumber.trim().length === 0) {
|
||||
errors.push('Account number cannot be empty');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate purpose of payment
|
||||
*/
|
||||
export function validatePurposeOfPayment(purpose) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!purpose || purpose.trim().length === 0) {
|
||||
errors.push('Purpose of payment is required');
|
||||
}
|
||||
else if (purpose.trim().length < 5) {
|
||||
warnings.push('Purpose of payment is very short - provide more detail');
|
||||
}
|
||||
else if (purpose.length > 140) {
|
||||
warnings.push('Purpose of payment exceeds 140 characters - may be truncated');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate name field
|
||||
*/
|
||||
export function validateName(name, fieldName) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!name || name.trim().length === 0) {
|
||||
errors.push(`${fieldName} is required`);
|
||||
}
|
||||
else if (name.trim().length < 2) {
|
||||
errors.push(`${fieldName} must be at least 2 characters`);
|
||||
}
|
||||
else if (name.length > 140) {
|
||||
warnings.push(`${fieldName} exceeds 140 characters`);
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate address
|
||||
*/
|
||||
export function validateAddress(address, city, country) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!address && !city) {
|
||||
errors.push('Either address or city is required');
|
||||
}
|
||||
if (!country) {
|
||||
errors.push('Country is required');
|
||||
}
|
||||
else if (country.length !== 2) {
|
||||
errors.push('Country must be a 2-letter ISO code');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate tax ID (CPF or CNPJ)
|
||||
*/
|
||||
export function validateTaxId(taxId, fieldName) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!taxId) {
|
||||
errors.push(`${fieldName} is required`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
const validation = validateBrazilianTaxId(taxId);
|
||||
if (!validation.valid) {
|
||||
errors.push(`${fieldName}: ${validation.error || 'Invalid format'}`);
|
||||
}
|
||||
return {
|
||||
valid: validation.valid,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Sanitize string input (remove dangerous characters)
|
||||
*/
|
||||
export function sanitizeString(input) {
|
||||
return input
|
||||
.replace(/[<>]/g, '') // Remove potential HTML/XML tags
|
||||
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
|
||||
.trim();
|
||||
}
|
||||
/**
|
||||
* Sanitize number input
|
||||
*/
|
||||
export function sanitizeNumber(input) {
|
||||
if (typeof input === 'number') {
|
||||
return isNaN(input) ? 0 : input;
|
||||
}
|
||||
const num = parseFloat(input.replace(/[^\d.-]/g, ''));
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
//# sourceMappingURL=input-validation.js.map
|
||||
1
packages/utils/src/input-validation.js.map
Normal file
1
packages/utils/src/input-validation.js.map
Normal file
File diff suppressed because one or more lines are too long
256
packages/utils/src/input-validation.ts
Normal file
256
packages/utils/src/input-validation.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Input validation utilities for user inputs and transaction data
|
||||
*/
|
||||
|
||||
import { validateBrazilianTaxId } from './validation';
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transaction amount
|
||||
*/
|
||||
export function validateAmount(amount: number | string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
|
||||
if (isNaN(numAmount)) {
|
||||
errors.push('Amount must be a valid number');
|
||||
} else if (numAmount <= 0) {
|
||||
errors.push('Amount must be greater than zero');
|
||||
} else if (numAmount > 1000000000) {
|
||||
warnings.push('Amount exceeds 1 billion - please verify');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate currency code (ISO 4217)
|
||||
*/
|
||||
export function validateCurrency(currency: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!currency || currency.trim().length === 0) {
|
||||
errors.push('Currency code is required');
|
||||
} else if (currency.length !== 3) {
|
||||
errors.push('Currency code must be 3 characters (ISO 4217)');
|
||||
} else if (!/^[A-Z]{3}$/.test(currency)) {
|
||||
errors.push('Currency code must be uppercase letters only');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email address
|
||||
*/
|
||||
export function validateEmail(email: string | undefined): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!email) {
|
||||
return { valid: true, errors, warnings };
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
errors.push('Invalid email address format');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate phone number (basic validation)
|
||||
*/
|
||||
export function validatePhone(phone: string | undefined): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!phone) {
|
||||
return { valid: true, errors, warnings };
|
||||
}
|
||||
|
||||
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
errors.push('Invalid phone number format');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate account number or IBAN
|
||||
*/
|
||||
export function validateAccountNumber(
|
||||
accountNumber: string | undefined,
|
||||
iban: string | undefined
|
||||
): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!accountNumber && !iban) {
|
||||
errors.push('Either account number or IBAN is required');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
if (iban) {
|
||||
// Basic IBAN validation (2 letters + 2 digits + up to 30 alphanumeric)
|
||||
const ibanRegex = /^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/;
|
||||
if (!ibanRegex.test(iban.replace(/\s/g, ''))) {
|
||||
errors.push('Invalid IBAN format');
|
||||
}
|
||||
}
|
||||
|
||||
if (accountNumber && accountNumber.trim().length === 0) {
|
||||
errors.push('Account number cannot be empty');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate purpose of payment
|
||||
*/
|
||||
export function validatePurposeOfPayment(
|
||||
purpose: string | undefined
|
||||
): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!purpose || purpose.trim().length === 0) {
|
||||
errors.push('Purpose of payment is required');
|
||||
} else if (purpose.trim().length < 5) {
|
||||
warnings.push('Purpose of payment is very short - provide more detail');
|
||||
} else if (purpose.length > 140) {
|
||||
warnings.push('Purpose of payment exceeds 140 characters - may be truncated');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate name field
|
||||
*/
|
||||
export function validateName(name: string | undefined, fieldName: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
errors.push(`${fieldName} is required`);
|
||||
} else if (name.trim().length < 2) {
|
||||
errors.push(`${fieldName} must be at least 2 characters`);
|
||||
} else if (name.length > 140) {
|
||||
warnings.push(`${fieldName} exceeds 140 characters`);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate address
|
||||
*/
|
||||
export function validateAddress(
|
||||
address: string | undefined,
|
||||
city: string | undefined,
|
||||
country: string | undefined
|
||||
): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!address && !city) {
|
||||
errors.push('Either address or city is required');
|
||||
}
|
||||
|
||||
if (!country) {
|
||||
errors.push('Country is required');
|
||||
} else if (country.length !== 2) {
|
||||
errors.push('Country must be a 2-letter ISO code');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate tax ID (CPF or CNPJ)
|
||||
*/
|
||||
export function validateTaxId(taxId: string | undefined, fieldName: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!taxId) {
|
||||
errors.push(`${fieldName} is required`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
const validation = validateBrazilianTaxId(taxId);
|
||||
if (!validation.valid) {
|
||||
errors.push(`${fieldName}: ${validation.error || 'Invalid format'}`);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: validation.valid,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize string input (remove dangerous characters)
|
||||
*/
|
||||
export function sanitizeString(input: string): string {
|
||||
return input
|
||||
.replace(/[<>]/g, '') // Remove potential HTML/XML tags
|
||||
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize number input
|
||||
*/
|
||||
export function sanitizeNumber(input: string | number): number {
|
||||
if (typeof input === 'number') {
|
||||
return isNaN(input) ? 0 : input;
|
||||
}
|
||||
const num = parseFloat(input.replace(/[^\d.-]/g, ''));
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
504
pnpm-lock.yaml
generated
504
pnpm-lock.yaml
generated
@@ -100,6 +100,9 @@ importers:
|
||||
'@brazil-swift-ops/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
xmlbuilder2:
|
||||
specifier: ^3.0.2
|
||||
version: 3.1.1
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -136,6 +139,9 @@ importers:
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^1.0.0
|
||||
version: 1.6.1(@types/node@20.19.30)
|
||||
|
||||
packages/treasury:
|
||||
dependencies:
|
||||
@@ -562,6 +568,13 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@jest/schemas@29.6.3:
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
dev: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.13:
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
dependencies:
|
||||
@@ -613,6 +626,35 @@ packages:
|
||||
fastq: 1.20.1
|
||||
dev: true
|
||||
|
||||
/@oozcitak/dom@1.15.10:
|
||||
resolution: {integrity: sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
dependencies:
|
||||
'@oozcitak/infra': 1.0.8
|
||||
'@oozcitak/url': 1.0.4
|
||||
'@oozcitak/util': 8.3.8
|
||||
dev: false
|
||||
|
||||
/@oozcitak/infra@1.0.8:
|
||||
resolution: {integrity: sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==}
|
||||
engines: {node: '>=6.0'}
|
||||
dependencies:
|
||||
'@oozcitak/util': 8.3.8
|
||||
dev: false
|
||||
|
||||
/@oozcitak/url@1.0.4:
|
||||
resolution: {integrity: sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==}
|
||||
engines: {node: '>=8.0'}
|
||||
dependencies:
|
||||
'@oozcitak/infra': 1.0.8
|
||||
'@oozcitak/util': 8.3.8
|
||||
dev: false
|
||||
|
||||
/@oozcitak/util@8.3.8:
|
||||
resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
dev: false
|
||||
|
||||
/@remix-run/router@1.23.2:
|
||||
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -822,6 +864,10 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@sinclair/typebox@0.27.8:
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
dev: true
|
||||
|
||||
/@types/babel__core@7.20.5:
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
dependencies:
|
||||
@@ -895,6 +941,63 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@vitest/expect@1.6.1:
|
||||
resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==}
|
||||
dependencies:
|
||||
'@vitest/spy': 1.6.1
|
||||
'@vitest/utils': 1.6.1
|
||||
chai: 4.5.0
|
||||
dev: true
|
||||
|
||||
/@vitest/runner@1.6.1:
|
||||
resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==}
|
||||
dependencies:
|
||||
'@vitest/utils': 1.6.1
|
||||
p-limit: 5.0.0
|
||||
pathe: 1.1.2
|
||||
dev: true
|
||||
|
||||
/@vitest/snapshot@1.6.1:
|
||||
resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==}
|
||||
dependencies:
|
||||
magic-string: 0.30.21
|
||||
pathe: 1.1.2
|
||||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/@vitest/spy@1.6.1:
|
||||
resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==}
|
||||
dependencies:
|
||||
tinyspy: 2.2.1
|
||||
dev: true
|
||||
|
||||
/@vitest/utils@1.6.1:
|
||||
resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==}
|
||||
dependencies:
|
||||
diff-sequences: 29.6.3
|
||||
estree-walker: 3.0.3
|
||||
loupe: 2.3.7
|
||||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/acorn-walk@8.3.4:
|
||||
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
dev: true
|
||||
|
||||
/acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
dev: true
|
||||
@@ -911,6 +1014,16 @@ packages:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
dev: true
|
||||
|
||||
/argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
dev: false
|
||||
|
||||
/assertion-error@1.1.0:
|
||||
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
|
||||
dev: true
|
||||
|
||||
/autoprefixer@10.4.23(postcss@8.5.6):
|
||||
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -955,6 +1068,11 @@ packages:
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
dev: true
|
||||
|
||||
/cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/camelcase-css@2.0.1:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -964,6 +1082,25 @@ packages:
|
||||
resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
|
||||
dev: true
|
||||
|
||||
/chai@4.5.0:
|
||||
resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
assertion-error: 1.1.0
|
||||
check-error: 1.0.3
|
||||
deep-eql: 4.1.4
|
||||
get-func-name: 2.0.2
|
||||
loupe: 2.3.7
|
||||
pathval: 1.1.1
|
||||
type-detect: 4.1.0
|
||||
dev: true
|
||||
|
||||
/check-error@1.0.3:
|
||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -984,10 +1121,23 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/confbox@0.1.8:
|
||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||
dev: true
|
||||
|
||||
/convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
dev: true
|
||||
|
||||
/cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1017,10 +1167,22 @@ packages:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
dev: false
|
||||
|
||||
/deep-eql@4.1.4:
|
||||
resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
type-detect: 4.1.0
|
||||
dev: true
|
||||
|
||||
/didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
dev: true
|
||||
|
||||
/diff-sequences@29.6.3:
|
||||
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dev: true
|
||||
|
||||
/dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
dev: true
|
||||
@@ -1065,6 +1227,33 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
dev: true
|
||||
|
||||
/execa@8.0.1:
|
||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||
engines: {node: '>=16.17'}
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
get-stream: 8.0.1
|
||||
human-signals: 5.0.0
|
||||
is-stream: 3.0.0
|
||||
merge-stream: 2.0.0
|
||||
npm-run-path: 5.3.0
|
||||
onetime: 6.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-final-newline: 3.0.0
|
||||
dev: true
|
||||
|
||||
/fast-glob@3.3.3:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -1122,6 +1311,15 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/get-func-name@2.0.2:
|
||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||
dev: true
|
||||
|
||||
/get-stream@8.0.1:
|
||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||
engines: {node: '>=16'}
|
||||
dev: true
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1143,6 +1341,11 @@ packages:
|
||||
function-bind: 1.1.2
|
||||
dev: true
|
||||
|
||||
/human-signals@5.0.0:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
dev: true
|
||||
|
||||
/is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1174,6 +1377,15 @@ packages:
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/is-stream@3.0.0:
|
||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
dev: true
|
||||
|
||||
/jiti@1.21.7:
|
||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
hasBin: true
|
||||
@@ -1182,6 +1394,18 @@ packages:
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
/js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
dev: true
|
||||
|
||||
/js-yaml@3.14.1:
|
||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
dev: false
|
||||
|
||||
/jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1203,6 +1427,14 @@ packages:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
dev: true
|
||||
|
||||
/local-pkg@0.5.1:
|
||||
resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
mlly: 1.8.0
|
||||
pkg-types: 1.3.1
|
||||
dev: true
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
@@ -1210,12 +1442,28 @@ packages:
|
||||
js-tokens: 4.0.0
|
||||
dev: false
|
||||
|
||||
/loupe@2.3.7:
|
||||
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
dev: true
|
||||
|
||||
/magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
dev: true
|
||||
|
||||
/merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
dev: true
|
||||
|
||||
/merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1229,6 +1477,20 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/mimic-fn@4.0.0:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/mlly@1.8.0:
|
||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
pathe: 2.0.3
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.3
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
@@ -1256,6 +1518,13 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/npm-run-path@5.3.0:
|
||||
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dependencies:
|
||||
path-key: 4.0.0
|
||||
dev: true
|
||||
|
||||
/object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1266,10 +1535,46 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/onetime@6.0.0:
|
||||
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
mimic-fn: 4.0.0
|
||||
dev: true
|
||||
|
||||
/p-limit@5.0.0:
|
||||
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
yocto-queue: 1.2.2
|
||||
dev: true
|
||||
|
||||
/path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/path-key@4.0.0:
|
||||
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
dev: true
|
||||
|
||||
/pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
dev: true
|
||||
|
||||
/pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
dev: true
|
||||
|
||||
/pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
dev: true
|
||||
|
||||
/picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
dev: true
|
||||
@@ -1294,6 +1599,14 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
mlly: 1.8.0
|
||||
pathe: 2.0.3
|
||||
dev: true
|
||||
|
||||
/postcss-import@15.1.0(postcss@8.5.6):
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -1370,6 +1683,15 @@ packages:
|
||||
source-map-js: 1.2.1
|
||||
dev: true
|
||||
|
||||
/pretty-format@29.7.0:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.3
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
dev: true
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
@@ -1384,6 +1706,10 @@ packages:
|
||||
scheduler: 0.23.2
|
||||
dev: false
|
||||
|
||||
/react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
dev: true
|
||||
|
||||
/react-refresh@0.17.0:
|
||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1499,11 +1825,55 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
dev: true
|
||||
|
||||
/shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
dev: true
|
||||
|
||||
/signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
dev: false
|
||||
|
||||
/stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
dev: true
|
||||
|
||||
/std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
dev: true
|
||||
|
||||
/strip-final-newline@3.0.0:
|
||||
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/strip-literal@2.1.1:
|
||||
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
dev: true
|
||||
|
||||
/sucrase@3.35.1:
|
||||
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -1568,6 +1938,10 @@ packages:
|
||||
any-promise: 1.3.0
|
||||
dev: true
|
||||
|
||||
/tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
dev: true
|
||||
|
||||
/tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1576,6 +1950,16 @@ packages:
|
||||
picomatch: 4.0.3
|
||||
dev: true
|
||||
|
||||
/tinypool@0.8.4:
|
||||
resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/tinyspy@2.2.1:
|
||||
resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -1647,12 +2031,21 @@ packages:
|
||||
turbo-windows-arm64: 1.13.4
|
||||
dev: true
|
||||
|
||||
/type-detect@4.1.0:
|
||||
resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ufo@1.6.3:
|
||||
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
|
||||
dev: true
|
||||
|
||||
/undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
dev: true
|
||||
@@ -1680,6 +2073,28 @@ packages:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
||||
/vite-node@1.6.1(@types/node@20.19.30):
|
||||
resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.1.1
|
||||
vite: 5.4.21(@types/node@20.19.30)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite@5.4.21(@types/node@20.19.30):
|
||||
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -1719,10 +2134,99 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vitest@1.6.1(@types/node@20.19.30):
|
||||
resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': 1.6.1
|
||||
'@vitest/ui': 1.6.1
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.19.30
|
||||
'@vitest/expect': 1.6.1
|
||||
'@vitest/runner': 1.6.1
|
||||
'@vitest/snapshot': 1.6.1
|
||||
'@vitest/spy': 1.6.1
|
||||
'@vitest/utils': 1.6.1
|
||||
acorn-walk: 8.3.4
|
||||
chai: 4.5.0
|
||||
debug: 4.4.3
|
||||
execa: 8.0.1
|
||||
local-pkg: 0.5.1
|
||||
magic-string: 0.30.21
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.1.1
|
||||
std-env: 3.10.0
|
||||
strip-literal: 2.1.1
|
||||
tinybench: 2.9.0
|
||||
tinypool: 0.8.4
|
||||
vite: 5.4.21(@types/node@20.19.30)
|
||||
vite-node: 1.6.1(@types/node@20.19.30)
|
||||
why-is-node-running: 2.3.0
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
dev: true
|
||||
|
||||
/why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
dev: true
|
||||
|
||||
/xmlbuilder2@3.1.1:
|
||||
resolution: {integrity: sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==}
|
||||
engines: {node: '>=12.0'}
|
||||
dependencies:
|
||||
'@oozcitak/dom': 1.15.10
|
||||
'@oozcitak/infra': 1.0.8
|
||||
'@oozcitak/util': 8.3.8
|
||||
js-yaml: 3.14.1
|
||||
dev: false
|
||||
|
||||
/yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: true
|
||||
|
||||
/yocto-queue@1.2.2:
|
||||
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
|
||||
engines: {node: '>=12.20'}
|
||||
dev: true
|
||||
|
||||
/zustand@4.5.7(@types/react@18.3.27)(react@18.3.1):
|
||||
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
|
||||
Reference in New Issue
Block a user