feat(import): harden workbook parser boundaries

This commit is contained in:
2026-03-31 22:48:30 +02:00
parent 3e8b1702bc
commit db50e2e555
20 changed files with 936 additions and 174 deletions
+5 -3
View File
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start -p 3100",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --project tsconfig.typecheck.json --noEmit",
"test:unit": "vitest run",
"test:e2e": "playwright test"
},
"dependencies": {
@@ -43,12 +44,12 @@
"recharts": "^3.7.0",
"tailwind-merge": "^2.6.0",
"three": "^0.183.2",
"xlsx": "^0.18.5",
"zod": "^3.23.8"
},
"devDependencies": {
"@capakraken/tsconfig": "workspace:*",
"@playwright/test": "^1.49.1",
"@vitest/coverage-v8": "^2.1.9",
"@types/dompurify": "^3.2.0",
"@types/node": "^22.10.2",
"@types/react": "^19.0.6",
@@ -58,6 +59,7 @@
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"vitest": "^2.1.9"
}
}
+83
View File
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";
import {
MAX_BROWSER_SPREADSHEET_BYTES,
assertSpreadsheetFile,
parseSpreadsheet,
} from "./excel.js";
async function createWorkbookFile(
rows: unknown[][],
fileName = "spreadsheet.xlsx",
): Promise<File> {
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet1");
for (const row of rows) {
worksheet.addRow(row);
}
const buffer = await workbook.xlsx.writeBuffer();
return new File([buffer], fileName, {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
}
describe("excel import helpers", () => {
it("parses csv files with quoted values and skips blank rows", async () => {
const file = new File(
['name,role\n"Alice, A.",Engineer\n\nBob,Producer\n'],
"people.csv",
{ type: "text/csv" },
);
await expect(parseSpreadsheet(file)).resolves.toEqual([
{ name: "Alice, A.", role: "Engineer" },
{ name: "Bob", role: "Producer" },
]);
});
it("parses xlsx files and normalizes date cells to ISO strings", async () => {
const file = await createWorkbookFile([
["name", "startDate", "active"],
["Alice", new Date("2026-03-30T09:15:00.000Z"), true],
]);
await expect(parseSpreadsheet(file)).resolves.toEqual([
{
name: "Alice",
startDate: "2026-03-30T09:15:00.000Z",
active: "true",
},
]);
});
it("rejects duplicate headers in xlsx imports", async () => {
const file = await createWorkbookFile([
["Name", "name"],
["Alice", "Producer"],
]);
await expect(parseSpreadsheet(file)).rejects.toThrow('duplicate header "name"');
});
it("rejects legacy .xls uploads before parsing", () => {
const file = new File(["legacy"], "legacy.xls", {
type: "application/vnd.ms-excel",
});
expect(() => assertSpreadsheetFile(file)).toThrow(
"Legacy .xls files are not supported.",
);
});
it("rejects oversized spreadsheet uploads before parsing", () => {
const file = new File([Buffer.alloc(MAX_BROWSER_SPREADSHEET_BYTES + 1)], "oversized.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
expect(() => assertSpreadsheetFile(file)).toThrow(
`The selected file exceeds the ${MAX_BROWSER_SPREADSHEET_BYTES} byte limit`,
);
});
});
+44 -3
View File
@@ -3,6 +3,8 @@ const CSV_EXTENSION = ".csv";
const XLS_EXTENSION = ".xls";
export const MAX_BROWSER_SPREADSHEET_BYTES = 10 * 1024 * 1024;
export const MAX_BROWSER_SPREADSHEET_ROWS = 5000;
export const MAX_BROWSER_SPREADSHEET_COLUMNS = 200;
type ExcelJsModule = typeof import("exceljs");
let _excelJs: ExcelJsModule | null = null;
@@ -117,8 +119,47 @@ function parseCsvMatrix(input: string): string[][] {
return rows;
}
function matrixToObjects(rows: string[][]): Record<string, string>[] {
export function assertTabularMatrixWithinLimits(rows: string[][], contextLabel: string): void {
if (rows.length > MAX_BROWSER_SPREADSHEET_ROWS + 1) {
throw new Error(
`The selected file exceeds the ${MAX_BROWSER_SPREADSHEET_ROWS} row limit for ${contextLabel}.`,
);
}
const widestRow = rows.reduce((max, row) => Math.max(max, row.length), 0);
if (widestRow > MAX_BROWSER_SPREADSHEET_COLUMNS) {
throw new Error(
`The selected file exceeds the ${MAX_BROWSER_SPREADSHEET_COLUMNS} column limit for ${contextLabel}.`,
);
}
}
export function assertHeaderRow(headers: string[], contextLabel: string): void {
if (headers.length === 0) {
return;
}
const blankHeaderIndex = headers.findIndex((header) => header.length === 0);
if (blankHeaderIndex >= 0) {
throw new Error(
`The selected file contains an empty header cell in column ${blankHeaderIndex + 1} and cannot be used for ${contextLabel}.`,
);
}
const seen = new Set<string>();
for (const header of headers) {
const normalized = header.toLowerCase();
if (seen.has(normalized)) {
throw new Error(`The selected file contains duplicate header "${header}" and cannot be used for ${contextLabel}.`);
}
seen.add(normalized);
}
}
function matrixToObjects(rows: string[][], contextLabel: string): Record<string, string>[] {
assertTabularMatrixWithinLimits(rows, contextLabel);
const headers = (rows[0] ?? []).map((header) => header.trim());
assertHeaderRow(headers, contextLabel);
if (headers.length === 0) {
return [];
}
@@ -203,7 +244,7 @@ async function parseXlsxSpreadsheet(file: File): Promise<Record<string, string>[
rows.push(cells);
}
return matrixToObjects(rows);
return matrixToObjects(rows, "spreadsheet import");
}
/**
@@ -214,7 +255,7 @@ export async function parseSpreadsheet(file: File): Promise<Record<string, strin
assertSpreadsheetFile(file);
if (getFileExtension(file.name) === CSV_EXTENSION) {
return matrixToObjects(parseCsvMatrix(await file.text()));
return matrixToObjects(parseCsvMatrix(await file.text()), "spreadsheet import");
}
return parseXlsxSpreadsheet(file);
+106
View File
@@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import { matchRoleName, parseSkillMatrixWorkbook } from "./skillMatrixParser.js";
async function createWorkbookBuffer(
sheets: Array<{ name: string; rows: unknown[][] }>,
): Promise<ArrayBuffer> {
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.Workbook();
for (const sheet of sheets) {
const worksheet = workbook.addWorksheet(sheet.name);
for (const row of sheet.rows) {
worksheet.addRow(row);
}
}
const buffer = await workbook.xlsx.writeBuffer();
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}
describe("skill matrix parser", () => {
it("extracts employee info and merges skills by highest proficiency", async () => {
const workbook = await createWorkbookBuffer([
{
name: "Employee Information",
rows: [
["item", "property"],
["Full Name", "Alex Artist"],
["Area of Expertise", "Compositing"],
["Years of Experience", "7.4"],
["Portfolio URL", "https://portfolio.example/alex"],
],
},
{
name: "Software Skills",
rows: [
["category", "item", "property", "main skillset"],
["Software", "Nuke", "2", "1"],
["Software", "Photoshop", "0", ""],
],
},
{
name: "Technical Skillset",
rows: [
["category", "item", "property", "main skillset"],
["Pipeline", "Nuke", "4", ""],
["Pipeline", "Python", "3", "2"],
],
},
]);
await expect(parseSkillMatrixWorkbook(workbook)).resolves.toEqual({
employeeInfo: {
displayName: "Alex Artist",
areaOfExpertise: "Compositing",
yearsOfExperience: 7,
portfolioUrl: "https://portfolio.example/alex",
},
skills: expect.arrayContaining([
{
skill: "Nuke",
category: "Pipeline",
proficiency: 5,
},
{
skill: "Python",
category: "Pipeline",
proficiency: 4,
isMainSkill: true,
},
]),
});
});
it("rejects duplicate headers in skill sheets", async () => {
const workbook = await createWorkbookBuffer([
{
name: "Employee Information",
rows: [
["item", "property"],
["Full Name", "Alex Artist"],
],
},
{
name: "Software Skills",
rows: [
["item", "item", "property"],
["Nuke", "Duplicate", "2"],
],
},
{
name: "Technical Skillset",
rows: [["category", "item", "property"]],
},
]);
await expect(parseSkillMatrixWorkbook(workbook)).rejects.toThrow('duplicate header "item"');
});
it("matches role names by exact and partial matches", () => {
expect(matchRoleName("Compositing", ["Producer", "Compositing"])).toBe("Compositing");
expect(matchRoleName("Senior Producer", ["Producer", "Lighting"])).toBe("Producer");
expect(matchRoleName("Rigging", ["Producer", "Lighting"])).toBeNull();
});
});
+3
View File
@@ -1,4 +1,5 @@
import type { SkillEntry } from "@capakraken/shared";
import { assertHeaderRow, assertTabularMatrixWithinLimits } from "./excel.js";
type ExcelJsModule = typeof import("exceljs");
@@ -80,7 +81,9 @@ function worksheetToRowObjects(
rows.push(cells);
}
assertTabularMatrixWithinLimits(rows, "skill matrix import");
const headers = (rows[0] ?? []).map((header) => header.trim());
assertHeaderRow(headers, "skill matrix import");
if (headers.length === 0) {
return [];
}
+10 -4
View File
@@ -8,7 +8,7 @@
- Untrusted workbook imports no longer accept legacy `.xls`.
- Server-side dispo imports accept only `.xlsx` files.
- Browser-side ad hoc imports accept `.xlsx` and `.csv`.
- Trusted export generation may still use `xlsx` until the export paths are migrated separately.
- Workbook import and export generation now use `exceljs` instead of direct runtime `xlsx` usage.
## Server Boundary
@@ -18,7 +18,9 @@ The dispo-import reader in [read-workbook.ts](/home/hartmut/Documents/Copilot/ca
- regular-file checks
- non-empty file checks
- a hard size limit of `15 MiB`
- `.xlsx`-only parsing behind a hardened server-side parser boundary
- a worksheet row limit of `10,000`
- a worksheet column limit of `256`
- `.xlsx`-only parsing through `exceljs` behind a hardened server-side parser boundary
The API entry points in [dispo.ts](/home/hartmut/Documents/Copilot/capakraken/packages/api/src/router/dispo.ts) reject non-`.xlsx` workbook paths before staging or validation begins.
@@ -28,6 +30,9 @@ The browser import helpers in [excel.ts](/home/hartmut/Documents/Copilot/capakra
- a hard client-side file size limit of `10 MiB`
- explicit rejection of legacy `.xls`
- a tabular row limit of `5,000` data rows plus the header row
- a tabular column limit of `200`
- header validation that rejects blank and duplicate column names
- `.xlsx` parsing through `exceljs`
- `.csv` parsing through a local parser for simple tabular imports
@@ -41,6 +46,7 @@ Affected upload flows:
## Rationale
- `.xls` support keeps the old binary workbook format in the untrusted path without enough payoff.
- the server path keeps compatibility-first `.xlsx` parsing for the current dispo workbooks, but only behind explicit file validation and limits
- the browser path moves away from blanket `xlsx` import usage to a narrower parser boundary
- the server path keeps compatibility-first `.xlsx` parsing for the current dispo workbooks, but only behind explicit file validation, size limits, and `exceljs`
- the browser path moves away from blanket spreadsheet parsing to a narrower parser boundary
- export generation follows the same maintained workbook stack as import parsing
- CSV remains useful for lightweight business imports and is small enough to parse with a narrow local parser.
+1 -1
View File
@@ -16,7 +16,7 @@
"@capakraken/shared": "workspace:*",
"@capakraken/staffing": "workspace:*",
"@trpc/server": "^11.0.0",
"xlsx": "^0.18.5"
"exceljs": "^4.4.0"
},
"devDependencies": {
"@capakraken/tsconfig": "workspace:*",
@@ -29,6 +29,7 @@ function createCommitDb(overrides: Record<string, unknown> = {}) {
},
stagedVacation: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
stagedAvailabilityRule: {
@@ -94,6 +95,9 @@ function createCommitDb(overrides: Record<string, unknown> = {}) {
findUnique: vi.fn().mockResolvedValue({ id: "batch_1", status: "STAGED", summary: {} }),
update: vi.fn().mockResolvedValue({}),
},
stagedVacation: {
count: vi.fn().mockResolvedValue(0),
},
stagedUnresolvedRecord: {
findMany: vi.fn().mockResolvedValue([]),
},
@@ -233,11 +237,11 @@ describe("commitDispoImportBatch", () => {
{
id: "sv_1",
resourceExternalId: "ada.director",
vacationType: "PUBLIC_HOLIDAY",
startDate: new Date("2026-01-01T00:00:00.000Z"),
endDate: new Date("2026-01-01T00:00:00.000Z"),
note: "New Year",
holidayName: "New Year",
vacationType: "ANNUAL",
startDate: new Date("2026-01-08T00:00:00.000Z"),
endDate: new Date("2026-01-09T00:00:00.000Z"),
note: "Winter vacation",
holidayName: null,
isHalfDay: false,
halfDayPart: null,
},
@@ -705,4 +709,18 @@ describe("commitDispoImportBatch", () => {
}),
);
});
it("rejects staged PUBLIC_HOLIDAY rows until holiday calendars are synchronized", async () => {
const { db, tx } = createCommitDb();
db.stagedVacation.count.mockResolvedValue(2);
await expect(
commitDispoImportBatch(db as never, {
importBatchId: "batch_1",
}),
).rejects.toThrow(
'Import batch "batch_1" still contains 2 staged PUBLIC_HOLIDAY row(s). Public holidays must be synchronized through holiday calendars before commit.',
);
});
});
@@ -230,8 +230,8 @@ describe("dispo import", () => {
});
expect(report.resourceCount).toBeGreaterThan(500);
expect(report.canCommitWithStrictSourceData).toBe(true);
expect(report.canCommitWithFallbacks).toBe(true);
expect(report.canCommitWithStrictSourceData).toBe(false);
expect(report.canCommitWithFallbacks).toBe(false);
expect(report.issues.find((issue) => issue.code === "FALLBACK_EMAIL_REQUIRED")).toBeUndefined();
expect(report.issues.find((issue) => issue.code === "FALLBACK_LCR_REQUIRED")).toBeUndefined();
expect(report.issues.find((issue) => issue.code === "FALLBACK_UCR_REQUIRED")).toBeUndefined();
@@ -247,6 +247,10 @@ describe("dispo import", () => {
);
expect(report.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "PUBLIC_HOLIDAY_IMPORT_REQUIRES_CALENDAR_SYNC",
severity: "blocker",
}),
expect.objectContaining({
code: "UNRESOLVED_RECORDS_PRESENT",
severity: "warning",
@@ -740,7 +744,7 @@ describe("dispo import", () => {
expect(result.counts.stagedResources).toBeGreaterThan(800);
expect(result.counts.stagedRosterResources).toBeGreaterThan(500);
expect(result.counts.stagedAssignments).toBeGreaterThan(1000);
expect(result.readiness.canCommitWithStrictSourceData).toBe(true);
expect(result.readiness.canCommitWithStrictSourceData).toBe(false);
expect(result.readiness.issues).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -754,7 +758,7 @@ describe("dispo import", () => {
data: expect.objectContaining({
summary: expect.objectContaining({
readiness: expect.objectContaining({
canCommitWithStrictSourceData: true,
canCommitWithStrictSourceData: false,
}),
}),
}),
@@ -5,12 +5,23 @@ import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import {
MAX_DISPO_WORKBOOK_BYTES,
MAX_DISPO_WORKBOOK_COLUMNS,
MAX_DISPO_WORKBOOK_ROWS,
readWorksheetMatrix,
} from "../use-cases/dispo-import/read-workbook.js";
const referenceWorkbookPath = fileURLToPath(
new URL("../../../../samples/Dispov2/MandatoryDispoCategories_V3.xlsx", import.meta.url),
);
const chargeabilityWorkbookPath = fileURLToPath(
new URL(
"../../../../samples/Dispov2/20260309_Bi-Weekly_Chargeability_Reporting_Content_Production_V0.943_4Hartmut.xlsx",
import.meta.url,
),
);
const planningWorkbookPath = fileURLToPath(
new URL("../../../../samples/Dispov2/DISPO_2026.xlsx", import.meta.url),
);
const tempDirectories: string[] = [];
@@ -28,6 +39,18 @@ async function makeTempDirectory(): Promise<string> {
return directory;
}
async function writeWorkbook(filePath: string, rows: unknown[][], sheetName = "Sheet1"): Promise<void> {
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(sheetName);
for (const row of rows) {
worksheet.addRow(row);
}
await workbook.xlsx.writeFile(filePath);
}
describe("readWorksheetMatrix", () => {
it("reads trusted xlsx worksheets through the hardened reader", async () => {
const rows = await readWorksheetMatrix(referenceWorkbookPath, "EID-Attr");
@@ -36,6 +59,21 @@ describe("readWorksheetMatrix", () => {
expect(rows.some((row) => row.length > 0)).toBe(true);
});
it("tolerates workbook tables that contain unsupported exceljs date group filters", async () => {
const rows = await readWorksheetMatrix(chargeabilityWorkbookPath, "ChgFC");
expect(rows.length).toBeGreaterThan(300);
expect(rows[0]?.length).toBeGreaterThan(5);
});
it("accepts real dispo planning worksheets within the supported width envelope", async () => {
const rows = await readWorksheetMatrix(planningWorkbookPath, "Dispo");
expect(rows.length).toBeGreaterThan(500);
expect(rows.some((row) => row.length > 256)).toBe(true);
expect(rows.every((row) => row.length <= MAX_DISPO_WORKBOOK_COLUMNS)).toBe(true);
});
it("rejects legacy .xls workbook paths", async () => {
const directory = await makeTempDirectory();
const legacyPath = path.join(directory, "legacy-input.xls");
@@ -55,4 +93,30 @@ describe("readWorksheetMatrix", () => {
"Workbook file exceeds the",
);
});
it("rejects worksheets that exceed the row limit", async () => {
const directory = await makeTempDirectory();
const workbookPath = path.join(directory, "too-many-rows.xlsx");
await writeWorkbook(
workbookPath,
Array.from({ length: MAX_DISPO_WORKBOOK_ROWS + 1 }, (_, index) => [`row-${index + 1}`]),
);
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
`exceeds the ${MAX_DISPO_WORKBOOK_ROWS} row import limit`,
);
});
it("rejects worksheets that exceed the column limit", async () => {
const directory = await makeTempDirectory();
const workbookPath = path.join(directory, "too-many-columns.xlsx");
await writeWorkbook(
workbookPath,
[Array.from({ length: MAX_DISPO_WORKBOOK_COLUMNS + 1 }, (_, index) => `col-${index + 1}`)],
);
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
`exceeds the ${MAX_DISPO_WORKBOOK_COLUMNS} column import limit`,
);
});
});
@@ -27,6 +27,7 @@ export interface DispoImportReadinessIssue {
| "FALLBACK_EMAIL_REQUIRED"
| "FALLBACK_LCR_REQUIRED"
| "FALLBACK_UCR_REQUIRED"
| "PUBLIC_HOLIDAY_IMPORT_REQUIRES_CALENDAR_SYNC"
| "PLANNING_RESOURCE_MISSING_FROM_ROSTER"
| "REFERENCE_RESOURCE_MASTER_MISSING"
| "UNRESOLVED_RECORDS_PRESENT";
@@ -172,6 +173,10 @@ export async function assessDispoImportReadiness(
filterUnresolvedCount(chargeabilityWorkbook.unresolved, excludedIds) +
filterUnresolvedCount(planningWorkbook.unresolved, excludedIds) +
filterUnresolvedCount(rosterWorkbook?.unresolved ?? [], excludedIds);
const publicHolidayImportCount = planningWorkbook.vacations.filter(
(vacation) =>
!excludedIds.has(vacation.resourceExternalId) && vacation.vacationType === "PUBLIC_HOLIDAY",
).length;
const missingEmailCount = Array.from(mergedResources.values()).filter(
(resource) => !resource.email,
).length;
@@ -254,6 +259,20 @@ export async function assessDispoImportReadiness(
);
}
if (publicHolidayImportCount > 0) {
issues.push(
buildReadinessIssue({
code: "PUBLIC_HOLIDAY_IMPORT_REQUIRES_CALENDAR_SYNC",
count: publicHolidayImportCount,
message:
"Planning import contains PUBLIC_HOLIDAY rows. Public holidays must be managed through holiday calendars so country/state/city-specific rules stay canonical.",
resolution:
"Import or update the relevant holiday calendars first, then remove PUBLIC_HOLIDAY rows from the generic planning/vacation import before commit.",
severity: "blocker",
}),
);
}
if (unresolvedCount > 0) {
issues.push(
buildReadinessIssue({
@@ -166,6 +166,25 @@ function getSlotHalfDayPart(slotLabel: string | null): "AFTERNOON" | "MORNING" |
return null;
}
function isPlanningSummaryRow(row: ReadonlyArray<WorksheetCellValue>): boolean {
if ((row[0] ?? null) !== null || (row[1] ?? null) !== null) {
return false;
}
const repeatedLabels = row
.slice(DISPO_EID_COLUMN - 1, 9)
.map((value) => normalizeNullableWorkbookValue(value))
.filter((value): value is string => value !== null);
if (repeatedLabels.length === 0) {
return false;
}
const normalizedLabels = new Set(repeatedLabels.map((value) => value.toLowerCase()));
const label = repeatedLabels[0] ?? null;
return normalizedLabels.size === 1 && label !== null && label.startsWith("(") && label.endsWith(")");
}
function buildPlanningColumns(rows: ReadonlyArray<ReadonlyArray<WorksheetCellValue>>) {
const columns: PlanningColumn[] = [];
const headerWidth = Math.max(rows[DISPO_DATE_ROW - 1]?.length ?? 0, rows[DISPO_SLOT_ROW - 1]?.length ?? 0);
@@ -483,6 +502,9 @@ export async function parseDispoPlanningWorkbook(
for (let rowNumber = DISPO_DATA_START_ROW; rowNumber <= rows.length; rowNumber += 1) {
const row = rows[rowNumber - 1] ?? [];
if (isPlanningSummaryRow(row)) {
continue;
}
const eid = normalizeNullableWorkbookValue(row[DISPO_EID_COLUMN - 1]);
if (!eid) {
@@ -1,32 +1,41 @@
import { stat } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
export type WorksheetCellValue = boolean | Date | number | string | null;
export type WorksheetMatrix = WorksheetCellValue[][];
type XlsxWorkbook = {
Sheets: Record<string, unknown>;
type ExcelJsModule = typeof import("exceljs");
type ExcelJsWorkbook = InstanceType<ExcelJsModule["Workbook"]>;
type ExcelJsXlsxReader = ExcelJsWorkbook["xlsx"] & {
_processTableEntry?: (
stream: unknown,
model: Record<string, unknown>,
name: string,
) => Promise<unknown>;
};
type SheetToJsonOptions = {
header: 1;
raw: true;
defval: null;
};
type XlsxRuntime = {
readFile(filePath: string, options: { cellDates: true; dense: true }): XlsxWorkbook;
utils: {
sheet_to_json<T>(worksheet: unknown, options: SheetToJsonOptions): T[];
};
};
const require = createRequire(import.meta.url);
const XLSX = require("xlsx") as XlsxRuntime;
const DISPO_WORKBOOK_EXTENSION = ".xlsx";
export const MAX_DISPO_WORKBOOK_BYTES = 15 * 1024 * 1024;
export const MAX_DISPO_WORKBOOK_ROWS = 10000;
export const MAX_DISPO_WORKBOOK_COLUMNS = 1024;
const EXCELJS_IGNORE_WORKSHEET_NODES = ["tableParts"];
const EXCELJS_UNSUPPORTED_TABLE_FILTER_MARKER = '"name":"dateGroupItem"';
let _excelJs: ExcelJsModule | null = null;
const worksheetMatrixCache = new Map<string, Promise<WorksheetMatrix>>();
function normalizeExcelJsModule(module: ExcelJsModule | { default?: ExcelJsModule }): ExcelJsModule {
return "Workbook" in module ? module : (module.default as ExcelJsModule);
}
async function getExcelJS() {
if (!_excelJs) {
_excelJs = normalizeExcelJsModule(await import("exceljs"));
}
return _excelJs;
}
function trimTrailingNulls(row: WorksheetCellValue[]): WorksheetCellValue[] {
let end = row.length;
@@ -44,6 +53,10 @@ function trimTrailingEmptyRows(rows: WorksheetMatrix): WorksheetMatrix {
return rows.slice(0, end);
}
function cloneWorksheetMatrix(rows: WorksheetMatrix): WorksheetMatrix {
return rows.map((row) => row.slice());
}
async function validateWorkbookPath(workbookPath: string): Promise<string> {
const resolvedPath = path.resolve(workbookPath);
@@ -119,31 +132,99 @@ function normalizeWorksheetCellValue(value: unknown): WorksheetCellValue {
return String(value);
}
function assertWorksheetShape(rows: WorksheetMatrix, sheetName: string, workbookPath: string): void {
if (rows.length > MAX_DISPO_WORKBOOK_ROWS) {
throw new Error(
`Worksheet "${sheetName}" in "${workbookPath}" exceeds the ${MAX_DISPO_WORKBOOK_ROWS} row import limit.`,
);
}
const widestRow = rows.reduce((max, row) => Math.max(max, row.length), 0);
if (widestRow > MAX_DISPO_WORKBOOK_COLUMNS) {
throw new Error(
`Worksheet "${sheetName}" in "${workbookPath}" exceeds the ${MAX_DISPO_WORKBOOK_COLUMNS} column import limit.`,
);
}
}
function isUnsupportedExcelJsTableFilterError(error: unknown): boolean {
return error instanceof Error && error.message.includes(EXCELJS_UNSUPPORTED_TABLE_FILTER_MARKER);
}
function patchExcelJsTableCompatibility(workbook: ExcelJsWorkbook): void {
const reader = workbook.xlsx as ExcelJsXlsxReader;
const originalProcessTableEntry = reader._processTableEntry;
if (typeof originalProcessTableEntry !== "function") {
return;
}
reader._processTableEntry = async function processTableEntryWithCompatibilityFallback(
stream,
model,
name,
) {
try {
return await originalProcessTableEntry.call(this, stream, model, name);
} catch (error) {
if (isUnsupportedExcelJsTableFilterError(error)) {
return undefined;
}
throw error;
}
};
}
export async function readWorksheetMatrix(
workbookPath: string,
sheetName: string,
): Promise<WorksheetMatrix> {
const resolvedPath = await validateWorkbookPath(workbookPath);
const workbook = XLSX.readFile(resolvedPath, {
cellDates: true,
dense: true,
});
const worksheet = workbook.Sheets[sheetName];
if (!worksheet) {
throw new Error(`Worksheet "${sheetName}" not found in workbook "${resolvedPath}"`);
const cacheKey = `${resolvedPath}::${sheetName}`;
const cachedMatrix = worksheetMatrixCache.get(cacheKey);
if (cachedMatrix) {
return cloneWorksheetMatrix(await cachedMatrix);
}
const rows = XLSX.utils.sheet_to_json<(WorksheetCellValue | null)[]>(worksheet, {
header: 1,
raw: true,
defval: null,
});
const matrixPromise = (async () => {
const ExcelJS = await getExcelJS();
const workbook = new ExcelJS.Workbook();
patchExcelJsTableCompatibility(workbook);
await workbook.xlsx.readFile(resolvedPath, { ignoreNodes: EXCELJS_IGNORE_WORKSHEET_NODES });
return trimTrailingEmptyRows(
rows.map((row: (WorksheetCellValue | null)[]) =>
trimTrailingNulls(row.map((value: WorksheetCellValue | null) => normalizeWorksheetCellValue(value))),
),
);
const worksheet = workbook.getWorksheet(sheetName);
if (!worksheet) {
throw new Error(`Worksheet "${sheetName}" not found in workbook "${resolvedPath}"`);
}
const rows: WorksheetMatrix = [];
for (let rowNumber = 1; rowNumber <= worksheet.rowCount; rowNumber += 1) {
const row = worksheet.getRow(rowNumber);
const cells: WorksheetCellValue[] = [];
for (let columnNumber = 1; columnNumber <= row.cellCount; columnNumber += 1) {
cells.push(normalizeWorksheetCellValue(row.getCell(columnNumber).value));
}
rows.push(trimTrailingNulls(cells));
}
const normalizedRows = trimTrailingEmptyRows(rows);
assertWorksheetShape(normalizedRows, sheetName, resolvedPath);
return normalizedRows;
})();
worksheetMatrixCache.set(cacheKey, matrixPromise);
try {
return cloneWorksheetMatrix(await matrixPromise);
} catch (error) {
worksheetMatrixCache.delete(cacheKey);
throw error;
}
}
export function getCellString(
@@ -55,6 +55,12 @@ export async function validateDispoBatch(
status: StagedRecordStatus.UNRESOLVED,
},
});
const stagedPublicHolidayCount = await db.stagedVacation.count({
where: {
importBatchId: batch.id,
vacationType: "PUBLIC_HOLIDAY",
},
});
const blockingUnresolved = unresolved.filter(
(record) =>
!(
@@ -70,6 +76,12 @@ export async function validateDispoBatch(
);
}
if (stagedPublicHolidayCount > 0) {
throw new Error(
`Import batch "${batch.id}" still contains ${stagedPublicHolidayCount} staged PUBLIC_HOLIDAY row(s). Public holidays must be synchronized through holiday calendars before commit.`,
);
}
return {
batchId: batch.id,
batchSummary: batch.summary,
+6 -2
View File
@@ -2,14 +2,18 @@
* Generate samples/CapaKrakenExamples.xlsx from the live database.
*
* Run from repo root:
* DATABASE_URL=postgresql://capakraken:capakraken_dev@localhost:5433/capakraken \
* pnpm --filter @capakraken/db tsx src/generate-excel.ts
* pnpm --filter @capakraken/db db:excel
*/
import { PrismaClient } from "@prisma/client";
import ExcelJS from "exceljs";
import path from "path";
import { fileURLToPath } from "url";
import { loadWorkspaceEnv } from "./load-workspace-env.js";
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
loadWorkspaceEnv();
assertCapaKrakenDbTarget("db:excel");
const prisma = new PrismaClient();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
+2
View File
@@ -2,6 +2,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { PrismaClient, StagedRecordStatus } from "@prisma/client";
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
loadWorkspaceEnv();
@@ -378,6 +379,7 @@ function ensureCommitAllowed(options: ImportDispoBatchOptions, readiness: DispoI
}
export async function runImportDispoBatch(options: ImportDispoBatchOptions) {
assertCapaKrakenDbTarget("db:import:dispo");
const dispoImport = await loadDispoImportModule();
printWorkbookSources(options);
+1 -1
View File
@@ -16,7 +16,7 @@
},
"dependencies": {
"@capakraken/shared": "workspace:*",
"xlsx": "^0.18.5"
"exceljs": "^4.4.0"
},
"devDependencies": {
"@capakraken/tsconfig": "workspace:*",
@@ -1,4 +1,3 @@
import * as XLSX from "xlsx";
import {
EstimateExportFormat,
EstimateStatus,
@@ -144,8 +143,8 @@ function buildSource(): EstimateExportSource {
}
describe("estimate export serializer", () => {
it("creates a structured JSON export payload", () => {
const payload = serializeEstimateExport(buildSource(), EstimateExportFormat.JSON);
it("creates a structured JSON export payload", async () => {
const payload = await serializeEstimateExport(buildSource(), EstimateExportFormat.JSON);
expect(payload.encoding).toBe("utf8");
expect(payload.mimeType).toBe("application/json; charset=utf-8");
@@ -154,9 +153,16 @@ describe("estimate export serializer", () => {
expect(payload.previewText).toContain('"schemaVersion": 1');
});
it("creates a multi-sheet xlsx export payload", () => {
const payload = serializeEstimateExport(buildSource(), EstimateExportFormat.XLSX);
const workbook = XLSX.read(payload.content, { type: "base64" });
it("creates a multi-sheet xlsx export payload", async () => {
const payload = await serializeEstimateExport(buildSource(), EstimateExportFormat.XLSX);
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.Workbook();
const workbookBytes = Uint8Array.from(Buffer.from(payload.content, "base64"));
const workbookBuffer = workbookBytes.buffer.slice(
workbookBytes.byteOffset,
workbookBytes.byteOffset + workbookBytes.byteLength,
);
await workbook.xlsx.load(workbookBuffer);
expect(payload.encoding).toBe("base64");
expect(payload.sheetNames).toEqual([
@@ -167,7 +173,7 @@ describe("estimate export serializer", () => {
"Resources",
"Metrics",
]);
expect(workbook.SheetNames).toContain("DemandLines");
expect(workbook.getWorksheet("DemandLines")).toBeDefined();
expect(payload.byteLength).toBeGreaterThan(100);
});
});
@@ -1,4 +1,3 @@
import * as XLSX from "xlsx";
import {
EstimateExportFormat,
type EstimateExportArtifactPayload,
@@ -8,6 +7,8 @@ import {
} from "@capakraken/shared";
import { summarizeEstimateDemandLines } from "./metrics.js";
type ExcelJsModule = typeof import("exceljs");
type ExportProjectRef = {
id: string;
name: string;
@@ -109,6 +110,18 @@ type ExportMetric = {
updatedAt: Date;
};
type ExportSheetRow = Record<string, unknown>;
let _excelJs: ExcelJsModule | null = null;
async function getExcelJS() {
if (!_excelJs) {
_excelJs = await import("exceljs");
}
return _excelJs;
}
export interface EstimateExportSource {
estimate: {
id: string;
@@ -508,6 +521,52 @@ function base64ByteLength(content: string) {
return Math.floor((content.length * 3) / 4) - padding;
}
function buildSheetColumns(rows: ExportSheetRow[]) {
return Array.from(
rows.reduce((keys, row) => {
for (const key of Object.keys(row)) {
keys.add(key);
}
return keys;
}, new Set<string>()),
);
}
function toWorksheetCellValue(value: unknown): boolean | Date | number | string {
if (value == null) {
return "";
}
if (value instanceof Date) {
return value;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
return stringifyValue(value);
}
function appendWorksheetFromRows(
workbook: InstanceType<ExcelJsModule["Workbook"]>,
sheetName: string,
rows: ExportSheetRow[],
): void {
const worksheet = workbook.addWorksheet(sheetName);
const columns = buildSheetColumns(rows);
if (columns.length === 0) {
return;
}
worksheet.addRow(columns);
for (const row of rows) {
worksheet.addRow(columns.map((column) => toWorksheetCellValue(row[column])));
}
}
function buildTextPayload(
format: EstimateExportFormat,
content: string,
@@ -536,17 +595,18 @@ function buildTextPayload(
};
}
function buildXlsxPayload(
async function buildXlsxPayload(
source: EstimateExportSource,
summary: EstimateExportSummary,
): EstimateExportArtifactPayload {
): Promise<EstimateExportArtifactPayload> {
const overviewRows = buildOverviewRows(source, summary);
const assumptionRows = buildAssumptionRows(source.version.assumptions);
const scopeRows = buildScopeRows(source.version.scopeItems);
const demandRows = buildDemandRows(source);
const resourceRows = buildResourceRows(source.version.resourceSnapshots);
const metricRows = buildMetricRows(source.version.metrics);
const workbook = XLSX.utils.book_new();
const ExcelJS = await getExcelJS();
const workbook = new ExcelJS.Workbook();
const sheets = [
{ name: "Overview", rows: overviewRows },
{ name: "Assumptions", rows: assumptionRows },
@@ -557,17 +617,11 @@ function buildXlsxPayload(
] as const;
for (const sheet of sheets) {
XLSX.utils.book_append_sheet(
workbook,
XLSX.utils.json_to_sheet(sheet.rows),
sheet.name,
);
appendWorksheetFromRows(workbook, sheet.name, sheet.rows);
}
const content = XLSX.write(workbook, {
type: "base64",
bookType: "xlsx",
});
const buffer = await workbook.xlsx.writeBuffer();
const content = Buffer.from(buffer).toString("base64");
return {
schemaVersion: 1,
@@ -593,10 +647,10 @@ function buildXlsxPayload(
};
}
export function serializeEstimateExport(
export async function serializeEstimateExport(
source: EstimateExportSource,
format: EstimateExportFormat,
): EstimateExportArtifactPayload {
): Promise<EstimateExportArtifactPayload> {
const summary = buildSummary(source);
if (format === EstimateExportFormat.JSON) {
+328 -93
View File
@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
flatted: ^3.4.2
picomatch: ^4.0.4
importers:
.:
@@ -126,9 +130,6 @@ importers:
three:
specifier: ^0.183.2
version: 0.183.2
xlsx:
specifier: ^0.18.5
version: 0.18.5
zod:
specifier: ^3.23.8
version: 3.25.76
@@ -157,6 +158,9 @@ importers:
'@types/three':
specifier: ^0.183.1
version: 0.183.1
'@vitest/coverage-v8':
specifier: ^2.1.9
version: 2.1.9(vitest@2.1.9(@types/node@22.19.13)(terser@5.46.1))
autoprefixer:
specifier: ^10.4.20
version: 10.4.27(postcss@8.5.8)
@@ -169,6 +173,9 @@ importers:
typescript:
specifier: ^5.6.3
version: 5.9.3
vitest:
specifier: ^2.1.9
version: 2.1.9(@types/node@22.19.13)(terser@5.46.1)
packages/api:
dependencies:
@@ -245,9 +252,9 @@ importers:
'@trpc/server':
specifier: ^11.0.0
version: 11.11.0(typescript@5.9.3)
xlsx:
specifier: ^0.18.5
version: 0.18.5
exceljs:
specifier: ^4.4.0
version: 4.4.0
devDependencies:
'@capakraken/tsconfig':
specifier: workspace:*
@@ -298,9 +305,9 @@ importers:
'@capakraken/shared':
specifier: workspace:*
version: link:../shared
xlsx:
specifier: ^0.18.5
version: 0.18.5
exceljs:
specifier: ^4.4.0
version: 4.4.0
devDependencies:
'@capakraken/tsconfig':
specifier: workspace:*
@@ -407,6 +414,10 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@auth/core@0.41.0':
resolution: {integrity: sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==}
peerDependencies:
@@ -492,6 +503,9 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
@@ -1025,6 +1039,14 @@ packages:
'@ioredis/commands@1.5.1':
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@istanbuljs/schema@0.1.3':
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -1411,6 +1433,10 @@ packages:
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@playwright/test@1.58.2':
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
engines: {node: '>=18'}
@@ -2003,6 +2029,15 @@ packages:
resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@2.1.9':
resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==}
peerDependencies:
'@vitest/browser': 2.1.9
vitest: 2.1.9
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@2.1.9':
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
@@ -2114,10 +2149,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
@@ -2141,10 +2172,22 @@ packages:
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -2328,10 +2371,6 @@ packages:
caniuse-lite@1.0.30001776:
resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
@@ -2373,10 +2412,6 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -2593,12 +2628,21 @@ packages:
duplexer2@0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
electron-to-chromium@1.5.307:
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
emoji-regex-xs@1.0.0:
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
@@ -2806,7 +2850,7 @@ packages:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
picomatch: ^4.0.4
peerDependenciesMeta:
picomatch:
optional: true
@@ -2830,8 +2874,8 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.3.4:
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
float-tooltip@1.7.5:
resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==}
@@ -2844,13 +2888,13 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
@@ -2933,6 +2977,11 @@ packages:
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
@@ -2989,6 +3038,9 @@ packages:
hsl-to-rgb-for-reals@1.1.1:
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@@ -3097,6 +3149,10 @@ packages:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-generator-function@1.1.2:
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
engines: {node: '>= 0.4'}
@@ -3172,6 +3228,25 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-lib-source-maps@5.0.6:
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jay-peg@1.1.1:
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
@@ -3324,6 +3399,9 @@ packages:
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.7:
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
engines: {node: 20 || >=22}
@@ -3334,6 +3412,13 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -3374,6 +3459,10 @@ packages:
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
engines: {node: '>=10'}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
@@ -3559,6 +3648,9 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
@@ -3587,6 +3679,10 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.2:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
@@ -3612,12 +3708,8 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pify@2.3.0:
@@ -4018,6 +4110,10 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
@@ -4039,10 +4135,6 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -4060,6 +4152,14 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string.prototype.trim@1.2.10:
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
engines: {node: '>= 0.4'}
@@ -4078,6 +4178,14 @@ packages:
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.2.0:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@@ -4156,6 +4264,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
test-exclude@7.0.2:
resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==}
engines: {node: '>=18'}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -4469,26 +4581,21 @@ packages:
engines: {node: '>=8'}
hasBin: true
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
@@ -4525,6 +4632,11 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@auth/core@0.41.0':
dependencies:
'@panva/hkdf': 1.2.1
@@ -4635,6 +4747,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@0.2.3': {}
'@dimforge/rapier3d-compat@0.12.0': {}
'@dnd-kit/accessibility@3.1.1(react@19.2.4)':
@@ -5010,6 +5124,17 @@ snapshots:
'@ioredis/commands@1.5.1': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.2.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@istanbuljs/schema@0.1.3': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -5409,6 +5534,9 @@ snapshots:
'@pinojs/redact@0.4.0': {}
'@pkgjs/parseargs@0.11.0':
optional: true
'@playwright/test@1.58.2':
dependencies:
playwright: 1.58.2
@@ -5563,10 +5691,10 @@ snapshots:
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
fdir: 6.5.0(picomatch@4.0.4)
is-reference: 1.2.1
magic-string: 0.30.21
picomatch: 4.0.3
picomatch: 4.0.4
optionalDependencies:
rollup: 4.59.0
@@ -5574,7 +5702,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
picomatch: 4.0.4
optionalDependencies:
rollup: 4.59.0
@@ -6101,6 +6229,24 @@ snapshots:
'@typescript-eslint/types': 8.56.1
eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.19.13)(terser@5.46.1))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 0.2.3
debug: 4.4.3
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
istanbul-reports: 3.2.0
magic-string: 0.30.21
magicast: 0.3.5
std-env: 3.10.0
test-exclude: 7.0.2
tinyrainbow: 1.2.0
vitest: 2.1.9(@types/node@22.19.13)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
'@vitest/expect@2.1.9':
dependencies:
'@vitest/spy': 2.1.9
@@ -6241,8 +6387,6 @@ snapshots:
acorn@8.16.0: {}
adler-32@1.3.1: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
@@ -6272,16 +6416,22 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
picomatch: 4.0.4
archiver-utils@2.1.0:
dependencies:
@@ -6496,11 +6646,6 @@ snapshots:
caniuse-lite@1.0.30001776: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
@@ -6544,8 +6689,6 @@ snapshots:
cluster-key-slot@1.1.2: {}
codepage@1.15.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -6737,10 +6880,16 @@ snapshots:
dependencies:
readable-stream: 2.3.8
eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.307: {}
emoji-regex-xs@1.0.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
@@ -7076,9 +7225,9 @@ snapshots:
dependencies:
reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.3):
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.3
picomatch: 4.0.4
fflate@0.8.2: {}
@@ -7097,10 +7246,10 @@ snapshots:
flat-cache@4.0.1:
dependencies:
flatted: 3.3.4
flatted: 3.4.2
keyv: 4.5.4
flatted@3.3.4: {}
flatted@3.4.2: {}
float-tooltip@1.7.5:
dependencies:
@@ -7124,9 +7273,12 @@ snapshots:
dependencies:
is-callable: 1.2.7
forwarded-parse@2.1.2: {}
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
frac@1.1.2: {}
forwarded-parse@2.1.2: {}
fraction.js@5.3.4: {}
@@ -7211,6 +7363,15 @@ snapshots:
glob-to-regexp@0.4.1: {}
glob@10.5.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.9
minipass: 7.1.3
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@13.0.6:
dependencies:
minimatch: 10.2.4
@@ -7265,6 +7426,8 @@ snapshots:
hsl-to-rgb-for-reals@1.1.1: {}
html-escaper@2.0.2: {}
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
@@ -7388,6 +7551,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-fullwidth-code-point@3.0.0: {}
is-generator-function@1.1.2:
dependencies:
call-bound: 1.0.4
@@ -7462,6 +7627,33 @@ snapshots:
isexe@2.0.0: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
debug: 4.4.3
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jay-peg@1.1.1:
dependencies:
restructure: 3.0.2
@@ -7585,6 +7777,8 @@ snapshots:
loupe@3.2.1: {}
lru-cache@10.4.3: {}
lru-cache@11.2.7: {}
lru-cache@5.1.1:
@@ -7595,6 +7789,16 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magicast@0.3.5:
dependencies:
'@babel/parser': 7.29.2
'@babel/types': 7.29.0
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.7.4
math-intrinsics@1.1.0: {}
media-engine@1.0.3: {}
@@ -7608,7 +7812,7 @@ snapshots:
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
picomatch: 4.0.4
mime-db@1.52.0: {}
@@ -7628,6 +7832,10 @@ snapshots:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.9:
dependencies:
brace-expansion: 2.0.2
minimist@1.2.8: {}
minipass@7.1.3: {}
@@ -7795,6 +8003,8 @@ snapshots:
dependencies:
p-limit: 3.1.0
package-json-from-dist@1.0.1: {}
pako@0.2.9: {}
pako@1.0.11: {}
@@ -7813,6 +8023,11 @@ snapshots:
path-parse@1.0.7: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.3
path-scurry@2.0.2:
dependencies:
lru-cache: 11.2.7
@@ -7836,9 +8051,7 @@ snapshots:
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {}
picomatch@4.0.4: {}
pify@2.3.0: {}
@@ -8053,7 +8266,7 @@ snapshots:
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
picomatch: 4.0.4
real-require@0.2.0: {}
@@ -8311,6 +8524,8 @@ snapshots:
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
simple-swizzle@0.2.4:
dependencies:
is-arrayish: 0.3.4
@@ -8330,10 +8545,6 @@ snapshots:
split2@4.2.0: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
stackback@0.0.2: {}
stacktrace-parser@0.1.11:
@@ -8349,6 +8560,18 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.2.0
string.prototype.trim@1.2.10:
dependencies:
call-bind: 1.0.8
@@ -8380,6 +8603,14 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
strip-bom@3.0.0: {}
strip-json-comments@3.1.1: {}
@@ -8468,6 +8699,12 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
test-exclude@7.0.2:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 10.5.0
minimatch: 10.2.4
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@@ -8517,8 +8754,8 @@ snapshots:
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tinypool@1.1.1: {}
@@ -8860,24 +9097,22 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
wmf@1.0.2: {}
word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.2.0
wrappy@1.0.2: {}
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xmlchars@2.2.0: {}
xtend@4.0.2: {}