commit 4acbc03b35f4cb989f8761ada00c32089305aac5 Author: Joshua Oosthuizen Date: Thu Sep 3 13:27:19 2026 +0200 Initial commit: multi-company financial management system Root company app (src/, client/) — GL foundation, AR, and AP: - Segmented Chart of Accounts (Sage 300-style), double-entry PostingService with three-layer balance enforcement (app validation, CHECK constraints, deferred DB trigger), gap-free document numbering, RBAC with branch restrictions. - Accounts Receivable: Customers, Invoices, Credit Notes, Receipts with allocation, all posting through PostingService. - Accounts Payable: Suppliers, Supplier Invoices, Debit Notes, Payments with allocation, mirroring AR's posting pattern. - Tax Codes shared between AR and AP. - Company-app login is a token handoff from the Account Manager (RS256, single-use, company-scoped) rather than a local password. - Frontend reskinned to the approved mockup: navy sidebar, dashboard, tree-view Chart of Accounts, Customer/Supplier 360 pages. Account Manager (manager/, manager/client/) — central login gateway: - Password + TOTP 2FA + Google/Microsoft OAuth, company picker, and the RS256 handoff token issuance that the company app verifies. Covered by unit + integration tests against a real Postgres database (GL balance invariants, AR/AP posting correctness and allocation limits, the full handoff exchange including replay/cross-company/expiry/forgery rejection). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Tq2DbEvzHzs13jDFhLaX44 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8ee5992 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/cfm?schema=public" +JWT_ACCESS_SECRET="change-me-access-secret" +JWT_REFRESH_SECRET="change-me-refresh-secret" +JWT_ACCESS_TTL="15m" +JWT_REFRESH_TTL="7d" +PORT="4000" +COMPANY_SLUG="demo" +# One or more (comma-separated) paths to public keys copied from the Account Manager's +# manager/keys/handoff-public.pem, used to verify incoming handoff tokens. +HANDOFF_PUBLIC_KEY_PATHS="./keys/handoff-public.pem" diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..110629d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,46 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "import"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "env": { + "node": true, + "es2022": true, + "jest": true + }, + "rules": { + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.object.property.name='journalEntry'][callee.property.name=/^(create|createMany|createManyAndReturn|update|updateMany|upsert|delete|deleteMany)$/]", + "message": "Only src/modules/gl/posting.service.ts may write JournalEntry rows. Build a JournalEntryDraft and call PostingService.postEntry()/reverseEntry() instead." + }, + { + "selector": "CallExpression[callee.object.property.name='journalEntryLine'][callee.property.name=/^(create|createMany|createManyAndReturn|update|updateMany|upsert|delete|deleteMany)$/]", + "message": "Only src/modules/gl/posting.service.ts may write JournalEntryLine rows. Build a JournalEntryDraft and call PostingService.postEntry()/reverseEntry() instead." + } + ] + }, + "overrides": [ + { + "files": ["src/modules/gl/posting.service.ts"], + "rules": { + "no-restricted-syntax": "off" + } + }, + { + "files": ["tests/integration/posting.balance.spec.ts"], + "rules": { + "no-restricted-syntax": "off" + } + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6718574 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +.env +*.log +coverage/ +client/node_modules/ +client/dist/ +.idea/ +keys/ diff --git a/PHASE_1.5_TEST_PLAN.md b/PHASE_1.5_TEST_PLAN.md new file mode 100644 index 0000000..53ad407 --- /dev/null +++ b/PHASE_1.5_TEST_PLAN.md @@ -0,0 +1,153 @@ +# Phase 1.5 — Visual Redesign: Manual Test Checklist + +Reskinned the company-app frontend to match `temp/mockups/Cross Financial Manage Mockup.png` +(navy sidebar, blue accent, card-based content) and added three new screens (Dashboard, +Chart of Accounts tree view, Reports Centre shell, GL account drill-down). No backend or +schema changes in this phase — everything below should work against data you already have. + +Tick each box as you go. If something fails, note what you saw under it — that's what I'll fix. + +## 0. Start everything + +``` +# Terminal 1 — company API +cd D:\Webstorm\cross-financial-management +npm run dev # http://localhost:5000 + +# Terminal 2 — company frontend +cd D:\Webstorm\cross-financial-management\client +npm run dev # http://localhost:5173 + +# Terminal 3 — Manager API +cd D:\Webstorm\cross-financial-management\manager +npm run dev # http://localhost:5100 + +# Terminal 4 — Manager frontend +cd D:\Webstorm\cross-financial-management\manager\client +npm run dev # http://localhost:5174 +``` + +Manager login: `joosth2002@icloud.com` / `ChangeMe123!` (change this password once you're +through testing — see `manager/SETUP.md`). + +- [ ] All four `npm run dev` terminals started with no errors + +## 1. Sign-in / handoff flow (regression — not new, but the entry point to everything below) + +- [ ] Open `http://localhost:5174`, sign in with the Manager credentials above +- [ ] Company picker shows "Demo Company" +- [ ] Clicking it redirects to `http://localhost:5173` and you land **already signed in** — + no login form appears in the company app +- [ ] Refreshing the company app keeps you signed in (session persists) +- [ ] Signing out (avatar menu, top-right, in the company app) redirects you back to the + Manager rather than a dead `/login` page + +## 2. Sidebar & top bar (new look) + +- [ ] Left sidebar renders navy with the "Cross Financial / MANAGER" logo mark at the top +- [ ] Nav items are grouped under **General Ledger**, **Reports**, **Administration** with a + standalone **Dashboard** link above them +- [ ] Each nav item has an icon; the currently active page is highlighted (lighter navy pill, + blue icon, white text) +- [ ] Clicking every visible nav item navigates correctly and updates the highlight +- [ ] Top bar (white, right-aligned) shows the Branch switcher and your avatar with initials +- [ ] Avatar menu shows your name + email and a working **Sign out** +- [ ] Nav items you don't have permission for are hidden (test with a restricted user if you + have one, or trust that `hasPermission` filtering is unchanged from before) + +## 3. Dashboard (new — this is now the `/` landing page) + +- [ ] Landing on `http://localhost:5173/` after sign-in shows the new Dashboard, not the + journal entry list +- [ ] Four KPI tiles render: **Posted Entries**, **Draft Entries**, **Total Posted Movement**, + **Total Entries** — numbers should match what you'd expect from your seeded/test data +- [ ] "Posted Movement by Account Type" bar list appears once you have at least one posted + entry, with one bar per account type that has activity (ASSET/LIABILITY/etc.), and the + relative bar widths look proportionate to the amounts shown +- [ ] With **zero** posted entries (fresh DB), the panel shows "No posted entries yet." instead + of breaking +- [ ] "Quick Actions" only shows actions you have permission for, and each one navigates to the + right page (New Manual Entry / Chart of Accounts / Journals / Users & Roles) +- [ ] "Recent Activity" lists your most recent journal entries (any status) with date, entry + number, description, amount, and a status pill; clicking a row opens that entry's detail + page +- [ ] Switching the Branch switcher between "Main" and a specific branch updates the dashboard + figures to that branch's data + +## 4. Chart of Accounts (rebuilt as a grouped/collapsible tree) + +- [ ] Accounts are grouped under collapsible headers: ASSET, LIABILITY, EQUITY, INCOME, EXPENSE +- [ ] Clicking a group header collapses/expands its accounts, and the chevron icon flips + accordingly +- [ ] Each group header shows an account count chip (e.g. "12 accounts") +- [ ] Each account row shows: account code (monospace), account name, normal balance + (DEBIT/CREDIT), a control-account pill when applicable, and an Active/Closed status pill +- [ ] The search box filters accounts by code or name across all groups as you type +- [ ] "New Account" still opens the existing account-creation dialog and newly created accounts + appear in the correct group after saving +- [ ] Groups with zero matching accounts (e.g. after a narrow search) don't render or show + cleanly rather than looking broken + +## 5. General Ledger — "All Entries" tab (regression) + +- [ ] `Journal Entries` in the sidebar opens a page titled **General Ledger** with two tabs: + "All Entries" and "By Account" +- [ ] "All Entries" behaves exactly as before: same grid, same columns, sortable by date, + clicking a row opens the entry detail page +- [ ] "New Manual Entry" button still only shows if you have `gl.journal.post` + +## 6. General Ledger — "By Account" tab (new — account-centric drill-down) + +- [ ] Selecting an account from the dropdown loads its posted transaction history +- [ ] A running balance column updates correctly line by line (increases/decreases match the + account's normal balance direction — e.g. a debit-normal account's balance goes up on + debits, down on credits) +- [ ] Restricting the From/To date fields narrows the rows and the running balance still looks + correct for the restricted range +- [ ] An account with no posted activity shows "No posted transactions for this account in the + selected range." instead of an empty broken table +- [ ] Before selecting an account, you see a prompt to pick one rather than an empty table +- [ ] Switching the Branch switcher changes which entries are considered + +## 7. Journal Entry form (visual alignment + new static footer) + +- [ ] Form still works exactly as before: adding/removing lines, debit/credit mutual exclusion + per line, the live "Balanced ✓ / Not balanced" indicator, and Post Entry / Cancel +- [ ] Two new footer cards appear below the line table: **Attachments** ("+ Add Attachment") and + **Notes** ("+ Add Note") — these are intentionally disabled/inert placeholders for now, + just confirm they render without errors and don't block submission +- [ ] Posting a valid balanced entry still redirects to its detail page as before + +## 8. Reports Centre (new shell — no live reports yet) + +- [ ] `Reports Centre` in the sidebar opens a page with four categories: Financial, + Receivables, Payables, Management +- [ ] Each category shows report cards (e.g. Trial Balance, Profit & Loss, Aged Receivables) + with a name, short description, and a "Coming soon" chip +- [ ] Cards are visually disabled (dimmed, non-interactive) — this is expected; no report is + wired up to real data yet + +## 9. Other admin screens — style regression only (Segments, Journals, Fiscal Years, Users) + +These pages weren't rebuilt, but they now render on the new theme (cards instead of flat +panels, rounded corners, updated table header style). Confirm nothing looks broken: + +- [ ] Segments & Branches — page renders, "New Segment"/"Add Value" dialogs still work +- [ ] Journals — grid renders, "New Journal" dialog still works +- [ ] Fiscal Years — year buttons + period grid still work, "Create Year" still works +- [ ] Users & Roles — both tabs (Users / Role Permissions) still work, toggling a user's Active + switch still works + +## 10. General polish + +- [ ] Page titles, card borders, and table headers look consistent across every screen you + visited (uppercase gray table headers, light card borders, no harsh drop-shadows) +- [ ] No console errors in the browser dev tools on any page you visited +- [ ] Resizing the browser window narrower doesn't visibly break the layout (some horizontal + scroll on wide tables/grids is fine — full mobile responsiveness isn't a goal yet) + +--- + +Once everything above is ticked (or you've told me what's broken), the plan's next phase is +Accounts Receivable — customers, invoices, credit notes, receipts — built directly in this +visual language. diff --git a/PHASE_2_AR_TEST_PLAN.md b/PHASE_2_AR_TEST_PLAN.md new file mode 100644 index 0000000..2dde04b --- /dev/null +++ b/PHASE_2_AR_TEST_PLAN.md @@ -0,0 +1,113 @@ +# Phase 2 — Accounts Receivable: Manual Test Checklist + +Adds Customers, Tax Codes, AR Invoices, Credit Notes, and Receipts with allocation — all +posting through the existing `PostingService`, so every one of these creates a real, +balanced GL entry you can trace from the Journal Entries screen. Backend correctness +(balanced postings, tax calc, allocation limits) is already covered by +`tests/integration/ar.posting.spec.ts` (7 tests, passing) and was additionally verified by +hand over the real HTTP API end-to-end. This checklist is for the parts only a human can +judge: does the UI make sense, does the data look right, does it match how the business +actually invoices customers. + +Start the same four dev servers as Phase 1.5 (see `PHASE_1.5_TEST_PLAN.md` §0) and sign in +through the Manager. + +## 0. New nav items + +- [ ] Sidebar shows a new **Sales** section above General Ledger: Customers, Invoices, Receipts +- [ ] Administration section has a new **Tax Codes** item at the bottom +- [ ] Two tax codes are pre-seeded: STD (15%) and ZERO (0%) — visible at `/admin/tax-codes` + +## 1. Tax Codes + +- [ ] "New Tax Code" creates a code (e.g. `EXPORT`, `Zero-Rated Export`, `0`) and it appears in the grid +- [ ] Toggling a tax code's Active switch off, then back on, works +- [ ] A tax code is usable immediately on new invoice/credit note lines without a page refresh + +## 2. Customers + +- [ ] "New Customer" requires a branch selection; creating one without a branch shows an error +- [ ] A created customer appears in the Customers grid with the right code/name/credit limit/terms +- [ ] Switching the top-right Branch switcher to a single branch filters the Customers grid to + that branch only; switching back to Main shows all +- [ ] Clicking a customer row opens their Customer 360 page + +## 3. Customer 360 + +- [ ] Header shows the customer's name and an Active/Closed status pill +- [ ] Four summary tiles: Outstanding Balance, Overdue (30+ days), Credit Limit, Payment Terms +- [ ] A brand-new customer with no invoices shows R0.00 everywhere, not blank or broken +- [ ] Contact Details card shows code/email/phone/tax number (or "—" for anything empty) +- [ ] Three tabs — Invoices / Payments / Credit Notes — each show "No … yet." until you create one +- [ ] "New Invoice" and "Receive Payment" buttons only appear if you have the AR permissions + (test with a restricted role, or trust the code path — both gate on `ar.invoice.manage` / + `ar.receipt.manage`) + +## 4. Creating and posting an invoice + +- [ ] From Customer 360, "New Invoice" opens the invoice form with that customer pre-selected +- [ ] Due date auto-fills from invoice date + the customer's payment terms and is read-only +- [ ] Adding a line, picking a revenue account (only INCOME-type accounts for that customer's + branch are offered) and a tax code updates that line's Amount live +- [ ] Applying the STD (15%) tax code to a R1,000 line shows R150 tax and R1,150 line total +- [ ] The footer Subtotal / Tax / Total roll up correctly across multiple lines with different + tax codes (including a line with no tax code — contributes 0 to tax) +- [ ] "Save Draft" creates the invoice with status DRAFT and does **not** create a GL entry — + confirm by checking General Ledger's entry count is unchanged +- [ ] From the draft's detail page, "Post Invoice" posts it — status flips to POSTED, and a new + entry appears in General Ledger (journal `SALES`) that is balanced (Debits = Credits) +- [ ] Opening that GL entry shows: one debit line to the AR Control account for the invoice + total, one or more credit lines to the revenue account(s) for the subtotal(s), and (if + tax was applied) a credit line to Output Tax Payable for the tax total +- [ ] "Post Invoice" directly from the form (skipping the separate detail-page click) also works + and lands you on the now-posted invoice +- [ ] A draft invoice can be **Voided** (status → VOID, no GL entry ever created); a posted + invoice has no Void option — only a credit note can reverse it +- [ ] The invoice now appears in the customer's Invoices tab and in the global Invoices list, + with the correct balance and status pill + +## 5. Receiving a payment + +- [ ] From Customer 360, "Receive Payment" opens the receipt form with the customer pre-selected +- [ ] "Deposited to" only offers accounts marked as a Bank control account +- [ ] Saving and posting a receipt for the invoice's exact total creates a balanced GL entry: + debit to Bank, credit to AR Control, journal `CASH_RECEIPT` +- [ ] The posted receipt's detail page shows an "Allocate Against Invoices" panel listing the + customer's open (posted, balance > 0) invoices +- [ ] Clicking "Full" next to an invoice fills in its exact remaining balance (capped at what's + still unallocated on the receipt) +- [ ] Applying the allocation reduces both the invoice's balance and the receipt's unallocated + amount by the applied amount — reload both pages and confirm the numbers persist +- [ ] Fully allocating a receipt makes the "Allocate" panel show "Fully allocated." instead of + the invoice table +- [ ] Attempting to allocate more than a receipt's unallocated amount, or more than an invoice's + balance, is rejected with a clear error and changes nothing +- [ ] A part-paid invoice shows the correct partial balance on its detail page and in the + Customer 360 Invoices tab + +## 6. Credit notes + +- [ ] Create a credit note for the same customer, one line mirroring a posted invoice line + (same account, same tax code, same amount) +- [ ] Posting it creates a GL entry that is the mirror image of the invoice's: debit to the + revenue account (+ debit to Output Tax if taxed), credit to AR Control — and is balanced +- [ ] The credit note's detail page shows the same "Allocate Against Invoices" panel; allocating + it against the open invoice reduces both the invoice's balance and the credit note's + remaining balance +- [ ] A draft credit note can be voided; a posted one cannot + +## 7. Cross-cutting checks + +- [ ] Every invoice/receipt/credit note number follows its own series (`ARINV-2026-######`, + `ARRCT-2026-######`, `ARCN-2026-######`) and never repeats or skips oddly between runs +- [ ] The Dashboard's "Posted Movement by Account Type" bars now reflect AR activity (ASSET bar + grows from AR Control + Bank movement, INCOME bar grows from Sales) +- [ ] Switching to a specific branch (not Main) on any AR screen shows only that branch's + customers/invoices/receipts +- [ ] No console errors in the browser dev tools while working through this checklist + +--- + +Once this is ticked (or you've flagged issues), the plan's next step is Accounts Payable — +Suppliers, Supplier Invoices, Debit Notes, Payments — built the same way, followed by Tax +reporting and then Stock Control. diff --git a/PHASE_3_AP_TEST_PLAN.md b/PHASE_3_AP_TEST_PLAN.md new file mode 100644 index 0000000..e25922e --- /dev/null +++ b/PHASE_3_AP_TEST_PLAN.md @@ -0,0 +1,90 @@ +# Phase 3 — Accounts Payable: Manual Test Checklist + +Mirrors Accounts Receivable exactly, but for money owed to suppliers instead of money owed by +customers: Suppliers, Supplier Invoices (Bills), Debit Notes, and Payments with allocation — +same Draft → Post workflow, same shared `PostingService`, same Tax Codes. Backend correctness +is already covered by `tests/integration/ap.posting.spec.ts` (7 tests, passing alongside AR's +7 and GL's 9) and was additionally verified over the real HTTP API by hand (create supplier → +create/post invoice → create/post payment → allocate → invoice balance hit zero). This +checklist is for the parts only a human can judge. + +Start the same four dev servers as before and sign in through the Manager. + +## 0. New nav items + +- [ ] Sidebar shows a new **Purchasing** section (between Sales and General Ledger): Suppliers, + Bills, Payments + +## 1. Suppliers + +- [ ] "New Supplier" requires a branch selection; creating one without a branch shows an error +- [ ] A created supplier appears in the Suppliers grid +- [ ] Branch switcher filters the Suppliers grid the same way it does Customers +- [ ] Clicking a supplier opens their detail page (the AP equivalent of Customer 360) + +## 2. Supplier detail page + +- [ ] Header shows the supplier's name and an Active/Closed status pill +- [ ] Three summary tiles: Outstanding Balance, Overdue (30+ days), Payment Terms +- [ ] A brand-new supplier with no invoices shows R0.00 everywhere +- [ ] Contact Details card shows code/email/phone/tax number (or "—" for anything empty) +- [ ] Three tabs — Invoices / Payments / Debit Notes — each show "No … yet." until you create one +- [ ] "New Invoice" and "Make Payment" buttons only appear if you have the AP permissions + +## 3. Creating and posting a supplier invoice (Bill) + +- [ ] From the supplier's page, "New Invoice" opens the form with that supplier pre-selected +- [ ] Due date auto-fills from invoice date + the supplier's payment terms and is read-only +- [ ] Adding a line, picking an expense account (only EXPENSE-type accounts for that supplier's + branch are offered) and a tax code updates that line's Amount live +- [ ] Applying the STD (15%) tax code to a R1,000 line shows R150 tax and R1,150 line total +- [ ] "Save Draft" creates the invoice with status DRAFT and does **not** create a GL entry +- [ ] "Post Invoice" posts it — status flips to POSTED, and a new balanced entry appears in + General Ledger under journal `PURCHASES` +- [ ] Opening that entry shows: debit line(s) to the expense account(s) for the subtotal, a + debit line to Input Tax Receivable for the tax (if any), and a credit line to Accounts + Payable Control for the total +- [ ] A draft invoice can be **Voided**; a posted invoice has no Void option — only a debit note + can reverse it +- [ ] The invoice appears in the supplier's Invoices tab and in the global Bills list + +## 4. Making a payment + +- [ ] From the supplier's page, "Make Payment" opens the form with the supplier pre-selected +- [ ] "Paid from" only offers accounts marked as a Bank control account +- [ ] Posting a payment for the invoice's exact total creates a balanced GL entry: debit to + Accounts Payable Control, credit to Bank, journal `CASH_PAYMENT` +- [ ] The posted payment's detail page shows an "Allocate Against Invoices" panel listing the + supplier's open (posted, balance > 0) invoices +- [ ] "Full" fills in the invoice's exact remaining balance, capped at what's still unallocated + on the payment +- [ ] Applying the allocation reduces both the invoice's balance and the payment's unallocated + amount — reload both pages and confirm the numbers persist +- [ ] Over-allocating (more than the payment's unallocated amount, or more than an invoice's + balance) is rejected with a clear error and changes nothing +- [ ] A part-paid invoice shows the correct partial balance on its detail page and on the + supplier's Invoices tab + +## 5. Debit notes + +- [ ] Create a debit note for the same supplier, one line mirroring a posted invoice line +- [ ] Posting it creates the mirror-image GL entry: debit to Accounts Payable Control, credit + to the expense account (+ credit to Input Tax if taxed) — and is balanced +- [ ] The debit note's detail page shows the same allocation panel; allocating it against the + open invoice reduces both the invoice's balance and the debit note's remaining balance +- [ ] A draft debit note can be voided; a posted one cannot + +## 6. Cross-cutting checks + +- [ ] Every invoice/payment/debit-note number follows its own series (`APINV-2026-######`, + `APPMT-2026-######`, `APDN-2026-######`) and never repeats or skips oddly +- [ ] The Dashboard's "Posted Movement by Account Type" bars now also reflect AP activity + (LIABILITY bar grows from AP Control movement, EXPENSE bar grows from bills) +- [ ] Switching to a specific branch shows only that branch's suppliers/invoices/payments +- [ ] No console errors in the browser dev tools while working through this checklist + +--- + +Once this is ticked, the plan's next step is Tax reporting (VAT/output-input reconciliation), +then Stock Control + Procurement Matching (GRV/PPV, incorporating the Posting Ledger reference +already reviewed). diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..13865f3 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,46 @@ +# Setup — Phase 1 (Foundation) + +## Prerequisites +- Node.js 18+ (tested on v22) +- A reachable PostgreSQL 15+ database (this project does not use Docker) + +## Login +This app has no local login screen — see `../manager/`. Sign-in happens once at the central +Account Manager (2FA, Google, Microsoft), which hands this app a short-lived signed token +(`POST /api/auth/handoff`) instead of a password. `../manager/keys/handoff-public.pem` must be +copied into `keys/handoff-public.pem` here (see step 4) before that will work. + +## First-time setup +1. Copy `.env.example` to `.env` and fill in a real `DATABASE_URL`, two random JWT secrets + (e.g. `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`), and + `COMPANY_SLUG` (must match the `slug` of this company's `Company` row in the Manager). `.env` + is gitignored — never put real secrets in `.env.example`. +2. `npm install` +3. `npm run prisma:migrate` — applies all migrations, including the hand-written + `prisma/migrations/*_add_journal_balance_trigger/migration.sql` (the double-entry DB backstop; + source copy also kept at `prisma/manual-sql/add_journal_balance_trigger.sql` for reference). +4. `mkdir keys && cp ../manager/keys/handoff-public.pem keys/` (after running + `npm run generate-handoff-keys` in `../manager/` at least once). +5. `npm run prisma:seed` — creates the default 4-segment account structure, default Chart of + Accounts across two example branches (HQ, AMA), the GJ general journal, the 2026 fiscal year, + default roles/permissions, and an admin user (`joosth2002@icloud.com`, no local password — + this email must match the Manager `Person` who signs in). +6. `npm run dev` — starts the API on `http://localhost:4000` (see `PORT` in `.env`). + +## Verifying it works +- `npm test` — unit tests (no DB required; validates PostingService's pre-write balance checks). +- `npm run test:integration` — integration tests against the real DB: posts balanced/unbalanced + entries through the full stack (including a direct raw-SQL attempt that proves the DB trigger + backstop works even if `PostingService` is bypassed), and exchanges real handoff tokens (minted + with the Manager's real private key) to prove sign-in, replay protection, and cross-company + rejection all work end-to-end. +- `npm run smoke` — seeds two sample balanced entries (HQ and AMA), confirms an unbalanced one is + rejected, and prints a Main-level trial balance check. +- `GET /health` — basic liveness check. + +## Adding a new migration +Prisma's schema DSL can't express multi-row CHECK constraints or triggers, so any future raw-SQL +addition (e.g. more constraints) follows the same pattern used for the balance trigger: +1. `npx prisma migrate dev --create-only --name ` +2. Hand-edit the generated `migration.sql` +3. `npx prisma migrate dev` to apply it diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..df17633 --- /dev/null +++ b/client/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL="http://localhost:5000/api" +VITE_MANAGER_URL="http://localhost:5174" diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..438657a --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/.oxlintrc.json b/client/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/client/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/client/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..f4f90a7 --- /dev/null +++ b/client/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + Cross Financial Management + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..48a0304 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,2964 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@hookform/resolvers": "^5.9.1", + "@mui/icons-material": "^9.4.0", + "@mui/material": "^9.4.0", + "@mui/x-data-grid": "^9.12.0", + "@tanstack/react-query": "^5.102.8", + "axios": "^1.20.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-hook-form": "^7.87.0", + "react-router-dom": "^7.18.3", + "zod": "^3.25.76", + "zustand": "^5.0.15" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "typescript": "~6.0.2", + "vite": "^8.2.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@base-ui/utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", + "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", + "integrity": "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^1.2.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0 || ^18.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=6.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "@standard-schema/spec": { + "optional": true + }, + "@typeschema/main": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-errors": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "arktype": { + "optional": true + }, + "ata-validator": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "computed-types": { + "optional": true + }, + "effect": { + "optional": true + }, + "fluentvalidation-ts": { + "optional": true + }, + "fp-ts": { + "optional": true + }, + "io-ts": { + "optional": true + }, + "joi": { + "optional": true + }, + "nope-validator": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typanion": { + "optional": true + }, + "valibot": { + "optional": true + }, + "vest": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.4.0.tgz", + "integrity": "sha512-d+gIYM1raJP87yrVQcVg+Q3vycVMArMkgu/TjJLi2vTvO9FHKWNRR1xs9nnN4eLpM9nGxxm02UMvc9BrmcUKvQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.4.0.tgz", + "integrity": "sha512-5PVgBYtLOXTk6u0YUAjoV9GoTUQDbeZ8g+pus2h0GphLT/rpmftRnB5D/Kw6QCAovB7EyBX7UxKdZqPBBAHUKw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.4.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.4.0.tgz", + "integrity": "sha512-6KKUIvkQ2r/s48s9VZV9JEZ1W2dRa7RndVsF+Ipx4CYxDOQeozeRsS5r9NiheFf8A5hpbqJJIGoX7hjg5X4XUw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/core-downloads-tracker": "^9.4.0", + "@mui/system": "^9.4.0", + "@mui/types": "^9.4.0", + "@mui/utils": "^9.4.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.8", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.4.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.4.0.tgz", + "integrity": "sha512-qQpX/2XPGDz5F2OIWzo4kkorURlDeXGKytRQudXCdLS5TuwKsMKaZyx77LZEVSB73c7o3UrLWwQ/goRuHBdtjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.4.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.4.0.tgz", + "integrity": "sha512-SlqVEYqnyEgXMFW4JuIDMelyhKzoWBJ85v2mNCQd0XtU0uk7U9E1zaJZ06XqodhA8oJrb+4Q+3E/JlwBe4jWkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.4.0.tgz", + "integrity": "sha512-AZz9z13oGS1J0BYeAFHTbV7PegCtPiE/uMEIi7TJ1kTx3Vkt+jUpLyXF/2uV8B6Y79iTh98kVeOH9fX3x3zXXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/private-theming": "^9.4.0", + "@mui/styled-engine": "^9.4.0", + "@mui/types": "^9.4.0", + "@mui/utils": "^9.4.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.4.0.tgz", + "integrity": "sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.4.0.tgz", + "integrity": "sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.4.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-9.12.0.tgz", + "integrity": "sha512-lI7NYAPhoLIWJECB2gZuVairzWviDDOE1RpjLcPXJM+hw1mwCG+isqNfx81X+17xuHtnEEBVahCDWAJFAvCF9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "@mui/x-internals": "^9.12.0", + "@mui/x-virtualizer": "0.7.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^7.3.0 || ^9.0.0", + "@mui/system": "^7.3.0 || ^9.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.12.0.tgz", + "integrity": "sha512-rpG9EgJhH3JeOK2Q/tKaUWk5vP8C1NfL8DaXMKR1XCA7RhpZUI/xT67WKY80QtogNWu7U/BRezKyQGtiZ8soYg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.7.0.tgz", + "integrity": "sha512-Bz5p/t78zE2q6rf/nHY3j29hr7JlWaBNT0OD8jZFcfstr8jqrPkzirGsl/Bz5EpEjEzxV73kLfZLpIToGql+rA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "@mui/x-internals": "^9.12.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz", + "integrity": "sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.81.0.tgz", + "integrity": "sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz", + "integrity": "sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.81.0.tgz", + "integrity": "sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.81.0.tgz", + "integrity": "sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.81.0.tgz", + "integrity": "sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.81.0.tgz", + "integrity": "sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.81.0.tgz", + "integrity": "sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.81.0.tgz", + "integrity": "sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.81.0.tgz", + "integrity": "sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.81.0.tgz", + "integrity": "sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.81.0.tgz", + "integrity": "sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.81.0.tgz", + "integrity": "sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.81.0.tgz", + "integrity": "sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.81.0.tgz", + "integrity": "sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.81.0.tgz", + "integrity": "sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.81.0.tgz", + "integrity": "sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.81.0.tgz", + "integrity": "sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz", + "integrity": "sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oxlint": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.81.0.tgz", + "integrity": "sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.81.0", + "@oxlint/binding-android-arm64": "1.81.0", + "@oxlint/binding-darwin-arm64": "1.81.0", + "@oxlint/binding-darwin-x64": "1.81.0", + "@oxlint/binding-freebsd-x64": "1.81.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.81.0", + "@oxlint/binding-linux-arm-musleabihf": "1.81.0", + "@oxlint/binding-linux-arm64-gnu": "1.81.0", + "@oxlint/binding-linux-arm64-musl": "1.81.0", + "@oxlint/binding-linux-ppc64-gnu": "1.81.0", + "@oxlint/binding-linux-riscv64-gnu": "1.81.0", + "@oxlint/binding-linux-riscv64-musl": "1.81.0", + "@oxlint/binding-linux-s390x-gnu": "1.81.0", + "@oxlint/binding-linux-x64-gnu": "1.81.0", + "@oxlint/binding-linux-x64-musl": "1.81.0", + "@oxlint/binding-openharmony-arm64": "1.81.0", + "@oxlint/binding-win32-arm64-msvc": "1.81.0", + "@oxlint/binding-win32-ia32-msvc": "1.81.0", + "@oxlint/binding-win32-x64-msvc": "1.81.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-hook-form": { + "version": "7.87.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.87.0.tgz", + "integrity": "sha512-zhFzWvLxNHH+8839OnZcUxgMZw88ah2jZWDWvKWgF3Tpbnd0vKL+dlcuU3nZVWESZQjd81EW8K+wU+cYfYAc0w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..f3e9e6c --- /dev/null +++ b/client/package.json @@ -0,0 +1,37 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@hookform/resolvers": "^5.9.1", + "@mui/icons-material": "^9.4.0", + "@mui/material": "^9.4.0", + "@mui/x-data-grid": "^9.12.0", + "@tanstack/react-query": "^5.102.8", + "axios": "^1.20.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-hook-form": "^7.87.0", + "react-router-dom": "^7.18.3", + "zod": "^3.25.76", + "zustand": "^5.0.15" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "typescript": "~6.0.2", + "vite": "^8.2.2" + } +} diff --git a/client/public/favicon.svg b/client/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/client/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/icons.svg b/client/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/client/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..98e26ef --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,15 @@ +import { BrowserRouter } from 'react-router-dom'; +import { ThemeProvider, CssBaseline } from '@mui/material'; +import { theme } from './theme'; +import { AppRouter } from './routes/AppRouter'; + +export default function App() { + return ( + + + + + + + ); +} diff --git a/client/src/api/apDebitNotes.api.ts b/client/src/api/apDebitNotes.api.ts new file mode 100644 index 0000000..9b28c72 --- /dev/null +++ b/client/src/api/apDebitNotes.api.ts @@ -0,0 +1,51 @@ +import { apiClient } from './client'; +import type { APAllocation, APDebitNote } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchDebitNotes(branchScope: BranchSelection, supplierId?: number): Promise { + const response = await apiClient.get('/ap/debit-notes', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), supplierId }, + }); + return response.data; +} + +export async function fetchDebitNote(id: number): Promise { + const response = await apiClient.get(`/ap/debit-notes/${id}`); + return response.data; +} + +export interface DebitNoteLineInput { + description: string; + quantity: number; + unitPrice: number; + expenseGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateDebitNoteInput { + supplierId: number; + debitNoteDate: string; + reference?: string; + reason?: string; + lines: DebitNoteLineInput[]; +} + +export async function createDebitNote(input: CreateDebitNoteInput): Promise { + const response = await apiClient.post('/ap/debit-notes', input); + return response.data; +} + +export async function postDebitNote(id: number): Promise { + const response = await apiClient.post(`/ap/debit-notes/${id}/post`); + return response.data; +} + +export async function voidDebitNote(id: number): Promise { + const response = await apiClient.post(`/ap/debit-notes/${id}/void`); + return response.data; +} + +export async function allocateDebitNote(id: number, allocations: { invoiceId: number; amount: number }[]): Promise { + const response = await apiClient.post(`/ap/debit-notes/${id}/allocations`, { allocations }); + return response.data; +} diff --git a/client/src/api/apInvoices.api.ts b/client/src/api/apInvoices.api.ts new file mode 100644 index 0000000..b60ad08 --- /dev/null +++ b/client/src/api/apInvoices.api.ts @@ -0,0 +1,46 @@ +import { apiClient } from './client'; +import type { APInvoice } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchInvoices(branchScope: BranchSelection, supplierId?: number): Promise { + const response = await apiClient.get('/ap/invoices', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), supplierId }, + }); + return response.data; +} + +export async function fetchInvoice(id: number): Promise { + const response = await apiClient.get(`/ap/invoices/${id}`); + return response.data; +} + +export interface InvoiceLineInput { + description: string; + quantity: number; + unitPrice: number; + expenseGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateInvoiceInput { + supplierId: number; + invoiceDate: string; + dueDate: string; + supplierReference?: string; + lines: InvoiceLineInput[]; +} + +export async function createInvoice(input: CreateInvoiceInput): Promise { + const response = await apiClient.post('/ap/invoices', input); + return response.data; +} + +export async function postInvoice(id: number): Promise { + const response = await apiClient.post(`/ap/invoices/${id}/post`); + return response.data; +} + +export async function voidInvoice(id: number): Promise { + const response = await apiClient.post(`/ap/invoices/${id}/void`); + return response.data; +} diff --git a/client/src/api/apPayments.api.ts b/client/src/api/apPayments.api.ts new file mode 100644 index 0000000..6d72ae6 --- /dev/null +++ b/client/src/api/apPayments.api.ts @@ -0,0 +1,43 @@ +import { apiClient } from './client'; +import type { APAllocation, APPayment } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchPayments(branchScope: BranchSelection, supplierId?: number): Promise { + const response = await apiClient.get('/ap/payments', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), supplierId }, + }); + return response.data; +} + +export async function fetchPayment(id: number): Promise { + const response = await apiClient.get(`/ap/payments/${id}`); + return response.data; +} + +export interface CreatePaymentInput { + supplierId: number; + paymentDate: string; + amount: number; + bankGLAccountId: number; + reference?: string; +} + +export async function createPayment(input: CreatePaymentInput): Promise { + const response = await apiClient.post('/ap/payments', input); + return response.data; +} + +export async function postPayment(id: number): Promise { + const response = await apiClient.post(`/ap/payments/${id}/post`); + return response.data; +} + +export async function voidPayment(id: number): Promise { + const response = await apiClient.post(`/ap/payments/${id}/void`); + return response.data; +} + +export async function allocatePayment(id: number, allocations: { invoiceId: number; amount: number }[]): Promise { + const response = await apiClient.post(`/ap/payments/${id}/allocations`, { allocations }); + return response.data; +} diff --git a/client/src/api/arCreditNotes.api.ts b/client/src/api/arCreditNotes.api.ts new file mode 100644 index 0000000..de68245 --- /dev/null +++ b/client/src/api/arCreditNotes.api.ts @@ -0,0 +1,51 @@ +import { apiClient } from './client'; +import type { ARAllocation, ARCreditNote } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchCreditNotes(branchScope: BranchSelection, customerId?: number): Promise { + const response = await apiClient.get('/ar/credit-notes', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), customerId }, + }); + return response.data; +} + +export async function fetchCreditNote(id: number): Promise { + const response = await apiClient.get(`/ar/credit-notes/${id}`); + return response.data; +} + +export interface CreditNoteLineInput { + description: string; + quantity: number; + unitPrice: number; + revenueGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateCreditNoteInput { + customerId: number; + creditNoteDate: string; + reference?: string; + reason?: string; + lines: CreditNoteLineInput[]; +} + +export async function createCreditNote(input: CreateCreditNoteInput): Promise { + const response = await apiClient.post('/ar/credit-notes', input); + return response.data; +} + +export async function postCreditNote(id: number): Promise { + const response = await apiClient.post(`/ar/credit-notes/${id}/post`); + return response.data; +} + +export async function voidCreditNote(id: number): Promise { + const response = await apiClient.post(`/ar/credit-notes/${id}/void`); + return response.data; +} + +export async function allocateCreditNote(id: number, allocations: { invoiceId: number; amount: number }[]): Promise { + const response = await apiClient.post(`/ar/credit-notes/${id}/allocations`, { allocations }); + return response.data; +} diff --git a/client/src/api/arInvoices.api.ts b/client/src/api/arInvoices.api.ts new file mode 100644 index 0000000..93a8a46 --- /dev/null +++ b/client/src/api/arInvoices.api.ts @@ -0,0 +1,46 @@ +import { apiClient } from './client'; +import type { ARInvoice } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchInvoices(branchScope: BranchSelection, customerId?: number): Promise { + const response = await apiClient.get('/ar/invoices', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), customerId }, + }); + return response.data; +} + +export async function fetchInvoice(id: number): Promise { + const response = await apiClient.get(`/ar/invoices/${id}`); + return response.data; +} + +export interface InvoiceLineInput { + description: string; + quantity: number; + unitPrice: number; + revenueGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateInvoiceInput { + customerId: number; + invoiceDate: string; + dueDate: string; + reference?: string; + lines: InvoiceLineInput[]; +} + +export async function createInvoice(input: CreateInvoiceInput): Promise { + const response = await apiClient.post('/ar/invoices', input); + return response.data; +} + +export async function postInvoice(id: number): Promise { + const response = await apiClient.post(`/ar/invoices/${id}/post`); + return response.data; +} + +export async function voidInvoice(id: number): Promise { + const response = await apiClient.post(`/ar/invoices/${id}/void`); + return response.data; +} diff --git a/client/src/api/arReceipts.api.ts b/client/src/api/arReceipts.api.ts new file mode 100644 index 0000000..56ad4a3 --- /dev/null +++ b/client/src/api/arReceipts.api.ts @@ -0,0 +1,43 @@ +import { apiClient } from './client'; +import type { ARAllocation, ARReceipt } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchReceipts(branchScope: BranchSelection, customerId?: number): Promise { + const response = await apiClient.get('/ar/receipts', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), customerId }, + }); + return response.data; +} + +export async function fetchReceipt(id: number): Promise { + const response = await apiClient.get(`/ar/receipts/${id}`); + return response.data; +} + +export interface CreateReceiptInput { + customerId: number; + receiptDate: string; + amount: number; + bankGLAccountId: number; + reference?: string; +} + +export async function createReceipt(input: CreateReceiptInput): Promise { + const response = await apiClient.post('/ar/receipts', input); + return response.data; +} + +export async function postReceipt(id: number): Promise { + const response = await apiClient.post(`/ar/receipts/${id}/post`); + return response.data; +} + +export async function voidReceipt(id: number): Promise { + const response = await apiClient.post(`/ar/receipts/${id}/void`); + return response.data; +} + +export async function allocateReceipt(id: number, allocations: { invoiceId: number; amount: number }[]): Promise { + const response = await apiClient.post(`/ar/receipts/${id}/allocations`, { allocations }); + return response.data; +} diff --git a/client/src/api/auth.api.ts b/client/src/api/auth.api.ts new file mode 100644 index 0000000..44942ad --- /dev/null +++ b/client/src/api/auth.api.ts @@ -0,0 +1,18 @@ +import { apiClient } from './client'; +import type { AuthUser } from '../store/authStore'; + +export interface HandoffResponse { + accessToken: string; + refreshToken: string; + user: AuthUser; +} + +export async function handoff(token: string): Promise { + const response = await apiClient.post('/auth/handoff', { token }); + return response.data; +} + +export async function fetchMe(): Promise { + const response = await apiClient.get<{ user: AuthUser }>('/auth/me'); + return response.data.user; +} diff --git a/client/src/api/client.ts b/client/src/api/client.ts new file mode 100644 index 0000000..1116dd5 --- /dev/null +++ b/client/src/api/client.ts @@ -0,0 +1,53 @@ +import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'; +import { useAuthStore } from '../store/authStore'; + +const baseURL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5000/api'; + +export const apiClient = axios.create({ baseURL }); + +apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => { + const { accessToken } = useAuthStore.getState(); + if (accessToken) { + config.headers.set('Authorization', `Bearer ${accessToken}`); + } + return config; +}); + +let refreshInFlight: Promise | null = null; + +async function refreshAccessToken(): Promise { + const { refreshToken, setAccessToken, clearSession } = useAuthStore.getState(); + if (!refreshToken) { + clearSession(); + throw new Error('No refresh token available'); + } + try { + const response = await axios.post<{ accessToken: string }>(`${baseURL}/auth/refresh`, { refreshToken }); + setAccessToken(response.data.accessToken); + return response.data.accessToken; + } catch (error) { + clearSession(); + throw error; + } +} + +apiClient.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config as (InternalAxiosRequestConfig & { _retried?: boolean }) | undefined; + if (error.response?.status === 401 && originalRequest && !originalRequest._retried) { + originalRequest._retried = true; + try { + refreshInFlight ??= refreshAccessToken().finally(() => { + refreshInFlight = null; + }); + const accessToken = await refreshInFlight; + originalRequest.headers.set('Authorization', `Bearer ${accessToken}`); + return apiClient(originalRequest); + } catch { + return Promise.reject(error); + } + } + return Promise.reject(error); + }, +); diff --git a/client/src/api/customers.api.ts b/client/src/api/customers.api.ts new file mode 100644 index 0000000..8ec84df --- /dev/null +++ b/client/src/api/customers.api.ts @@ -0,0 +1,37 @@ +import { apiClient } from './client'; +import type { Customer, CustomerDetail } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchCustomers(branchScope: BranchSelection): Promise { + const response = await apiClient.get('/ar/customers', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope) }, + }); + return response.data; +} + +export async function fetchCustomer(id: number): Promise { + const response = await apiClient.get(`/ar/customers/${id}`); + return response.data; +} + +export interface CustomerInput { + code: string; + name: string; + email?: string; + phone?: string; + address?: string; + taxNumber?: string; + branchSegmentValueId: number; + creditLimit?: number; + paymentTermsDays?: number; +} + +export async function createCustomer(input: CustomerInput): Promise { + const response = await apiClient.post('/ar/customers', input); + return response.data; +} + +export async function updateCustomer(id: number, input: Partial & { isActive?: boolean }): Promise { + const response = await apiClient.put(`/ar/customers/${id}`, input); + return response.data; +} diff --git a/client/src/api/fiscalPeriods.api.ts b/client/src/api/fiscalPeriods.api.ts new file mode 100644 index 0000000..cfb6693 --- /dev/null +++ b/client/src/api/fiscalPeriods.api.ts @@ -0,0 +1,12 @@ +import { apiClient } from './client'; +import type { FiscalYear } from '../types/api'; + +export async function fetchFiscalYears(): Promise { + const response = await apiClient.get('/company/fiscal-years'); + return response.data; +} + +export async function createFiscalYear(year: number): Promise { + const response = await apiClient.post('/company/fiscal-years', { year }); + return response.data; +} diff --git a/client/src/api/glAccounts.api.ts b/client/src/api/glAccounts.api.ts new file mode 100644 index 0000000..3973026 --- /dev/null +++ b/client/src/api/glAccounts.api.ts @@ -0,0 +1,24 @@ +import { apiClient } from './client'; +import type { GLAccount, GLAccountType, DebitCredit, ControlAccountType } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchGLAccounts(branchScope: BranchSelection, accountType?: GLAccountType): Promise { + const response = await apiClient.get('/gl/accounts', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope), accountType }, + }); + return response.data; +} + +export interface CreateGLAccountInput { + accountName: string; + accountType: GLAccountType; + normalBalance: DebitCredit; + isControlAccount?: boolean; + controlType?: ControlAccountType; + segmentSelections: { segmentDefId: number; segmentValueId: number }[]; +} + +export async function createGLAccount(input: CreateGLAccountInput): Promise { + const response = await apiClient.post('/gl/accounts', input); + return response.data; +} diff --git a/client/src/api/journalEntries.api.ts b/client/src/api/journalEntries.api.ts new file mode 100644 index 0000000..3df41ae --- /dev/null +++ b/client/src/api/journalEntries.api.ts @@ -0,0 +1,40 @@ +import { apiClient } from './client'; +import type { JournalEntry } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchJournalEntries(branchScope: BranchSelection): Promise { + const response = await apiClient.get('/gl/journal-entries', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope) }, + }); + return response.data; +} + +export async function fetchJournalEntry(id: number): Promise { + const response = await apiClient.get(`/gl/journal-entries/${id}`); + return response.data; +} + +export interface CreateManualEntryLineInput { + glAccountId: number; + debit: number; + credit: number; + description?: string; + branchSegmentValueId?: number; +} + +export interface CreateManualEntryInput { + journalCode: string; + entryDate: string; + description: string; + lines: CreateManualEntryLineInput[]; +} + +export async function createManualEntry(input: CreateManualEntryInput): Promise { + const response = await apiClient.post('/gl/journal-entries', input); + return response.data; +} + +export async function reverseJournalEntry(id: number, reason?: string): Promise { + const response = await apiClient.post(`/gl/journal-entries/${id}/reverse`, { reason }); + return response.data; +} diff --git a/client/src/api/journals.api.ts b/client/src/api/journals.api.ts new file mode 100644 index 0000000..6ea196f --- /dev/null +++ b/client/src/api/journals.api.ts @@ -0,0 +1,19 @@ +import { apiClient } from './client'; +import type { Journal, JournalType } from '../types/api'; + +export async function fetchJournals(): Promise { + const response = await apiClient.get('/gl/journals'); + return response.data; +} + +export interface CreateJournalInput { + code: string; + name: string; + journalType: JournalType; + defaultBranchSegmentValueId?: number; +} + +export async function createJournal(input: CreateJournalInput): Promise { + const response = await apiClient.post('/gl/journals', input); + return response.data; +} diff --git a/client/src/api/roles.api.ts b/client/src/api/roles.api.ts new file mode 100644 index 0000000..379fec5 --- /dev/null +++ b/client/src/api/roles.api.ts @@ -0,0 +1,17 @@ +import { apiClient } from './client'; +import type { Permission, Role } from '../types/api'; + +export async function fetchRoles(): Promise { + const response = await apiClient.get('/roles'); + return response.data; +} + +export async function fetchPermissions(): Promise { + const response = await apiClient.get('/roles/permissions'); + return response.data; +} + +export async function setRolePermissions(roleId: number, permissionIds: number[]): Promise { + const response = await apiClient.put(`/roles/${roleId}/permissions`, { permissionIds }); + return response.data; +} diff --git a/client/src/api/segments.api.ts b/client/src/api/segments.api.ts new file mode 100644 index 0000000..3a37b64 --- /dev/null +++ b/client/src/api/segments.api.ts @@ -0,0 +1,37 @@ +import { apiClient } from './client'; +import type { AccountSegmentDefinition, AccountSegmentValue, SegmentFormat } from '../types/api'; + +export async function fetchSegmentDefinitions(): Promise { + const response = await apiClient.get('/company/segment-definitions'); + return response.data; +} + +export interface CreateSegmentDefinitionInput { + segmentKey: string; + label: string; + order: number; + length: number; + format: SegmentFormat; + isBranchSegment: boolean; +} + +export async function createSegmentDefinition(input: CreateSegmentDefinitionInput): Promise { + const response = await apiClient.post('/company/segment-definitions', input); + return response.data; +} + +export interface CreateSegmentValueInput { + segmentDefId: number; + code: string; + description: string; +} + +export async function createSegmentValue(input: CreateSegmentValueInput): Promise { + const response = await apiClient.post('/company/segment-values', input); + return response.data; +} + +export async function fetchBranchValues(): Promise { + const response = await apiClient.get('/company/branches'); + return response.data; +} diff --git a/client/src/api/suppliers.api.ts b/client/src/api/suppliers.api.ts new file mode 100644 index 0000000..5dd7f39 --- /dev/null +++ b/client/src/api/suppliers.api.ts @@ -0,0 +1,36 @@ +import { apiClient } from './client'; +import type { Supplier, SupplierDetail } from '../types/api'; +import type { BranchSelection } from '../store/branchStore'; + +export async function fetchSuppliers(branchScope: BranchSelection): Promise { + const response = await apiClient.get('/ap/suppliers', { + params: { branchScope: branchScope === 'MAIN' ? 'MAIN' : String(branchScope) }, + }); + return response.data; +} + +export async function fetchSupplier(id: number): Promise { + const response = await apiClient.get(`/ap/suppliers/${id}`); + return response.data; +} + +export interface SupplierInput { + code: string; + name: string; + email?: string; + phone?: string; + address?: string; + taxNumber?: string; + branchSegmentValueId: number; + paymentTermsDays?: number; +} + +export async function createSupplier(input: SupplierInput): Promise { + const response = await apiClient.post('/ap/suppliers', input); + return response.data; +} + +export async function updateSupplier(id: number, input: Partial & { isActive?: boolean }): Promise { + const response = await apiClient.put(`/ap/suppliers/${id}`, input); + return response.data; +} diff --git a/client/src/api/taxCodes.api.ts b/client/src/api/taxCodes.api.ts new file mode 100644 index 0000000..7ce4843 --- /dev/null +++ b/client/src/api/taxCodes.api.ts @@ -0,0 +1,24 @@ +import { apiClient } from './client'; +import type { TaxCode } from '../types/api'; + +export async function fetchTaxCodes(): Promise { + const response = await apiClient.get('/tax/codes'); + return response.data; +} + +export interface TaxCodeInput { + code: string; + name: string; + ratePercent: number; + isActive?: boolean; +} + +export async function createTaxCode(input: TaxCodeInput): Promise { + const response = await apiClient.post('/tax/codes', input); + return response.data; +} + +export async function updateTaxCode(id: number, input: Partial): Promise { + const response = await apiClient.put(`/tax/codes/${id}`, input); + return response.data; +} diff --git a/client/src/api/users.api.ts b/client/src/api/users.api.ts new file mode 100644 index 0000000..d750a9e --- /dev/null +++ b/client/src/api/users.api.ts @@ -0,0 +1,24 @@ +import { apiClient } from './client'; +import type { AppUser } from '../types/api'; + +export async function fetchUsers(): Promise { + const response = await apiClient.get('/users'); + return response.data; +} + +export interface CreateUserInput { + email: string; + fullName: string; + roleIds: number[]; + branchSegmentValueIds?: number[]; +} + +export async function createUser(input: CreateUserInput): Promise { + const response = await apiClient.post('/users', input); + return response.data; +} + +export async function setUserActive(id: number, isActive: boolean): Promise { + const response = await apiClient.patch(`/users/${id}/active`, { isActive }); + return response.data; +} diff --git a/client/src/components/common/BranchSwitcher.tsx b/client/src/components/common/BranchSwitcher.tsx new file mode 100644 index 0000000..66275ed --- /dev/null +++ b/client/src/components/common/BranchSwitcher.tsx @@ -0,0 +1,29 @@ +import { FormControl, MenuItem, Select, type SelectChangeEvent } from '@mui/material'; +import { useBranchScope } from '../../hooks/useBranchScope'; + +export function BranchSwitcher() { + const { selected, setSelected, branches, canViewMain } = useBranchScope(); + + const handleChange = (event: SelectChangeEvent) => { + const value = event.target.value; + setSelected(value === 'MAIN' ? 'MAIN' : Number(value)); + }; + + return ( + + + + ); +} diff --git a/client/src/components/common/ConfirmDialog.tsx b/client/src/components/common/ConfirmDialog.tsx new file mode 100644 index 0000000..1351875 --- /dev/null +++ b/client/src/components/common/ConfirmDialog.tsx @@ -0,0 +1,30 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; + +interface ConfirmDialogProps { + open: boolean; + title: string; + description: string; + confirmLabel?: string; + onConfirm: () => void; + onCancel: () => void; + loading?: boolean; +} + +export function ConfirmDialog({ open, title, description, confirmLabel = 'Confirm', onConfirm, onCancel, loading }: ConfirmDialogProps) { + return ( + + {title} + + {description} + + + + + + + ); +} diff --git a/client/src/components/common/MoneyCell.tsx b/client/src/components/common/MoneyCell.tsx new file mode 100644 index 0000000..6d5a070 --- /dev/null +++ b/client/src/components/common/MoneyCell.tsx @@ -0,0 +1,10 @@ +interface MoneyCellProps { + value: string | number | null | undefined; +} + +/** Display-only formatting — never round-trips this formatted value back to the server. */ +export function MoneyCell({ value }: MoneyCellProps) { + const amount = Number(value ?? 0); + if (amount === 0) return —; + return {amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}; +} diff --git a/client/src/components/common/StatusChip.tsx b/client/src/components/common/StatusChip.tsx new file mode 100644 index 0000000..02f014d --- /dev/null +++ b/client/src/components/common/StatusChip.tsx @@ -0,0 +1,15 @@ +import { Chip } from '@mui/material'; + +const COLOR_BY_STATUS: Record = { + POSTED: 'success', + DRAFT: 'warning', + REVERSED: 'error', + OPEN: 'success', + ACTIVE: 'success', + CLOSED: 'default', + LOCKED: 'warning', +}; + +export function StatusChip({ status }: { status: string }) { + return ; +} diff --git a/client/src/features/admin/coa/AdminChartOfAccountsPage.tsx b/client/src/features/admin/coa/AdminChartOfAccountsPage.tsx new file mode 100644 index 0000000..6bd998f --- /dev/null +++ b/client/src/features/admin/coa/AdminChartOfAccountsPage.tsx @@ -0,0 +1,157 @@ +import { useMemo, useState } from 'react'; +import { + Box, + Button, + Card, + Chip, + Collapse, + InputAdornment, + Stack, + TextField, + Typography, +} from '@mui/material'; +import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined'; +import ExpandMoreOutlinedIcon from '@mui/icons-material/ExpandMoreOutlined'; +import ChevronRightOutlinedIcon from '@mui/icons-material/ChevronRightOutlined'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useBranchStore } from '../../../store/branchStore'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { GLAccountDialog } from './GLAccountDialog'; +import type { GLAccount, GLAccountType } from '../../../types/api'; + +const TYPE_ORDER: GLAccountType[] = ['ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE']; + +export function AdminChartOfAccountsPage() { + const selected = useBranchStore((state) => state.selected); + const { data: accounts, isLoading } = useGLAccounts(selected); + const [dialogOpen, setDialogOpen] = useState(false); + const [search, setSearch] = useState(''); + const [collapsed, setCollapsed] = useState>(new Set()); + + const grouped = useMemo(() => { + const term = search.trim().toLowerCase(); + const filtered = (accounts ?? []).filter( + (a) => !term || a.accountCode.toLowerCase().includes(term) || a.accountName.toLowerCase().includes(term), + ); + const map = new Map(); + for (const type of TYPE_ORDER) map.set(type, []); + for (const account of filtered) { + if (!map.has(account.accountType)) map.set(account.accountType, []); + map.get(account.accountType)!.push(account); + } + for (const list of map.values()) list.sort((a, b) => a.accountCode.localeCompare(b.accountCode)); + return map; + }, [accounts, search]); + + const toggle = (type: GLAccountType) => + setCollapsed((prev) => { + const next = new Set(prev); + if (next.has(type)) next.delete(type); + else next.add(type); + return next; + }); + + return ( + + + Chart of Accounts + + + + setSearch(e.target.value)} + sx={{ mb: 2, width: 320 }} + slotProps={{ + input: { startAdornment: }, + }} + /> + + {isLoading && ( + + Loading… + + )} + + + {Array.from(grouped.entries()) + .filter(([, list]) => list.length > 0) + .map(([type, list]) => { + const isCollapsed = collapsed.has(type); + const totalBalance = list.length; + return ( + + toggle(type)} + sx={{ + alignItems: 'center', + px: 2, + py: 1.25, + cursor: 'pointer', + bgcolor: '#f7f9fd', + borderBottom: '1px solid', + borderColor: 'divider', + '&:hover': { bgcolor: '#eef2fa' }, + }} + > + {isCollapsed ? : } + + {type} + + + + + {list.map((account) => ( + + + {account.accountCode} + + + {account.accountName} + + + {account.normalBalance} + + + {account.isControlAccount && } + + + + + + ))} + {list.length === 0 && ( + + No accounts in this category. + + )} + + + ); + })} + + + setDialogOpen(false)} /> + + ); +} diff --git a/client/src/features/admin/coa/GLAccountDialog.tsx b/client/src/features/admin/coa/GLAccountDialog.tsx new file mode 100644 index 0000000..950428f --- /dev/null +++ b/client/src/features/admin/coa/GLAccountDialog.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Checkbox, + MenuItem, + Stack, + TextField, +} from '@mui/material'; +import { useSegmentDefinitions } from '../../../hooks/useSegments'; +import { useCreateGLAccount } from '../../../hooks/useGLAccounts'; +import type { GLAccountType, DebitCredit, ControlAccountType } from '../../../types/api'; + +const ACCOUNT_TYPES: GLAccountType[] = ['ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE']; +const DEBIT_CREDIT: DebitCredit[] = ['DEBIT', 'CREDIT']; +const CONTROL_TYPES: ControlAccountType[] = ['AR', 'AP', 'STOCK', 'TAX_OUTPUT', 'TAX_INPUT', 'BANK', 'RETAINED_EARNINGS']; + +export function GLAccountDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const { data: definitions } = useSegmentDefinitions(); + const createAccount = useCreateGLAccount(); + + const [accountName, setAccountName] = useState(''); + const [accountType, setAccountType] = useState('ASSET'); + const [normalBalance, setNormalBalance] = useState('DEBIT'); + const [isControlAccount, setIsControlAccount] = useState(false); + const [controlType, setControlType] = useState(''); + const [segmentValueBySegment, setSegmentValueBySegment] = useState>({}); + const [error, setError] = useState(null); + + const activeDefs = (definitions ?? []).filter((def) => def.isActive).sort((a, b) => a.order - b.order); + + const handleSubmit = async () => { + setError(null); + const segmentSelections = activeDefs.map((def) => ({ segmentDefId: def.id, segmentValueId: segmentValueBySegment[def.id] })); + if (segmentSelections.some((s) => !s.segmentValueId)) { + setError('Select a value for every segment'); + return; + } + try { + await createAccount.mutateAsync({ + accountName, + accountType, + normalBalance, + isControlAccount, + controlType: controlType || undefined, + segmentSelections, + }); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create account'); + } + }; + + return ( + + New GL Account + + + setAccountName(e.target.value)} fullWidth /> + + setAccountType(e.target.value as GLAccountType)} + fullWidth + > + {ACCOUNT_TYPES.map((type) => ( + + {type} + + ))} + + setNormalBalance(e.target.value as DebitCredit)} + fullWidth + > + {DEBIT_CREDIT.map((option) => ( + + {option} + + ))} + + + setIsControlAccount(e.target.checked)} />} + label="Control account" + /> + {isControlAccount && ( + setControlType(e.target.value as ControlAccountType)} + fullWidth + > + {CONTROL_TYPES.map((type) => ( + + {type} + + ))} + + )} + {activeDefs.map((def) => ( + setSegmentValueBySegment((prev) => ({ ...prev, [def.id]: Number(e.target.value) }))} + fullWidth + > + {def.values.map((value) => ( + + {value.code} — {value.description} + + ))} + + ))} + {error && {error}} + + + + + + + + ); +} diff --git a/client/src/features/admin/fiscal/AdminFiscalYearsPage.tsx b/client/src/features/admin/fiscal/AdminFiscalYearsPage.tsx new file mode 100644 index 0000000..2f0a609 --- /dev/null +++ b/client/src/features/admin/fiscal/AdminFiscalYearsPage.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import { Box, Button, Paper, Stack, TextField, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useCreateFiscalYear, useFiscalYears } from '../../../hooks/useFiscalYears'; +import { StatusChip } from '../../../components/common/StatusChip'; +import type { FiscalPeriod } from '../../../types/api'; + +export function AdminFiscalYearsPage() { + const { data: fiscalYears, isLoading } = useFiscalYears(); + const createFiscalYear = useCreateFiscalYear(); + const [newYear, setNewYear] = useState(''); + const [selectedYearId, setSelectedYearId] = useState(null); + + const selectedYear = (fiscalYears ?? []).find((year) => year.id === selectedYearId) ?? fiscalYears?.[0]; + + const periodColumns: GridColDef[] = [ + { field: 'periodNumber', headerName: 'Period', width: 100 }, + { field: 'startDate', headerName: 'Start', width: 140, valueFormatter: (value: string) => value?.slice(0, 10) }, + { field: 'endDate', headerName: 'End', width: 140, valueFormatter: (value: string) => value?.slice(0, 10) }, + { field: 'status', headerName: 'Status', width: 120, renderCell: (params) => }, + ]; + + return ( + + + Fiscal Years + + + setNewYear(e.target.value)} /> + + + + + {(fiscalYears ?? []).map((year) => ( + + ))} + + + {selectedYear && ( + + + + + + )} + + ); +} diff --git a/client/src/features/admin/journals/AdminJournalsPage.tsx b/client/src/features/admin/journals/AdminJournalsPage.tsx new file mode 100644 index 0000000..32fc036 --- /dev/null +++ b/client/src/features/admin/journals/AdminJournalsPage.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { Box, Button, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useJournals } from '../../../hooks/useJournals'; +import type { Journal } from '../../../types/api'; +import { JournalDialog } from './JournalDialog'; + +const columns: GridColDef[] = [ + { field: 'code', headerName: 'Code', width: 140 }, + { field: 'name', headerName: 'Name', flex: 1 }, + { field: 'journalType', headerName: 'Type', width: 160 }, + { field: 'isActive', headerName: 'Active', width: 100, type: 'boolean' }, +]; + +export function AdminJournalsPage() { + const { data: journals, isLoading } = useJournals(); + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + + + Journals + + + + + + setDialogOpen(false)} /> + + ); +} diff --git a/client/src/features/admin/journals/JournalDialog.tsx b/client/src/features/admin/journals/JournalDialog.tsx new file mode 100644 index 0000000..b0a1136 --- /dev/null +++ b/client/src/features/admin/journals/JournalDialog.tsx @@ -0,0 +1,71 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from '@mui/material'; +import { useCreateJournal } from '../../../hooks/useJournals'; +import { useSegmentDefinitions } from '../../../hooks/useSegments'; + +const JOURNAL_TYPES = ['GENERAL', 'SALES', 'PURCHASES', 'CASH_RECEIPT', 'CASH_PAYMENT', 'STOCK', 'BANK', 'CUSTOM'] as const; + +const schema = z.object({ + code: z.string().min(1).max(20), + name: z.string().min(1), + journalType: z.enum(JOURNAL_TYPES), + defaultBranchSegmentValueId: z.coerce.number().int().optional(), +}); + +type FormValues = z.infer; + +export function JournalDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const createJournal = useCreateJournal(); + const { data: definitions } = useSegmentDefinitions(); + const branchDef = (definitions ?? []).find((def) => def.isBranchSegment); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ resolver: zodResolver(schema), defaultValues: { code: '', name: '', journalType: 'CUSTOM' } }); + + const onSubmit = handleSubmit(async (values) => { + await createJournal.mutateAsync(values); + reset(); + onClose(); + }); + + return ( + + New Journal + + + + + + {JOURNAL_TYPES.map((type) => ( + + {type} + + ))} + + {branchDef && ( + + None + {branchDef.values.map((value) => ( + + {value.code} — {value.description} + + ))} + + )} + + + + + + + + ); +} diff --git a/client/src/features/admin/segments/AdminSegmentsPage.tsx b/client/src/features/admin/segments/AdminSegmentsPage.tsx new file mode 100644 index 0000000..f9ada13 --- /dev/null +++ b/client/src/features/admin/segments/AdminSegmentsPage.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; +import { + Box, + Button, + Chip, + CircularProgress, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useSegmentDefinitions } from '../../../hooks/useSegments'; +import { SegmentDefinitionDialog } from './SegmentDefinitionDialog'; +import { SegmentValueDialog } from './SegmentValueDialog'; + +export function AdminSegmentsPage() { + const { data: definitions, isLoading } = useSegmentDefinitions(); + const [defDialogOpen, setDefDialogOpen] = useState(false); + const [valueDialogFor, setValueDialogFor] = useState(null); + + if (isLoading) return ; + + return ( + + + Account Segments & Branches + + + + + {(definitions ?? []) + .sort((a, b) => a.order - b.order) + .map((def) => ( + + + + + {def.order}. {def.label} ({def.segmentKey}) + + {def.isBranchSegment && } + + + + + + + + Code + Description + + + + {def.values.map((value) => ( + + {value.code} + {value.description} + + ))} + +
+
+ ))} +
+ + setDefDialogOpen(false)} /> + setValueDialogFor(null)} /> +
+ ); +} diff --git a/client/src/features/admin/segments/SegmentDefinitionDialog.tsx b/client/src/features/admin/segments/SegmentDefinitionDialog.tsx new file mode 100644 index 0000000..25bc0bd --- /dev/null +++ b/client/src/features/admin/segments/SegmentDefinitionDialog.tsx @@ -0,0 +1,83 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + MenuItem, + Stack, + TextField, +} from '@mui/material'; +import { useCreateSegmentDefinition } from '../../../hooks/useSegments'; + +const schema = z.object({ + segmentKey: z.string().min(1).max(30), + label: z.string().min(1), + order: z.coerce.number().int().min(0), + length: z.coerce.number().int().min(1).max(20), + format: z.enum(['NUMERIC', 'ALPHA', 'ALPHANUMERIC']), + isBranchSegment: z.boolean(), +}); + +type FormValues = z.infer; + +export function SegmentDefinitionDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const createDefinition = useCreateSegmentDefinition(); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { segmentKey: '', label: '', order: 0, length: 3, format: 'NUMERIC', isBranchSegment: false }, + }); + + const onSubmit = handleSubmit(async (values) => { + await createDefinition.mutateAsync(values); + reset(); + onClose(); + }); + + return ( + + New Account Segment + + + + + + + + Numeric + Alpha + Alphanumeric + + } label="This is the branch segment" /> + + + + + + + + ); +} diff --git a/client/src/features/admin/segments/SegmentValueDialog.tsx b/client/src/features/admin/segments/SegmentValueDialog.tsx new file mode 100644 index 0000000..85e6d82 --- /dev/null +++ b/client/src/features/admin/segments/SegmentValueDialog.tsx @@ -0,0 +1,58 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from '@mui/material'; +import { useCreateSegmentValue } from '../../../hooks/useSegments'; + +const schema = z.object({ + code: z.string().min(1), + description: z.string().min(1), +}); + +type FormValues = z.infer; + +interface SegmentValueDialogProps { + open: boolean; + segmentDefId: number | null; + onClose: () => void; +} + +export function SegmentValueDialog({ open, segmentDefId, onClose }: SegmentValueDialogProps) { + const createValue = useCreateSegmentValue(); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ resolver: zodResolver(schema), defaultValues: { code: '', description: '' } }); + + const onSubmit = handleSubmit(async (values) => { + if (segmentDefId === null) return; + await createValue.mutateAsync({ segmentDefId, ...values }); + reset(); + onClose(); + }); + + return ( + + New Segment Value + + + + + + + + + + + + ); +} diff --git a/client/src/features/admin/tax/AdminTaxCodesPage.tsx b/client/src/features/admin/tax/AdminTaxCodesPage.tsx new file mode 100644 index 0000000..4b67711 --- /dev/null +++ b/client/src/features/admin/tax/AdminTaxCodesPage.tsx @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import { Box, Button, Card, Stack, Switch, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useTaxCodes, useUpdateTaxCode } from '../../../hooks/useTaxCodes'; +import { useAuth } from '../../../hooks/useAuth'; +import type { TaxCode } from '../../../types/api'; +import { TaxCodeDialog } from './TaxCodeDialog'; + +export function AdminTaxCodesPage() { + const { data: taxCodes, isLoading } = useTaxCodes(); + const updateTaxCode = useUpdateTaxCode(); + const { hasPermission } = useAuth(); + const [dialogOpen, setDialogOpen] = useState(false); + + const columns: GridColDef[] = [ + { field: 'code', headerName: 'Code', width: 140 }, + { field: 'name', headerName: 'Name', flex: 1 }, + { field: 'ratePercent', headerName: 'Rate %', width: 120, align: 'right', headerAlign: 'right' }, + { + field: 'isActive', + headerName: 'Active', + width: 100, + renderCell: (params) => ( + updateTaxCode.mutate({ id: params.row.id, input: { isActive: e.target.checked } })} + /> + ), + }, + ]; + + return ( + + + Tax Codes + {hasPermission('admin.tax.manage') && ( + + )} + + + + + setDialogOpen(false)} /> + + ); +} diff --git a/client/src/features/admin/tax/TaxCodeDialog.tsx b/client/src/features/admin/tax/TaxCodeDialog.tsx new file mode 100644 index 0000000..29fad46 --- /dev/null +++ b/client/src/features/admin/tax/TaxCodeDialog.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from '@mui/material'; +import { useCreateTaxCode } from '../../../hooks/useTaxCodes'; + +export function TaxCodeDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const createTaxCode = useCreateTaxCode(); + const [code, setCode] = useState(''); + const [name, setName] = useState(''); + const [ratePercent, setRatePercent] = useState(''); + const [error, setError] = useState(null); + + const handleSubmit = async () => { + setError(null); + try { + await createTaxCode.mutateAsync({ code, name, ratePercent: Number(ratePercent) }); + onClose(); + setCode(''); + setName(''); + setRatePercent(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create tax code'); + } + }; + + return ( + + New Tax Code + + + setCode(e.target.value.toUpperCase())} helperText="e.g. STD, ZERO" /> + setName(e.target.value)} /> + setRatePercent(e.target.value)} + slotProps={{ htmlInput: { min: 0, max: 100, step: '0.001' } }} + /> + {error && {error}} + + + + + + + + ); +} diff --git a/client/src/features/admin/users/AdminUsersPage.tsx b/client/src/features/admin/users/AdminUsersPage.tsx new file mode 100644 index 0000000..f4b2f57 --- /dev/null +++ b/client/src/features/admin/users/AdminUsersPage.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import { Box, Button, Stack, Switch, Tab, Tabs, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useSetUserActive, useUsers } from '../../../hooks/useUsers'; +import type { AppUser } from '../../../types/api'; +import { UserDialog } from './UserDialog'; +import { RolePermissionMatrix } from './RolePermissionMatrix'; + +export function AdminUsersPage() { + const { data: users, isLoading } = useUsers(); + const setActive = useSetUserActive(); + const [dialogOpen, setDialogOpen] = useState(false); + const [tab, setTab] = useState(0); + + const columns: GridColDef[] = [ + { field: 'fullName', headerName: 'Name', flex: 1 }, + { field: 'email', headerName: 'Email', flex: 1 }, + { + field: 'roles', + headerName: 'Roles', + flex: 1, + valueGetter: (_value, row) => row.roles.map((r) => r.role.name).join(', '), + }, + { + field: 'isActive', + headerName: 'Active', + width: 100, + renderCell: (params) => ( + setActive.mutate({ id: params.row.id, isActive: e.target.checked })} + /> + ), + }, + ]; + + return ( + + + Users & Roles + + setTab(value)} sx={{ mb: 2 }}> + + + + + {tab === 0 && ( + + + + + + + + setDialogOpen(false)} /> + + )} + + {tab === 1 && } + + ); +} diff --git a/client/src/features/admin/users/RolePermissionMatrix.tsx b/client/src/features/admin/users/RolePermissionMatrix.tsx new file mode 100644 index 0000000..c663597 --- /dev/null +++ b/client/src/features/admin/users/RolePermissionMatrix.tsx @@ -0,0 +1,53 @@ +import { Checkbox, CircularProgress, Paper, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material'; +import { useRoles, usePermissions, useSetRolePermissions } from '../../../hooks/useRoles'; + +export function RolePermissionMatrix() { + const { data: roles, isLoading: rolesLoading } = useRoles(); + const { data: permissions, isLoading: permissionsLoading } = usePermissions(); + const setRolePermissions = useSetRolePermissions(); + + if (rolesLoading || permissionsLoading) return ; + + const handleToggle = (roleId: number, currentPermissionIds: number[], permissionId: number) => { + const next = currentPermissionIds.includes(permissionId) + ? currentPermissionIds.filter((id) => id !== permissionId) + : [...currentPermissionIds, permissionId]; + setRolePermissions.mutate({ roleId, permissionIds: next }); + }; + + return ( + + + + + Permission + {(roles ?? []).map((role) => ( + + {role.name} + + ))} + + + + {(permissions ?? []).map((permission) => ( + + {permission.key} + {(roles ?? []).map((role) => { + const currentPermissionIds = role.permissions.map((rp) => rp.permission.id); + return ( + + handleToggle(role.id, currentPermissionIds, permission.id)} + /> + + ); + })} + + ))} + +
+
+ ); +} diff --git a/client/src/features/admin/users/UserDialog.tsx b/client/src/features/admin/users/UserDialog.tsx new file mode 100644 index 0000000..ad88d84 --- /dev/null +++ b/client/src/features/admin/users/UserDialog.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react'; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + FormGroup, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useCreateUser } from '../../../hooks/useUsers'; +import { useRoles } from '../../../hooks/useRoles'; +import { useSegmentDefinitions } from '../../../hooks/useSegments'; + +export function UserDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const { data: roles } = useRoles(); + const { data: definitions } = useSegmentDefinitions(); + const branchDef = (definitions ?? []).find((def) => def.isBranchSegment); + const createUser = useCreateUser(); + + const [email, setEmail] = useState(''); + const [fullName, setFullName] = useState(''); + const [roleIds, setRoleIds] = useState([]); + const [branchIds, setBranchIds] = useState([]); + const [error, setError] = useState(null); + + const toggle = (list: number[], setList: (ids: number[]) => void, id: number) => + setList(list.includes(id) ? list.filter((existing) => existing !== id) : [...list, id]); + + const handleSubmit = async () => { + setError(null); + if (roleIds.length === 0) { + setError('Select at least one role'); + return; + } + try { + await createUser.mutateAsync({ email, fullName, roleIds, branchSegmentValueIds: branchIds }); + onClose(); + setEmail(''); + setFullName(''); + setRoleIds([]); + setBranchIds([]); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create user'); + } + }; + + return ( + + New User + + + setFullName(e.target.value)} /> + setEmail(e.target.value)} + helperText="Must match the email this person signs in with at the Account Manager" + /> + Roles + + {(roles ?? []).map((role) => ( + toggle(roleIds, setRoleIds, role.id)} />} + label={role.name} + /> + ))} + + {branchDef && ( + <> + Branch restrictions (leave empty for unrestricted) + + {branchDef.values.map((value) => ( + toggle(branchIds, setBranchIds, value.id)} />} + label={value.code} + /> + ))} + + + )} + {error && {error}} + + + + + + + + ); +} diff --git a/client/src/features/ap/debitNotes/DebitNoteDetailPage.tsx b/client/src/features/ap/debitNotes/DebitNoteDetailPage.tsx new file mode 100644 index 0000000..345df92 --- /dev/null +++ b/client/src/features/ap/debitNotes/DebitNoteDetailPage.tsx @@ -0,0 +1,186 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + Alert, + Box, + Button, + Card, + CircularProgress, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useAllocateDebitNote, useDebitNote, usePostDebitNote, useVoidDebitNote } from '../../../hooks/useAPDebitNotes'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { APAllocationPanel } from '../shared/APAllocationPanel'; + +export function DebitNoteDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const debitNoteId = id ? Number(id) : undefined; + const { data: debitNote, isLoading } = useDebitNote(debitNoteId); + const postDebitNote = usePostDebitNote(); + const voidDebitNote = useVoidDebitNote(); + const allocateDebitNote = useAllocateDebitNote(); + const { hasPermission } = useAuth(); + const [error, setError] = useState(null); + const autoPosted = useRef(false); + + useEffect(() => { + if (searchParams.get('post') === '1' && debitNote?.status === 'DRAFT' && !autoPosted.current) { + autoPosted.current = true; + postDebitNote.mutateAsync(debitNote.id).catch((err) => { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post debit note'); + }); + setSearchParams({}, { replace: true }); + } + }, [debitNote, postDebitNote, searchParams, setSearchParams]); + + if (isLoading || !debitNote) return ; + + const handlePost = async () => { + setError(null); + try { + await postDebitNote.mutateAsync(debitNote.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post debit note'); + } + }; + + const handleVoid = async () => { + setError(null); + try { + await voidDebitNote.mutateAsync(debitNote.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to void debit note'); + } + }; + + return ( + + + + {debitNote.debitNoteNumber} + + + {debitNote.status === 'DRAFT' && hasPermission('ap.debitnote.manage') && ( + + + + + )} + + + {error && ( + + {error} + + )} + + + + + + Supplier + + navigate(`/ap/suppliers/${debitNote.supplierId}`)} + > + {debitNote.supplier?.name} + + + + + Date + + {debitNote.debitNoteDate.slice(0, 10)} + + + + Reason + + {debitNote.reason || '—'} + + + + Unallocated Balance + + + + + + + + + + + + + Description + Qty + Unit Price + Expense Account + Tax + Amount + + + + {debitNote.lines.map((line) => ( + + {line.description} + {line.quantity} + + + + {line.expenseGLAccount?.accountName} + {line.taxCode?.code ?? '—'} + + + + + ))} + + + Total + + + + + + + + +
+
+ + {debitNote.status === 'POSTED' && ( + + + Allocate Against Invoices + + allocateDebitNote.mutateAsync({ id: debitNote.id, allocations })} + /> + + )} +
+ ); +} diff --git a/client/src/features/ap/debitNotes/DebitNoteFormPage.tsx b/client/src/features/ap/debitNotes/DebitNoteFormPage.tsx new file mode 100644 index 0000000..9e07cfe --- /dev/null +++ b/client/src/features/ap/debitNotes/DebitNoteFormPage.tsx @@ -0,0 +1,153 @@ +import { useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, MenuItem, Stack, TextField, Typography } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { useSuppliers } from '../../../hooks/useSuppliers'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useTaxCodes } from '../../../hooks/useTaxCodes'; +import { useCreateDebitNote } from '../../../hooks/useAPDebitNotes'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { APDocumentLineEditor, blankAPDocumentLine, apLineTotals, type APDocumentLineDraft } from '../shared/APDocumentLineEditor'; + +export function DebitNoteFormPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const preselectedSupplierId = searchParams.get('supplierId'); + + const { data: suppliers } = useSuppliers('MAIN'); + const { data: taxCodes } = useTaxCodes(); + const createDebitNote = useCreateDebitNote(); + + const [supplierId, setSupplierId] = useState(preselectedSupplierId ? Number(preselectedSupplierId) : ''); + const [debitNoteDate, setDebitNoteDate] = useState(new Date().toISOString().slice(0, 10)); + const [reference, setReference] = useState(''); + const [reason, setReason] = useState(''); + const [lines, setLines] = useState([blankAPDocumentLine()]); + const [error, setError] = useState(null); + + const supplier = (suppliers ?? []).find((s) => s.id === supplierId); + const { data: expenseAccounts } = useGLAccounts(supplier ? supplier.branchSegmentValueId : 'MAIN', 'EXPENSE'); + + const totals = useMemo(() => { + const computed = lines.map((line) => apLineTotals(line, taxCodes ?? [])); + return { + subtotal: computed.reduce((sum, l) => sum + l.subtotal, 0), + tax: computed.reduce((sum, l) => sum + l.tax, 0), + total: computed.reduce((sum, l) => sum + l.total, 0), + }; + }, [lines, taxCodes]); + + const save = async (postAfter: boolean) => { + setError(null); + if (!supplierId) { + setError('Select a supplier'); + return; + } + if (lines.some((line) => !line.expenseGLAccountId || !line.description || !Number(line.unitPrice))) { + setError('Every line needs a description, unit price, and expense account'); + return; + } + try { + const debitNote = await createDebitNote.mutateAsync({ + supplierId, + debitNoteDate, + reference: reference || undefined, + reason: reason || undefined, + lines: lines.map((line) => ({ + description: line.description, + quantity: Number(line.quantity) || 1, + unitPrice: Number(line.unitPrice) || 0, + expenseGLAccountId: line.expenseGLAccountId as number, + taxCodeId: line.taxCodeId ? (line.taxCodeId as number) : undefined, + })), + }); + navigate(postAfter ? `/ap/debit-notes/${debitNote.id}?post=1` : `/ap/debit-notes/${debitNote.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to save debit note')); + } + }; + + return ( + + + New Debit Note + + + + + setSupplierId(Number(e.target.value))} + sx={{ minWidth: 260 }} + > + {(suppliers ?? []).map((s) => ( + + {s.code} — {s.name} + + ))} + + setDebitNoteDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setReference(e.target.value)} sx={{ minWidth: 180 }} /> + setReason(e.target.value)} sx={{ minWidth: 220 }} /> + + + + + + + + + + + Subtotal + + + + + + + + Tax + + + + + + + Total + + + + + + + + {error && ( + + {error} + + )} + + + + + + + + ); +} diff --git a/client/src/features/ap/invoices/InvoiceDetailPage.tsx b/client/src/features/ap/invoices/InvoiceDetailPage.tsx new file mode 100644 index 0000000..eeb2f22 --- /dev/null +++ b/client/src/features/ap/invoices/InvoiceDetailPage.tsx @@ -0,0 +1,224 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + Alert, + Box, + Button, + Card, + CircularProgress, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useInvoice, usePostInvoice, useVoidInvoice } from '../../../hooks/useAPInvoices'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; + +export function InvoiceDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const invoiceId = id ? Number(id) : undefined; + const { data: invoice, isLoading } = useInvoice(invoiceId); + const postInvoice = usePostInvoice(); + const voidInvoice = useVoidInvoice(); + const { hasPermission } = useAuth(); + const [error, setError] = useState(null); + const autoPosted = useRef(false); + + useEffect(() => { + if (searchParams.get('post') === '1' && invoice?.status === 'DRAFT' && !autoPosted.current) { + autoPosted.current = true; + postInvoice.mutateAsync(invoice.id).catch((err) => { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post invoice'); + }); + setSearchParams({}, { replace: true }); + } + }, [invoice, postInvoice, searchParams, setSearchParams]); + + if (isLoading || !invoice) return ; + + const handlePost = async () => { + setError(null); + try { + await postInvoice.mutateAsync(invoice.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post invoice'); + } + }; + + const handleVoid = async () => { + setError(null); + try { + await voidInvoice.mutateAsync(invoice.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to void invoice'); + } + }; + + return ( + + + + {invoice.invoiceNumber} + + + + {invoice.status === 'DRAFT' && hasPermission('ap.invoice.manage') && ( + <> + + + + )} + + + + {error && ( + + {error} + + )} + + + + + + Supplier + + navigate(`/ap/suppliers/${invoice.supplierId}`)} + > + {invoice.supplier?.name} + + + + + Invoice Date + + {invoice.invoiceDate.slice(0, 10)} + + + + Due Date + + {invoice.dueDate.slice(0, 10)} + + + + Supplier Reference + + {invoice.supplierReference || '—'} + + + + Balance + + + + + + + + + + + + + Description + Qty + Unit Price + Expense Account + Tax + Amount + + + + {invoice.lines.map((line) => ( + + {line.description} + {line.quantity} + + + + {line.expenseGLAccount?.accountName} + {line.taxCode?.code ?? '—'} + + + + + ))} + + + Subtotal + + + + + + + + Tax + + + + + + + + Total + + + + + + + + +
+
+ + {invoice.allocations && invoice.allocations.length > 0 && ( + + + Allocations + + + + + Source + Date + Amount + + + + {invoice.allocations.map((allocation) => ( + + + {allocation.payment?.paymentNumber ?? allocation.debitNote?.debitNoteNumber} + + {allocation.allocatedAt.slice(0, 10)} + + + + + ))} + +
+
+ )} +
+ ); +} diff --git a/client/src/features/ap/invoices/InvoiceFormPage.tsx b/client/src/features/ap/invoices/InvoiceFormPage.tsx new file mode 100644 index 0000000..468decf --- /dev/null +++ b/client/src/features/ap/invoices/InvoiceFormPage.tsx @@ -0,0 +1,164 @@ +import { useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, MenuItem, Stack, TextField, Typography } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { useSuppliers } from '../../../hooks/useSuppliers'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useTaxCodes } from '../../../hooks/useTaxCodes'; +import { useCreateInvoice } from '../../../hooks/useAPInvoices'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { APDocumentLineEditor, blankAPDocumentLine, apLineTotals, type APDocumentLineDraft } from '../shared/APDocumentLineEditor'; + +function addDays(dateStr: string, days: number): string { + const date = new Date(dateStr); + date.setDate(date.getDate() + days); + return date.toISOString().slice(0, 10); +} + +export function InvoiceFormPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const preselectedSupplierId = searchParams.get('supplierId'); + + const { data: suppliers } = useSuppliers('MAIN'); + const { data: taxCodes } = useTaxCodes(); + const createInvoice = useCreateInvoice(); + + const [supplierId, setSupplierId] = useState(preselectedSupplierId ? Number(preselectedSupplierId) : ''); + const [invoiceDate, setInvoiceDate] = useState(new Date().toISOString().slice(0, 10)); + const [supplierReference, setSupplierReference] = useState(''); + const [lines, setLines] = useState([blankAPDocumentLine()]); + const [error, setError] = useState(null); + + const supplier = (suppliers ?? []).find((s) => s.id === supplierId); + const { data: expenseAccounts } = useGLAccounts(supplier ? supplier.branchSegmentValueId : 'MAIN', 'EXPENSE'); + const dueDate = supplier ? addDays(invoiceDate, supplier.paymentTermsDays) : invoiceDate; + + const totals = useMemo(() => { + const computed = lines.map((line) => apLineTotals(line, taxCodes ?? [])); + return { + subtotal: computed.reduce((sum, l) => sum + l.subtotal, 0), + tax: computed.reduce((sum, l) => sum + l.tax, 0), + total: computed.reduce((sum, l) => sum + l.total, 0), + }; + }, [lines, taxCodes]); + + const save = async (postAfter: boolean) => { + setError(null); + if (!supplierId) { + setError('Select a supplier'); + return; + } + if (lines.some((line) => !line.expenseGLAccountId || !line.description || !Number(line.unitPrice))) { + setError('Every line needs a description, unit price, and expense account'); + return; + } + try { + const invoice = await createInvoice.mutateAsync({ + supplierId, + invoiceDate, + dueDate, + supplierReference: supplierReference || undefined, + lines: lines.map((line) => ({ + description: line.description, + quantity: Number(line.quantity) || 1, + unitPrice: Number(line.unitPrice) || 0, + expenseGLAccountId: line.expenseGLAccountId as number, + taxCodeId: line.taxCodeId ? (line.taxCodeId as number) : undefined, + })), + }); + navigate(postAfter ? `/ap/invoices/${invoice.id}?post=1` : `/ap/invoices/${invoice.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to save invoice')); + } + }; + + return ( + + + New Supplier Invoice + + + + + setSupplierId(Number(e.target.value))} + sx={{ minWidth: 260 }} + > + {(suppliers ?? []).map((s) => ( + + {s.code} — {s.name} + + ))} + + setInvoiceDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + + setSupplierReference(e.target.value)} + sx={{ minWidth: 200 }} + /> + + + + + + + + + + + Subtotal + + + + + + + + Tax + + + + + + + Total + + + + + + + + {error && ( + + {error} + + )} + + + + + + + + ); +} diff --git a/client/src/features/ap/invoices/InvoicesListPage.tsx b/client/src/features/ap/invoices/InvoicesListPage.tsx new file mode 100644 index 0000000..0d71e2f --- /dev/null +++ b/client/src/features/ap/invoices/InvoicesListPage.tsx @@ -0,0 +1,65 @@ +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useInvoices } from '../../../hooks/useAPInvoices'; +import { useBranchStore } from '../../../store/branchStore'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { APInvoice } from '../../../types/api'; + +export function InvoicesListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: invoices, isLoading } = useInvoices(selected); + const { hasPermission } = useAuth(); + + const columns: GridColDef[] = [ + { field: 'invoiceNumber', headerName: 'Invoice #', width: 180 }, + { field: 'supplier', headerName: 'Supplier', flex: 1, valueGetter: (_v, row) => row.supplier?.name }, + { field: 'invoiceDate', headerName: 'Date', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { field: 'dueDate', headerName: 'Due', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { + field: 'total', + headerName: 'Total', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { + field: 'balance', + headerName: 'Balance', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { field: 'status', headerName: 'Status', width: 120, renderCell: (params) => }, + ]; + + return ( + + + Supplier Invoices + {hasPermission('ap.invoice.manage') && ( + + )} + + + navigate(`/ap/invoices/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + initialState={{ sorting: { sortModel: [{ field: 'invoiceDate', sort: 'desc' }] } }} + /> + + + ); +} diff --git a/client/src/features/ap/payments/PaymentDetailPage.tsx b/client/src/features/ap/payments/PaymentDetailPage.tsx new file mode 100644 index 0000000..7a37713 --- /dev/null +++ b/client/src/features/ap/payments/PaymentDetailPage.tsx @@ -0,0 +1,140 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, CircularProgress, Stack, Typography } from '@mui/material'; +import { useAllocatePayment, usePayment, usePostPayment, useVoidPayment } from '../../../hooks/useAPPayments'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { APAllocationPanel } from '../shared/APAllocationPanel'; + +export function PaymentDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const paymentId = id ? Number(id) : undefined; + const { data: payment, isLoading } = usePayment(paymentId); + const postPayment = usePostPayment(); + const voidPayment = useVoidPayment(); + const allocatePayment = useAllocatePayment(); + const { hasPermission } = useAuth(); + const [error, setError] = useState(null); + const autoPosted = useRef(false); + + useEffect(() => { + if (searchParams.get('post') === '1' && payment?.status === 'DRAFT' && !autoPosted.current) { + autoPosted.current = true; + postPayment.mutateAsync(payment.id).catch((err) => { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post payment'); + }); + setSearchParams({}, { replace: true }); + } + }, [payment, postPayment, searchParams, setSearchParams]); + + if (isLoading || !payment) return ; + + const handlePost = async () => { + setError(null); + try { + await postPayment.mutateAsync(payment.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post payment'); + } + }; + + const handleVoid = async () => { + setError(null); + try { + await voidPayment.mutateAsync(payment.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to void payment'); + } + }; + + return ( + + + + {payment.paymentNumber} + + + {payment.status === 'DRAFT' && hasPermission('ap.payment.manage') && ( + + + + + )} + + + {error && ( + + {error} + + )} + + + + + + Supplier + + navigate(`/ap/suppliers/${payment.supplierId}`)} + > + {payment.supplier?.name} + + + + + Date + + {payment.paymentDate.slice(0, 10)} + + + + Paid From + + {payment.bankGLAccount?.accountName} + + + + Amount + + + + + + + + Unallocated + + + + + + + + + {payment.status === 'POSTED' && ( + + + Allocate Against Invoices + + allocatePayment.mutateAsync({ id: payment.id, allocations })} + /> + + )} + + ); +} diff --git a/client/src/features/ap/payments/PaymentFormPage.tsx b/client/src/features/ap/payments/PaymentFormPage.tsx new file mode 100644 index 0000000..f0a8bc7 --- /dev/null +++ b/client/src/features/ap/payments/PaymentFormPage.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, MenuItem, Stack, TextField, Typography } from '@mui/material'; +import { useSuppliers } from '../../../hooks/useSuppliers'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useCreatePayment } from '../../../hooks/useAPPayments'; + +export function PaymentFormPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const preselectedSupplierId = searchParams.get('supplierId'); + + const { data: suppliers } = useSuppliers('MAIN'); + const createPayment = useCreatePayment(); + + const [supplierId, setSupplierId] = useState(preselectedSupplierId ? Number(preselectedSupplierId) : ''); + const [paymentDate, setPaymentDate] = useState(new Date().toISOString().slice(0, 10)); + const [amount, setAmount] = useState(''); + const [bankGLAccountId, setBankGLAccountId] = useState(''); + const [reference, setReference] = useState(''); + const [error, setError] = useState(null); + + const supplier = (suppliers ?? []).find((s) => s.id === supplierId); + const { data: assetAccounts } = useGLAccounts(supplier ? supplier.branchSegmentValueId : 'MAIN', 'ASSET'); + const bankAccounts = (assetAccounts ?? []).filter((account) => account.controlType === 'BANK'); + + const save = async (postAfter: boolean) => { + setError(null); + if (!supplierId || !bankGLAccountId || !Number(amount)) { + setError('Select a supplier, bank account, and enter an amount'); + return; + } + try { + const payment = await createPayment.mutateAsync({ + supplierId, + paymentDate, + amount: Number(amount), + bankGLAccountId, + reference: reference || undefined, + }); + navigate(postAfter ? `/ap/payments/${payment.id}?post=1` : `/ap/payments/${payment.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to save payment')); + } + }; + + return ( + + + Make Payment + + + + + setSupplierId(Number(e.target.value))}> + {(suppliers ?? []).map((s) => ( + + {s.code} — {s.name} + + ))} + + setPaymentDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setAmount(e.target.value)} + slotProps={{ htmlInput: { min: 0, step: '0.01' } }} + /> + setBankGLAccountId(Number(e.target.value))}> + {bankAccounts.map((account) => ( + + {account.accountCode} — {account.accountName} + + ))} + + setReference(e.target.value)} /> + + + + {error && ( + + {error} + + )} + + + + + + + + ); +} diff --git a/client/src/features/ap/payments/PaymentsListPage.tsx b/client/src/features/ap/payments/PaymentsListPage.tsx new file mode 100644 index 0000000..8b27bdd --- /dev/null +++ b/client/src/features/ap/payments/PaymentsListPage.tsx @@ -0,0 +1,64 @@ +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { usePayments } from '../../../hooks/useAPPayments'; +import { useBranchStore } from '../../../store/branchStore'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { APPayment } from '../../../types/api'; + +export function PaymentsListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: payments, isLoading } = usePayments(selected); + const { hasPermission } = useAuth(); + + const columns: GridColDef[] = [ + { field: 'paymentNumber', headerName: 'Payment #', width: 180 }, + { field: 'supplier', headerName: 'Supplier', flex: 1, valueGetter: (_v, row) => row.supplier?.name }, + { field: 'paymentDate', headerName: 'Date', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { + field: 'amount', + headerName: 'Amount', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { + field: 'unallocatedAmount', + headerName: 'Unallocated', + width: 130, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { field: 'status', headerName: 'Status', width: 120, renderCell: (params) => }, + ]; + + return ( + + + Payments + {hasPermission('ap.payment.manage') && ( + + )} + + + navigate(`/ap/payments/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + initialState={{ sorting: { sortModel: [{ field: 'paymentDate', sort: 'desc' }] } }} + /> + + + ); +} diff --git a/client/src/features/ap/shared/APAllocationPanel.tsx b/client/src/features/ap/shared/APAllocationPanel.tsx new file mode 100644 index 0000000..6a3d811 --- /dev/null +++ b/client/src/features/ap/shared/APAllocationPanel.tsx @@ -0,0 +1,126 @@ +import { useMemo, useState } from 'react'; +import { Alert, Box, Button, Stack, Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from '@mui/material'; +import { useInvoices } from '../../../hooks/useAPInvoices'; +import { MoneyCell } from '../../../components/common/MoneyCell'; + +interface APAllocationPanelProps { + supplierId: number; + unallocatedAmount: number; + onAllocate: (allocations: { invoiceId: number; amount: number }[]) => Promise; + isSubmitting: boolean; +} + +export function APAllocationPanel({ supplierId, unallocatedAmount, onAllocate, isSubmitting }: APAllocationPanelProps) { + const { data: invoices } = useInvoices('MAIN', supplierId); + const openInvoices = (invoices ?? []).filter((invoice) => invoice.status === 'POSTED' && Number(invoice.balance) > 0); + + const [amounts, setAmounts] = useState>({}); + const [error, setError] = useState(null); + + const totalEntered = useMemo( + () => Object.values(amounts).reduce((sum, value) => sum + (Number(value) || 0), 0), + [amounts], + ); + const remaining = unallocatedAmount - totalEntered; + + if (unallocatedAmount <= 0) { + return ( + + Fully allocated. + + ); + } + + const applyFull = (invoiceId: number, balance: number) => { + const applied = Math.min(balance, unallocatedAmount - totalEntered + (Number(amounts[invoiceId]) || 0)); + setAmounts((prev) => ({ ...prev, [invoiceId]: applied > 0 ? applied.toFixed(2) : '' })); + }; + + const handleApply = async () => { + setError(null); + const allocations = Object.entries(amounts) + .map(([invoiceId, value]) => ({ invoiceId: Number(invoiceId), amount: Number(value) || 0 })) + .filter((allocation) => allocation.amount > 0); + if (allocations.length === 0) { + setError('Enter at least one allocation amount'); + return; + } + if (totalEntered > unallocatedAmount) { + setError('Total allocated exceeds the unallocated amount'); + return; + } + try { + await onAllocate(allocations); + setAmounts({}); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to allocate')); + } + }; + + if (openInvoices.length === 0) { + return ( + + This supplier has no open invoices to allocate against. + + ); + } + + return ( + + + + + Invoice # + Due + Balance + + Apply Amount + + + + + {openInvoices.map((invoice) => ( + + {invoice.invoiceNumber} + {invoice.dueDate.slice(0, 10)} + + + + + + setAmounts((prev) => ({ ...prev, [invoice.id]: e.target.value }))} + sx={{ width: 100 }} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + + + ))} + +
+ + + + Remaining unallocated: + + + + + {error && ( + + {error} + + )} +
+ ); +} diff --git a/client/src/features/ap/shared/APDocumentLineEditor.tsx b/client/src/features/ap/shared/APDocumentLineEditor.tsx new file mode 100644 index 0000000..675429f --- /dev/null +++ b/client/src/features/ap/shared/APDocumentLineEditor.tsx @@ -0,0 +1,142 @@ +import { IconButton, MenuItem, Table, TableBody, TableCell, TableHead, TableRow, TextField } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { GLAccount, TaxCode } from '../../../types/api'; + +export interface APDocumentLineDraft { + key: number; + description: string; + quantity: string; + unitPrice: string; + expenseGLAccountId: number | ''; + taxCodeId: number | ''; +} + +let keySeed = 0; +export const blankAPDocumentLine = (): APDocumentLineDraft => ({ + key: keySeed++, + description: '', + quantity: '1', + unitPrice: '', + expenseGLAccountId: '', + taxCodeId: '', +}); + +export function apLineTotals(line: APDocumentLineDraft, taxCodes: TaxCode[]) { + const quantity = Number(line.quantity) || 0; + const unitPrice = Number(line.unitPrice) || 0; + const subtotal = quantity * unitPrice; + const rate = line.taxCodeId ? Number(taxCodes.find((t) => t.id === line.taxCodeId)?.ratePercent ?? 0) : 0; + const tax = subtotal * (rate / 100); + return { subtotal, tax, total: subtotal + tax }; +} + +interface APDocumentLineEditorProps { + lines: APDocumentLineDraft[]; + onChange: (lines: APDocumentLineDraft[]) => void; + expenseAccounts: GLAccount[]; + taxCodes: TaxCode[]; +} + +export function APDocumentLineEditor({ lines, onChange, expenseAccounts, taxCodes }: APDocumentLineEditorProps) { + const updateLine = (key: number, patch: Partial) => + onChange(lines.map((line) => (line.key === key ? { ...line, ...patch } : line))); + + const removeLine = (key: number) => onChange(lines.length > 1 ? lines.filter((line) => line.key !== key) : lines); + + return ( + + + + Description + + Qty + + + Unit Price + + Expense Account + Tax + + Amount + + + + + + {lines.map((line) => { + const { total } = apLineTotals(line, taxCodes); + return ( + + + updateLine(line.key, { description: e.target.value })} + /> + + + updateLine(line.key, { quantity: e.target.value })} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + updateLine(line.key, { unitPrice: e.target.value })} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + updateLine(line.key, { expenseGLAccountId: Number(e.target.value) })} + > + {expenseAccounts.map((account) => ( + + {account.accountCode} — {account.accountName} + + ))} + + + + updateLine(line.key, { taxCodeId: e.target.value ? Number(e.target.value) : '' })} + > + None + {taxCodes.map((taxCode) => ( + + {taxCode.code} ({Number(taxCode.ratePercent)}%) + + ))} + + + + + + + removeLine(line.key)} disabled={lines.length <= 1}> + + + + + ); + })} + +
+ ); +} diff --git a/client/src/features/ap/suppliers/SupplierDetailPage.tsx b/client/src/features/ap/suppliers/SupplierDetailPage.tsx new file mode 100644 index 0000000..735602b --- /dev/null +++ b/client/src/features/ap/suppliers/SupplierDetailPage.tsx @@ -0,0 +1,218 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Box, Button, Card, CircularProgress, Stack, Tab, Tabs, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material'; +import { useSupplier } from '../../../hooks/useSuppliers'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { APDebitNote, APInvoice, APPayment } from '../../../types/api'; + +function InvoicesTable({ invoices, onRowClick }: { invoices: APInvoice[]; onRowClick: (id: number) => void }) { + if (invoices.length === 0) return No invoices yet.; + return ( + + + + Invoice # + Supplier Ref + Date + Due + Total + Balance + Status + + + + {invoices.map((invoice) => ( + onRowClick(invoice.id)} sx={{ cursor: 'pointer' }}> + {invoice.invoiceNumber} + {invoice.supplierReference || '—'} + {invoice.invoiceDate.slice(0, 10)} + {invoice.dueDate.slice(0, 10)} + + + + + + + + + + + ))} + +
+ ); +} + +function PaymentsTable({ payments }: { payments: APPayment[] }) { + if (payments.length === 0) return No payments yet.; + return ( + + + + Payment # + Date + Amount + Unallocated + Status + + + + {payments.map((payment) => ( + + {payment.paymentNumber} + {payment.paymentDate.slice(0, 10)} + + + + + + + + + + + ))} + +
+ ); +} + +function DebitNotesTable({ debitNotes }: { debitNotes: APDebitNote[] }) { + if (debitNotes.length === 0) return No debit notes yet.; + return ( + + + + Debit Note # + Date + Total + Unallocated + Status + + + + {debitNotes.map((debitNote) => ( + + {debitNote.debitNoteNumber} + {debitNote.debitNoteDate.slice(0, 10)} + + + + + + + + + + + ))} + +
+ ); +} + +export function SupplierDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const { data, isLoading } = useSupplier(id ? Number(id) : undefined); + const { hasPermission } = useAuth(); + const [tab, setTab] = useState(0); + + if (isLoading || !data) return ; + + const { supplier, invoices, debitNotes, payments, outstandingBalance, overdueBalance } = data; + + return ( + + + + {supplier.name} + + + + {hasPermission('ap.payment.manage') && ( + + )} + {hasPermission('ap.invoice.manage') && ( + + )} + + + + + + + Outstanding Balance + + + + + + + + Overdue (30+ days) + + 0 ? 'error.main' : 'text.primary'}> + + + + + + Payment Terms + + {supplier.paymentTermsDays} days + + + + + + Contact Details + + + + + Code + + {supplier.code} + + + + Email + + {supplier.email || '—'} + + + + Phone + + {supplier.phone || '—'} + + + + Tax Number + + {supplier.taxNumber || '—'} + + + + + setTab(value)} sx={{ mb: 2 }}> + + + + + + + {tab === 0 && navigate(`/ap/invoices/${invoiceId}`)} />} + {tab === 1 && } + {tab === 2 && } + + + ); +} diff --git a/client/src/features/ap/suppliers/SupplierDialog.tsx b/client/src/features/ap/suppliers/SupplierDialog.tsx new file mode 100644 index 0000000..f6772ca --- /dev/null +++ b/client/src/features/ap/suppliers/SupplierDialog.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from '@mui/material'; +import { useCreateSupplier } from '../../../hooks/useSuppliers'; +import { useSegmentDefinitions } from '../../../hooks/useSegments'; + +export function SupplierDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const createSupplier = useCreateSupplier(); + const { data: definitions } = useSegmentDefinitions(); + const branchDef = (definitions ?? []).find((def) => def.isBranchSegment); + + const [code, setCode] = useState(''); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [phone, setPhone] = useState(''); + const [branchSegmentValueId, setBranchSegmentValueId] = useState(''); + const [paymentTermsDays, setPaymentTermsDays] = useState('30'); + const [error, setError] = useState(null); + + const reset = () => { + setCode(''); + setName(''); + setEmail(''); + setPhone(''); + setBranchSegmentValueId(''); + setPaymentTermsDays('30'); + }; + + const handleSubmit = async () => { + setError(null); + if (!branchSegmentValueId) { + setError('Select a branch'); + return; + } + try { + await createSupplier.mutateAsync({ + code, + name, + email: email || undefined, + phone: phone || undefined, + branchSegmentValueId, + paymentTermsDays: paymentTermsDays ? Number(paymentTermsDays) : undefined, + }); + onClose(); + reset(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create supplier'); + } + }; + + return ( + + New Supplier + + + setCode(e.target.value)} /> + setName(e.target.value)} /> + setEmail(e.target.value)} /> + setPhone(e.target.value)} /> + setBranchSegmentValueId(Number(e.target.value))} + > + {(branchDef?.values ?? []).map((value) => ( + + {value.code} — {value.description} + + ))} + + setPaymentTermsDays(e.target.value)} + /> + {error && {error}} + + + + + + + + ); +} diff --git a/client/src/features/ap/suppliers/SuppliersListPage.tsx b/client/src/features/ap/suppliers/SuppliersListPage.tsx new file mode 100644 index 0000000..0a26317 --- /dev/null +++ b/client/src/features/ap/suppliers/SuppliersListPage.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useSuppliers } from '../../../hooks/useSuppliers'; +import { useBranchStore } from '../../../store/branchStore'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import type { Supplier } from '../../../types/api'; +import { SupplierDialog } from './SupplierDialog'; + +export function SuppliersListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: suppliers, isLoading } = useSuppliers(selected); + const { hasPermission } = useAuth(); + const [dialogOpen, setDialogOpen] = useState(false); + + const columns: GridColDef[] = [ + { field: 'code', headerName: 'Code', width: 140 }, + { field: 'name', headerName: 'Name', flex: 1 }, + { field: 'email', headerName: 'Email', flex: 1 }, + { field: 'phone', headerName: 'Phone', width: 160 }, + { field: 'paymentTermsDays', headerName: 'Terms (days)', width: 130 }, + { + field: 'isActive', + headerName: 'Status', + width: 100, + renderCell: (params) => , + }, + ]; + + return ( + + + Suppliers + {hasPermission('ap.supplier.manage') && ( + + )} + + + navigate(`/ap/suppliers/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + /> + + setDialogOpen(false)} /> + + ); +} diff --git a/client/src/features/ar/creditNotes/CreditNoteDetailPage.tsx b/client/src/features/ar/creditNotes/CreditNoteDetailPage.tsx new file mode 100644 index 0000000..7bf4869 --- /dev/null +++ b/client/src/features/ar/creditNotes/CreditNoteDetailPage.tsx @@ -0,0 +1,186 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + Alert, + Box, + Button, + Card, + CircularProgress, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useAllocateCreditNote, useCreditNote, usePostCreditNote, useVoidCreditNote } from '../../../hooks/useARCreditNotes'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { AllocationPanel } from '../shared/AllocationPanel'; + +export function CreditNoteDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const creditNoteId = id ? Number(id) : undefined; + const { data: creditNote, isLoading } = useCreditNote(creditNoteId); + const postCreditNote = usePostCreditNote(); + const voidCreditNote = useVoidCreditNote(); + const allocateCreditNote = useAllocateCreditNote(); + const { hasPermission } = useAuth(); + const [error, setError] = useState(null); + const autoPosted = useRef(false); + + useEffect(() => { + if (searchParams.get('post') === '1' && creditNote?.status === 'DRAFT' && !autoPosted.current) { + autoPosted.current = true; + postCreditNote.mutateAsync(creditNote.id).catch((err) => { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post credit note'); + }); + setSearchParams({}, { replace: true }); + } + }, [creditNote, postCreditNote, searchParams, setSearchParams]); + + if (isLoading || !creditNote) return ; + + const handlePost = async () => { + setError(null); + try { + await postCreditNote.mutateAsync(creditNote.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post credit note'); + } + }; + + const handleVoid = async () => { + setError(null); + try { + await voidCreditNote.mutateAsync(creditNote.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to void credit note'); + } + }; + + return ( + + + + {creditNote.creditNoteNumber} + + + {creditNote.status === 'DRAFT' && hasPermission('ar.creditnote.manage') && ( + + + + + )} + + + {error && ( + + {error} + + )} + + + + + + Customer + + navigate(`/ar/customers/${creditNote.customerId}`)} + > + {creditNote.customer?.name} + + + + + Date + + {creditNote.creditNoteDate.slice(0, 10)} + + + + Reason + + {creditNote.reason || '—'} + + + + Unallocated Balance + + + + + + + + + + + + + Description + Qty + Unit Price + Revenue Account + Tax + Amount + + + + {creditNote.lines.map((line) => ( + + {line.description} + {line.quantity} + + + + {line.revenueGLAccount?.accountName} + {line.taxCode?.code ?? '—'} + + + + + ))} + + + Total + + + + + + + + +
+
+ + {creditNote.status === 'POSTED' && ( + + + Allocate Against Invoices + + allocateCreditNote.mutateAsync({ id: creditNote.id, allocations })} + /> + + )} +
+ ); +} diff --git a/client/src/features/ar/creditNotes/CreditNoteFormPage.tsx b/client/src/features/ar/creditNotes/CreditNoteFormPage.tsx new file mode 100644 index 0000000..7e9cf03 --- /dev/null +++ b/client/src/features/ar/creditNotes/CreditNoteFormPage.tsx @@ -0,0 +1,153 @@ +import { useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, MenuItem, Stack, TextField, Typography } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { useCustomers } from '../../../hooks/useCustomers'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useTaxCodes } from '../../../hooks/useTaxCodes'; +import { useCreateCreditNote } from '../../../hooks/useARCreditNotes'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { DocumentLineEditor, blankDocumentLine, lineTotals, type DocumentLineDraft } from '../shared/DocumentLineEditor'; + +export function CreditNoteFormPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const preselectedCustomerId = searchParams.get('customerId'); + + const { data: customers } = useCustomers('MAIN'); + const { data: taxCodes } = useTaxCodes(); + const createCreditNote = useCreateCreditNote(); + + const [customerId, setCustomerId] = useState(preselectedCustomerId ? Number(preselectedCustomerId) : ''); + const [creditNoteDate, setCreditNoteDate] = useState(new Date().toISOString().slice(0, 10)); + const [reference, setReference] = useState(''); + const [reason, setReason] = useState(''); + const [lines, setLines] = useState([blankDocumentLine()]); + const [error, setError] = useState(null); + + const customer = (customers ?? []).find((c) => c.id === customerId); + const { data: revenueAccounts } = useGLAccounts(customer ? customer.branchSegmentValueId : 'MAIN', 'INCOME'); + + const totals = useMemo(() => { + const computed = lines.map((line) => lineTotals(line, taxCodes ?? [])); + return { + subtotal: computed.reduce((sum, l) => sum + l.subtotal, 0), + tax: computed.reduce((sum, l) => sum + l.tax, 0), + total: computed.reduce((sum, l) => sum + l.total, 0), + }; + }, [lines, taxCodes]); + + const save = async (postAfter: boolean) => { + setError(null); + if (!customerId) { + setError('Select a customer'); + return; + } + if (lines.some((line) => !line.revenueGLAccountId || !line.description || !Number(line.unitPrice))) { + setError('Every line needs a description, unit price, and revenue account'); + return; + } + try { + const creditNote = await createCreditNote.mutateAsync({ + customerId, + creditNoteDate, + reference: reference || undefined, + reason: reason || undefined, + lines: lines.map((line) => ({ + description: line.description, + quantity: Number(line.quantity) || 1, + unitPrice: Number(line.unitPrice) || 0, + revenueGLAccountId: line.revenueGLAccountId as number, + taxCodeId: line.taxCodeId ? (line.taxCodeId as number) : undefined, + })), + }); + navigate(postAfter ? `/ar/credit-notes/${creditNote.id}?post=1` : `/ar/credit-notes/${creditNote.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to save credit note')); + } + }; + + return ( + + + New Credit Note + + + + + setCustomerId(Number(e.target.value))} + sx={{ minWidth: 260 }} + > + {(customers ?? []).map((c) => ( + + {c.code} — {c.name} + + ))} + + setCreditNoteDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setReference(e.target.value)} sx={{ minWidth: 180 }} /> + setReason(e.target.value)} sx={{ minWidth: 220 }} /> + + + + + + + + + + + Subtotal + + + + + + + + Tax + + + + + + + Total + + + + + + + + {error && ( + + {error} + + )} + + + + + + + + ); +} diff --git a/client/src/features/ar/customers/CustomerDetailPage.tsx b/client/src/features/ar/customers/CustomerDetailPage.tsx new file mode 100644 index 0000000..5860952 --- /dev/null +++ b/client/src/features/ar/customers/CustomerDetailPage.tsx @@ -0,0 +1,224 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Box, Button, Card, CircularProgress, Stack, Tab, Tabs, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material'; +import { useCustomer } from '../../../hooks/useCustomers'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { ARCreditNote, ARInvoice, ARReceipt } from '../../../types/api'; + +function InvoicesTable({ invoices, onRowClick }: { invoices: ARInvoice[]; onRowClick: (id: number) => void }) { + if (invoices.length === 0) return No invoices yet.; + return ( + + + + Invoice # + Date + Due + Total + Balance + Status + + + + {invoices.map((invoice) => ( + onRowClick(invoice.id)} sx={{ cursor: 'pointer' }}> + {invoice.invoiceNumber} + {invoice.invoiceDate.slice(0, 10)} + {invoice.dueDate.slice(0, 10)} + + + + + + + + + + + ))} + +
+ ); +} + +function ReceiptsTable({ receipts }: { receipts: ARReceipt[] }) { + if (receipts.length === 0) return No receipts yet.; + return ( + + + + Receipt # + Date + Amount + Unallocated + Status + + + + {receipts.map((receipt) => ( + + {receipt.receiptNumber} + {receipt.receiptDate.slice(0, 10)} + + + + + + + + + + + ))} + +
+ ); +} + +function CreditNotesTable({ creditNotes }: { creditNotes: ARCreditNote[] }) { + if (creditNotes.length === 0) return No credit notes yet.; + return ( + + + + Credit Note # + Date + Total + Unallocated + Status + + + + {creditNotes.map((creditNote) => ( + + {creditNote.creditNoteNumber} + {creditNote.creditNoteDate.slice(0, 10)} + + + + + + + + + + + ))} + +
+ ); +} + +export function CustomerDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const { data, isLoading } = useCustomer(id ? Number(id) : undefined); + const { hasPermission } = useAuth(); + const [tab, setTab] = useState(0); + + if (isLoading || !data) return ; + + const { customer, invoices, creditNotes, receipts, outstandingBalance, overdueBalance } = data; + + return ( + + + + {customer.name} + + + + {hasPermission('ar.receipt.manage') && ( + + )} + {hasPermission('ar.invoice.manage') && ( + + )} + + + + + + + Outstanding Balance + + + + + + + + Overdue (30+ days) + + 0 ? 'error.main' : 'text.primary'}> + + + + + + Credit Limit + + + + + + + + Payment Terms + + {customer.paymentTermsDays} days + + + + + + Contact Details + + + + + Code + + {customer.code} + + + + Email + + {customer.email || '—'} + + + + Phone + + {customer.phone || '—'} + + + + Tax Number + + {customer.taxNumber || '—'} + + + + + setTab(value)} sx={{ mb: 2 }}> + + + + + + + {tab === 0 && navigate(`/ar/invoices/${invoiceId}`)} />} + {tab === 1 && } + {tab === 2 && } + + + ); +} diff --git a/client/src/features/ar/customers/CustomerDialog.tsx b/client/src/features/ar/customers/CustomerDialog.tsx new file mode 100644 index 0000000..3e7f801 --- /dev/null +++ b/client/src/features/ar/customers/CustomerDialog.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from '@mui/material'; +import { useCreateCustomer } from '../../../hooks/useCustomers'; +import { useSegmentDefinitions } from '../../../hooks/useSegments'; + +export function CustomerDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const createCustomer = useCreateCustomer(); + const { data: definitions } = useSegmentDefinitions(); + const branchDef = (definitions ?? []).find((def) => def.isBranchSegment); + + const [code, setCode] = useState(''); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [phone, setPhone] = useState(''); + const [branchSegmentValueId, setBranchSegmentValueId] = useState(''); + const [creditLimit, setCreditLimit] = useState(''); + const [paymentTermsDays, setPaymentTermsDays] = useState('30'); + const [error, setError] = useState(null); + + const reset = () => { + setCode(''); + setName(''); + setEmail(''); + setPhone(''); + setBranchSegmentValueId(''); + setCreditLimit(''); + setPaymentTermsDays('30'); + }; + + const handleSubmit = async () => { + setError(null); + if (!branchSegmentValueId) { + setError('Select a branch'); + return; + } + try { + await createCustomer.mutateAsync({ + code, + name, + email: email || undefined, + phone: phone || undefined, + branchSegmentValueId, + creditLimit: creditLimit ? Number(creditLimit) : undefined, + paymentTermsDays: paymentTermsDays ? Number(paymentTermsDays) : undefined, + }); + onClose(); + reset(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create customer'); + } + }; + + return ( + + New Customer + + + setCode(e.target.value)} /> + setName(e.target.value)} /> + setEmail(e.target.value)} /> + setPhone(e.target.value)} /> + setBranchSegmentValueId(Number(e.target.value))} + > + {(branchDef?.values ?? []).map((value) => ( + + {value.code} — {value.description} + + ))} + + + setCreditLimit(e.target.value)} + fullWidth + /> + setPaymentTermsDays(e.target.value)} + fullWidth + /> + + {error && {error}} + + + + + + + + ); +} diff --git a/client/src/features/ar/customers/CustomersListPage.tsx b/client/src/features/ar/customers/CustomersListPage.tsx new file mode 100644 index 0000000..c3d3c63 --- /dev/null +++ b/client/src/features/ar/customers/CustomersListPage.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useCustomers } from '../../../hooks/useCustomers'; +import { useBranchStore } from '../../../store/branchStore'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { Customer } from '../../../types/api'; +import { CustomerDialog } from './CustomerDialog'; + +export function CustomersListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: customers, isLoading } = useCustomers(selected); + const { hasPermission } = useAuth(); + const [dialogOpen, setDialogOpen] = useState(false); + + const columns: GridColDef[] = [ + { field: 'code', headerName: 'Code', width: 140 }, + { field: 'name', headerName: 'Name', flex: 1 }, + { field: 'email', headerName: 'Email', flex: 1 }, + { field: 'phone', headerName: 'Phone', width: 160 }, + { + field: 'creditLimit', + headerName: 'Credit Limit', + width: 140, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { field: 'paymentTermsDays', headerName: 'Terms (days)', width: 130 }, + { + field: 'isActive', + headerName: 'Status', + width: 100, + renderCell: (params) => , + }, + ]; + + return ( + + + Customers + {hasPermission('ar.customer.manage') && ( + + )} + + + navigate(`/ar/customers/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + /> + + setDialogOpen(false)} /> + + ); +} diff --git a/client/src/features/ar/invoices/InvoiceDetailPage.tsx b/client/src/features/ar/invoices/InvoiceDetailPage.tsx new file mode 100644 index 0000000..fe041c4 --- /dev/null +++ b/client/src/features/ar/invoices/InvoiceDetailPage.tsx @@ -0,0 +1,224 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + Alert, + Box, + Button, + Card, + CircularProgress, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useInvoice, usePostInvoice, useVoidInvoice } from '../../../hooks/useARInvoices'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; + +export function InvoiceDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const invoiceId = id ? Number(id) : undefined; + const { data: invoice, isLoading } = useInvoice(invoiceId); + const postInvoice = usePostInvoice(); + const voidInvoice = useVoidInvoice(); + const { hasPermission } = useAuth(); + const [error, setError] = useState(null); + const autoPosted = useRef(false); + + useEffect(() => { + if (searchParams.get('post') === '1' && invoice?.status === 'DRAFT' && !autoPosted.current) { + autoPosted.current = true; + postInvoice.mutateAsync(invoice.id).catch((err) => { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post invoice'); + }); + setSearchParams({}, { replace: true }); + } + }, [invoice, postInvoice, searchParams, setSearchParams]); + + if (isLoading || !invoice) return ; + + const handlePost = async () => { + setError(null); + try { + await postInvoice.mutateAsync(invoice.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post invoice'); + } + }; + + const handleVoid = async () => { + setError(null); + try { + await voidInvoice.mutateAsync(invoice.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to void invoice'); + } + }; + + return ( + + + + {invoice.invoiceNumber} + + + + {invoice.status === 'DRAFT' && hasPermission('ar.invoice.manage') && ( + <> + + + + )} + + + + {error && ( + + {error} + + )} + + + + + + Customer + + navigate(`/ar/customers/${invoice.customerId}`)} + > + {invoice.customer?.name} + + + + + Invoice Date + + {invoice.invoiceDate.slice(0, 10)} + + + + Due Date + + {invoice.dueDate.slice(0, 10)} + + + + Reference + + {invoice.reference || '—'} + + + + Balance + + + + + + + + + + + + + Description + Qty + Unit Price + Revenue Account + Tax + Amount + + + + {invoice.lines.map((line) => ( + + {line.description} + {line.quantity} + + + + {line.revenueGLAccount?.accountName} + {line.taxCode?.code ?? '—'} + + + + + ))} + + + Subtotal + + + + + + + + Tax + + + + + + + + Total + + + + + + + + +
+
+ + {invoice.allocations && invoice.allocations.length > 0 && ( + + + Allocations + + + + + Source + Date + Amount + + + + {invoice.allocations.map((allocation) => ( + + + {allocation.receipt?.receiptNumber ?? allocation.creditNote?.creditNoteNumber} + + {allocation.allocatedAt.slice(0, 10)} + + + + + ))} + +
+
+ )} +
+ ); +} diff --git a/client/src/features/ar/invoices/InvoiceFormPage.tsx b/client/src/features/ar/invoices/InvoiceFormPage.tsx new file mode 100644 index 0000000..2f16f91 --- /dev/null +++ b/client/src/features/ar/invoices/InvoiceFormPage.tsx @@ -0,0 +1,178 @@ +import { useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, MenuItem, Stack, TextField, Typography } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { useCustomers } from '../../../hooks/useCustomers'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useTaxCodes } from '../../../hooks/useTaxCodes'; +import { useCreateInvoice } from '../../../hooks/useARInvoices'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { DocumentLineEditor, blankDocumentLine, lineTotals, type DocumentLineDraft } from '../shared/DocumentLineEditor'; + +function addDays(dateStr: string, days: number): string { + const date = new Date(dateStr); + date.setDate(date.getDate() + days); + return date.toISOString().slice(0, 10); +} + +export function InvoiceFormPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const preselectedCustomerId = searchParams.get('customerId'); + + const { data: customers } = useCustomers('MAIN'); + const { data: taxCodes } = useTaxCodes(); + const createInvoice = useCreateInvoice(); + + const [customerId, setCustomerId] = useState(preselectedCustomerId ? Number(preselectedCustomerId) : ''); + const [invoiceDate, setInvoiceDate] = useState(new Date().toISOString().slice(0, 10)); + const [reference, setReference] = useState(''); + const [lines, setLines] = useState([blankDocumentLine()]); + const [error, setError] = useState(null); + + const customer = (customers ?? []).find((c) => c.id === customerId); + const { data: revenueAccounts } = useGLAccounts(customer ? customer.branchSegmentValueId : 'MAIN', 'INCOME'); + const dueDate = customer ? addDays(invoiceDate, customer.paymentTermsDays) : invoiceDate; + + const totals = useMemo(() => { + const computed = lines.map((line) => lineTotals(line, taxCodes ?? [])); + return { + subtotal: computed.reduce((sum, l) => sum + l.subtotal, 0), + tax: computed.reduce((sum, l) => sum + l.tax, 0), + total: computed.reduce((sum, l) => sum + l.total, 0), + }; + }, [lines, taxCodes]); + + const save = async (postAfter: boolean) => { + setError(null); + if (!customerId) { + setError('Select a customer'); + return; + } + if (lines.some((line) => !line.revenueGLAccountId || !line.description || !Number(line.unitPrice))) { + setError('Every line needs a description, unit price, and revenue account'); + return; + } + try { + const invoice = await createInvoice.mutateAsync({ + customerId, + invoiceDate, + dueDate, + reference: reference || undefined, + lines: lines.map((line) => ({ + description: line.description, + quantity: Number(line.quantity) || 1, + unitPrice: Number(line.unitPrice) || 0, + revenueGLAccountId: line.revenueGLAccountId as number, + taxCodeId: line.taxCodeId ? (line.taxCodeId as number) : undefined, + })), + }); + navigate(postAfter ? `/ar/invoices/${invoice.id}?post=1` : `/ar/invoices/${invoice.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to save invoice')); + } + }; + + return ( + + + New Invoice + + + + + setCustomerId(Number(e.target.value))} + sx={{ minWidth: 260 }} + > + {(customers ?? []).map((c) => ( + + {c.code} — {c.name} + + ))} + + setInvoiceDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + + setReference(e.target.value)} sx={{ minWidth: 200 }} /> + + + + + + + + + + + Subtotal + + + + + + + + Tax + + + + + + + Total + + + + + + + + + + + Notes + + + + + + Attachments + + + + + + {error && ( + + {error} + + )} + + + + + + + + ); +} diff --git a/client/src/features/ar/invoices/InvoicesListPage.tsx b/client/src/features/ar/invoices/InvoicesListPage.tsx new file mode 100644 index 0000000..f7dac9d --- /dev/null +++ b/client/src/features/ar/invoices/InvoicesListPage.tsx @@ -0,0 +1,65 @@ +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useInvoices } from '../../../hooks/useARInvoices'; +import { useBranchStore } from '../../../store/branchStore'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { ARInvoice } from '../../../types/api'; + +export function InvoicesListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: invoices, isLoading } = useInvoices(selected); + const { hasPermission } = useAuth(); + + const columns: GridColDef[] = [ + { field: 'invoiceNumber', headerName: 'Invoice #', width: 180 }, + { field: 'customer', headerName: 'Customer', flex: 1, valueGetter: (_v, row) => row.customer?.name }, + { field: 'invoiceDate', headerName: 'Date', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { field: 'dueDate', headerName: 'Due', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { + field: 'total', + headerName: 'Total', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { + field: 'balance', + headerName: 'Balance', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { field: 'status', headerName: 'Status', width: 120, renderCell: (params) => }, + ]; + + return ( + + + Invoices + {hasPermission('ar.invoice.manage') && ( + + )} + + + navigate(`/ar/invoices/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + initialState={{ sorting: { sortModel: [{ field: 'invoiceDate', sort: 'desc' }] } }} + /> + + + ); +} diff --git a/client/src/features/ar/receipts/ReceiptDetailPage.tsx b/client/src/features/ar/receipts/ReceiptDetailPage.tsx new file mode 100644 index 0000000..c162202 --- /dev/null +++ b/client/src/features/ar/receipts/ReceiptDetailPage.tsx @@ -0,0 +1,140 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, CircularProgress, Stack, Typography } from '@mui/material'; +import { useAllocateReceipt, usePostReceipt, useReceipt, useVoidReceipt } from '../../../hooks/useARReceipts'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import { AllocationPanel } from '../shared/AllocationPanel'; + +export function ReceiptDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const receiptId = id ? Number(id) : undefined; + const { data: receipt, isLoading } = useReceipt(receiptId); + const postReceipt = usePostReceipt(); + const voidReceipt = useVoidReceipt(); + const allocateReceipt = useAllocateReceipt(); + const { hasPermission } = useAuth(); + const [error, setError] = useState(null); + const autoPosted = useRef(false); + + useEffect(() => { + if (searchParams.get('post') === '1' && receipt?.status === 'DRAFT' && !autoPosted.current) { + autoPosted.current = true; + postReceipt.mutateAsync(receipt.id).catch((err) => { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post receipt'); + }); + setSearchParams({}, { replace: true }); + } + }, [receipt, postReceipt, searchParams, setSearchParams]); + + if (isLoading || !receipt) return ; + + const handlePost = async () => { + setError(null); + try { + await postReceipt.mutateAsync(receipt.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to post receipt'); + } + }; + + const handleVoid = async () => { + setError(null); + try { + await voidReceipt.mutateAsync(receipt.id); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to void receipt'); + } + }; + + return ( + + + + {receipt.receiptNumber} + + + {receipt.status === 'DRAFT' && hasPermission('ar.receipt.manage') && ( + + + + + )} + + + {error && ( + + {error} + + )} + + + + + + Customer + + navigate(`/ar/customers/${receipt.customerId}`)} + > + {receipt.customer?.name} + + + + + Date + + {receipt.receiptDate.slice(0, 10)} + + + + Deposited To + + {receipt.bankGLAccount?.accountName} + + + + Amount + + + + + + + + Unallocated + + + + + + + + + {receipt.status === 'POSTED' && ( + + + Allocate Against Invoices + + allocateReceipt.mutateAsync({ id: receipt.id, allocations })} + /> + + )} + + ); +} diff --git a/client/src/features/ar/receipts/ReceiptFormPage.tsx b/client/src/features/ar/receipts/ReceiptFormPage.tsx new file mode 100644 index 0000000..c48aa3b --- /dev/null +++ b/client/src/features/ar/receipts/ReceiptFormPage.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, Button, Card, MenuItem, Stack, TextField, Typography } from '@mui/material'; +import { useCustomers } from '../../../hooks/useCustomers'; +import { useGLAccounts } from '../../../hooks/useGLAccounts'; +import { useCreateReceipt } from '../../../hooks/useARReceipts'; + +export function ReceiptFormPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const preselectedCustomerId = searchParams.get('customerId'); + + const { data: customers } = useCustomers('MAIN'); + const createReceipt = useCreateReceipt(); + + const [customerId, setCustomerId] = useState(preselectedCustomerId ? Number(preselectedCustomerId) : ''); + const [receiptDate, setReceiptDate] = useState(new Date().toISOString().slice(0, 10)); + const [amount, setAmount] = useState(''); + const [bankGLAccountId, setBankGLAccountId] = useState(''); + const [reference, setReference] = useState(''); + const [error, setError] = useState(null); + + const customer = (customers ?? []).find((c) => c.id === customerId); + const { data: assetAccounts } = useGLAccounts(customer ? customer.branchSegmentValueId : 'MAIN', 'ASSET'); + const bankAccounts = (assetAccounts ?? []).filter((account) => account.controlType === 'BANK'); + + const save = async (postAfter: boolean) => { + setError(null); + if (!customerId || !bankGLAccountId || !Number(amount)) { + setError('Select a customer, bank account, and enter an amount'); + return; + } + try { + const receipt = await createReceipt.mutateAsync({ + customerId, + receiptDate, + amount: Number(amount), + bankGLAccountId, + reference: reference || undefined, + }); + navigate(postAfter ? `/ar/receipts/${receipt.id}?post=1` : `/ar/receipts/${receipt.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to save receipt')); + } + }; + + return ( + + + Receive Payment + + + + + setCustomerId(Number(e.target.value))}> + {(customers ?? []).map((c) => ( + + {c.code} — {c.name} + + ))} + + setReceiptDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setAmount(e.target.value)} + slotProps={{ htmlInput: { min: 0, step: '0.01' } }} + /> + setBankGLAccountId(Number(e.target.value))}> + {bankAccounts.map((account) => ( + + {account.accountCode} — {account.accountName} + + ))} + + setReference(e.target.value)} /> + + + + {error && ( + + {error} + + )} + + + + + + + + ); +} diff --git a/client/src/features/ar/receipts/ReceiptsListPage.tsx b/client/src/features/ar/receipts/ReceiptsListPage.tsx new file mode 100644 index 0000000..32aa6ab --- /dev/null +++ b/client/src/features/ar/receipts/ReceiptsListPage.tsx @@ -0,0 +1,64 @@ +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useReceipts } from '../../../hooks/useARReceipts'; +import { useBranchStore } from '../../../store/branchStore'; +import { useAuth } from '../../../hooks/useAuth'; +import { StatusChip } from '../../../components/common/StatusChip'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { ARReceipt } from '../../../types/api'; + +export function ReceiptsListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: receipts, isLoading } = useReceipts(selected); + const { hasPermission } = useAuth(); + + const columns: GridColDef[] = [ + { field: 'receiptNumber', headerName: 'Receipt #', width: 180 }, + { field: 'customer', headerName: 'Customer', flex: 1, valueGetter: (_v, row) => row.customer?.name }, + { field: 'receiptDate', headerName: 'Date', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { + field: 'amount', + headerName: 'Amount', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { + field: 'unallocatedAmount', + headerName: 'Unallocated', + width: 130, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { field: 'status', headerName: 'Status', width: 120, renderCell: (params) => }, + ]; + + return ( + + + Receipts + {hasPermission('ar.receipt.manage') && ( + + )} + + + navigate(`/ar/receipts/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + initialState={{ sorting: { sortModel: [{ field: 'receiptDate', sort: 'desc' }] } }} + /> + + + ); +} diff --git a/client/src/features/ar/shared/AllocationPanel.tsx b/client/src/features/ar/shared/AllocationPanel.tsx new file mode 100644 index 0000000..50b6725 --- /dev/null +++ b/client/src/features/ar/shared/AllocationPanel.tsx @@ -0,0 +1,126 @@ +import { useMemo, useState } from 'react'; +import { Alert, Box, Button, Stack, Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from '@mui/material'; +import { useInvoices } from '../../../hooks/useARInvoices'; +import { MoneyCell } from '../../../components/common/MoneyCell'; + +interface AllocationPanelProps { + customerId: number; + unallocatedAmount: number; + onAllocate: (allocations: { invoiceId: number; amount: number }[]) => Promise; + isSubmitting: boolean; +} + +export function AllocationPanel({ customerId, unallocatedAmount, onAllocate, isSubmitting }: AllocationPanelProps) { + const { data: invoices } = useInvoices('MAIN', customerId); + const openInvoices = (invoices ?? []).filter((invoice) => invoice.status === 'POSTED' && Number(invoice.balance) > 0); + + const [amounts, setAmounts] = useState>({}); + const [error, setError] = useState(null); + + const totalEntered = useMemo( + () => Object.values(amounts).reduce((sum, value) => sum + (Number(value) || 0), 0), + [amounts], + ); + const remaining = unallocatedAmount - totalEntered; + + if (unallocatedAmount <= 0) { + return ( + + Fully allocated. + + ); + } + + const applyFull = (invoiceId: number, balance: number) => { + const applied = Math.min(balance, unallocatedAmount - totalEntered + (Number(amounts[invoiceId]) || 0)); + setAmounts((prev) => ({ ...prev, [invoiceId]: applied > 0 ? applied.toFixed(2) : '' })); + }; + + const handleApply = async () => { + setError(null); + const allocations = Object.entries(amounts) + .map(([invoiceId, value]) => ({ invoiceId: Number(invoiceId), amount: Number(value) || 0 })) + .filter((allocation) => allocation.amount > 0); + if (allocations.length === 0) { + setError('Enter at least one allocation amount'); + return; + } + if (totalEntered > unallocatedAmount) { + setError('Total allocated exceeds the unallocated amount'); + return; + } + try { + await onAllocate(allocations); + setAmounts({}); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to allocate')); + } + }; + + if (openInvoices.length === 0) { + return ( + + This customer has no open invoices to allocate against. + + ); + } + + return ( + + + + + Invoice # + Due + Balance + + Apply Amount + + + + + {openInvoices.map((invoice) => ( + + {invoice.invoiceNumber} + {invoice.dueDate.slice(0, 10)} + + + + + + setAmounts((prev) => ({ ...prev, [invoice.id]: e.target.value }))} + sx={{ width: 100 }} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + + + ))} + +
+ + + + Remaining unallocated: + + + + + {error && ( + + {error} + + )} +
+ ); +} diff --git a/client/src/features/ar/shared/DocumentLineEditor.tsx b/client/src/features/ar/shared/DocumentLineEditor.tsx new file mode 100644 index 0000000..219b795 --- /dev/null +++ b/client/src/features/ar/shared/DocumentLineEditor.tsx @@ -0,0 +1,142 @@ +import { IconButton, MenuItem, Table, TableBody, TableCell, TableHead, TableRow, TextField } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { MoneyCell } from '../../../components/common/MoneyCell'; +import type { GLAccount, TaxCode } from '../../../types/api'; + +export interface DocumentLineDraft { + key: number; + description: string; + quantity: string; + unitPrice: string; + revenueGLAccountId: number | ''; + taxCodeId: number | ''; +} + +let keySeed = 0; +export const blankDocumentLine = (): DocumentLineDraft => ({ + key: keySeed++, + description: '', + quantity: '1', + unitPrice: '', + revenueGLAccountId: '', + taxCodeId: '', +}); + +export function lineTotals(line: DocumentLineDraft, taxCodes: TaxCode[]) { + const quantity = Number(line.quantity) || 0; + const unitPrice = Number(line.unitPrice) || 0; + const subtotal = quantity * unitPrice; + const rate = line.taxCodeId ? Number(taxCodes.find((t) => t.id === line.taxCodeId)?.ratePercent ?? 0) : 0; + const tax = subtotal * (rate / 100); + return { subtotal, tax, total: subtotal + tax }; +} + +interface DocumentLineEditorProps { + lines: DocumentLineDraft[]; + onChange: (lines: DocumentLineDraft[]) => void; + revenueAccounts: GLAccount[]; + taxCodes: TaxCode[]; +} + +export function DocumentLineEditor({ lines, onChange, revenueAccounts, taxCodes }: DocumentLineEditorProps) { + const updateLine = (key: number, patch: Partial) => + onChange(lines.map((line) => (line.key === key ? { ...line, ...patch } : line))); + + const removeLine = (key: number) => onChange(lines.length > 1 ? lines.filter((line) => line.key !== key) : lines); + + return ( + + + + Description + + Qty + + + Unit Price + + Revenue Account + Tax + + Amount + + + + + + {lines.map((line) => { + const { total } = lineTotals(line, taxCodes); + return ( + + + updateLine(line.key, { description: e.target.value })} + /> + + + updateLine(line.key, { quantity: e.target.value })} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + updateLine(line.key, { unitPrice: e.target.value })} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + updateLine(line.key, { revenueGLAccountId: Number(e.target.value) })} + > + {revenueAccounts.map((account) => ( + + {account.accountCode} — {account.accountName} + + ))} + + + + updateLine(line.key, { taxCodeId: e.target.value ? Number(e.target.value) : '' })} + > + None + {taxCodes.map((taxCode) => ( + + {taxCode.code} ({Number(taxCode.ratePercent)}%) + + ))} + + + + + + + removeLine(line.key)} disabled={lines.length <= 1}> + + + + + ); + })} + +
+ ); +} diff --git a/client/src/features/auth/HandoffPage.tsx b/client/src/features/auth/HandoffPage.tsx new file mode 100644 index 0000000..55aead3 --- /dev/null +++ b/client/src/features/auth/HandoffPage.tsx @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, CircularProgress, Typography } from '@mui/material'; +import { useAuth } from '../../hooks/useAuth'; + +/** + * Lands here after the Account Manager redirects with a one-time handoff token + * (see manager/src/modules/companies/companies.service.ts issueHandoff()). Exchanges it for a + * session in this company app and moves on — there is no local login form to fall back to. + */ +export function HandoffPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { handoff, handoffError } = useAuth(); + const attempted = useRef(false); + + useEffect(() => { + const token = searchParams.get('token'); + if (!token || attempted.current) return; + attempted.current = true; + handoff(token) + .then(() => navigate('/', { replace: true })) + .catch(() => undefined); + }, [searchParams, handoff, navigate]); + + return ( + + {!handoffError && ( + <> + + Signing you in… + + )} + {handoffError && ( + + {(handoffError as { response?: { data?: { error?: string } } })?.response?.data?.error ?? + 'Could not complete sign-in. Return to the Account Manager and try again.'} + + )} + + ); +} diff --git a/client/src/features/dashboard/DashboardPage.tsx b/client/src/features/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..8ec5d1d --- /dev/null +++ b/client/src/features/dashboard/DashboardPage.tsx @@ -0,0 +1,197 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Card, CircularProgress, Stack, Typography } from '@mui/material'; +import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined'; +import AccountBalanceOutlinedIcon from '@mui/icons-material/AccountBalanceOutlined'; +import MenuBookOutlinedIcon from '@mui/icons-material/MenuBookOutlined'; +import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined'; +import { useJournalEntries } from '../../hooks/useJournalEntries'; +import { useAuth } from '../../hooks/useAuth'; +import { StatusChip } from '../../components/common/StatusChip'; +import { MoneyCell } from '../../components/common/MoneyCell'; +import { ACCENT_BLUE } from '../../theme'; +import type { GLAccountType } from '../../types/api'; + +const TYPE_COLOR: Record = { + ASSET: '#2f6fed', + LIABILITY: '#e0631f', + EQUITY: '#7c3aed', + INCOME: '#1a9e6e', + EXPENSE: '#d1364c', +}; + +function KpiTile({ label, value, sub }: { label: string; value: string; sub?: string }) { + return ( + + + {label} + + {value} + {sub && ( + + {sub} + + )} + + ); +} + +export function DashboardPage() { + const navigate = useNavigate(); + const { hasPermission } = useAuth(); + const { data: entries, isLoading } = useJournalEntries('MAIN'); + + const stats = useMemo(() => { + const all = entries ?? []; + const posted = all.filter((e) => e.status === 'POSTED'); + const draft = all.filter((e) => e.status === 'DRAFT'); + const totalPostedValue = posted.reduce((sum, e) => sum + Number(e.totalDebits), 0); + const accountIds = new Set(); + const movementByType = new Map(); + for (const entry of posted) { + for (const line of entry.lines) { + accountIds.add(line.glAccountId); + const net = Number(line.debit) - Number(line.credit); + const signed = line.glAccount.normalBalance === 'DEBIT' ? net : -net; + movementByType.set(line.glAccount.accountType, (movementByType.get(line.glAccount.accountType) ?? 0) + Math.abs(signed)); + } + } + const recent = [...all].sort((a, b) => b.entryDate.localeCompare(a.entryDate)).slice(0, 8); + const maxMovement = Math.max(1, ...Array.from(movementByType.values())); + return { posted, draft, totalPostedValue, accountsInUse: accountIds.size, movementByType, recent, maxMovement }; + }, [entries]); + + if (isLoading) return ; + + const quickActions = [ + { label: 'New Manual Entry', path: '/gl/journal-entries/new', icon: , permission: 'gl.journal.post' }, + { label: 'Chart of Accounts', path: '/admin/chart-of-accounts', icon: , permission: 'gl.account.manage' }, + { label: 'Journals', path: '/admin/journals', icon: , permission: 'gl.journal.manage' }, + { label: 'Users & Roles', path: '/admin/users', icon: , permission: 'admin.users.manage' }, + ].filter((action) => !action.permission || hasPermission(action.permission)); + + return ( + + + Dashboard + + + Overview of general ledger activity across all branches. + + + + + + + + + + + + + Posted Movement by Account Type + + {stats.movementByType.size === 0 ? ( + + No posted entries yet. + + ) : ( + + {(Object.keys(TYPE_COLOR) as GLAccountType[]) + .filter((type) => stats.movementByType.has(type)) + .map((type) => { + const value = stats.movementByType.get(type) ?? 0; + const pct = (value / stats.maxMovement) * 100; + return ( + + + {type} + + + + + + + + + ); + })} + + )} + + + + + Quick Actions + + + {quickActions.map((action) => ( + navigate(action.path)} + sx={{ + alignItems: 'center', + p: 1, + borderRadius: 2, + cursor: 'pointer', + color: ACCENT_BLUE, + '&:hover': { bgcolor: '#f0f4ff' }, + }} + > + {action.icon} + + {action.label} + + + ))} + + + + + + + Recent Activity + + {stats.recent.length === 0 ? ( + + No journal entries yet. + + ) : ( + }> + {stats.recent.map((entry) => ( + navigate(`/gl/journal-entries/${entry.id}`)} + > + + + {entry.entryDate.slice(0, 10)} + + + + {entry.entryNumber} + + + {entry.description} + + + + + + + ))} + + )} + + + ); +} diff --git a/client/src/features/gl/GLAccountLedgerView.tsx b/client/src/features/gl/GLAccountLedgerView.tsx new file mode 100644 index 0000000..86f54a5 --- /dev/null +++ b/client/src/features/gl/GLAccountLedgerView.tsx @@ -0,0 +1,129 @@ +import { useMemo, useState } from 'react'; +import { Box, Card, MenuItem, Stack, Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from '@mui/material'; +import { useJournalEntries } from '../../hooks/useJournalEntries'; +import { useGLAccounts } from '../../hooks/useGLAccounts'; +import { useBranchStore } from '../../store/branchStore'; +import { MoneyCell } from '../../components/common/MoneyCell'; + +/** Account-centric drill-down, computed client-side from posted entries already in cache — no new endpoint. */ +export function GLAccountLedgerView() { + const selected = useBranchStore((state) => state.selected); + const { data: accounts } = useGLAccounts(selected); + const { data: entries, isLoading } = useJournalEntries(selected); + + const [accountId, setAccountId] = useState(''); + const [fromDate, setFromDate] = useState(''); + const [toDate, setToDate] = useState(''); + + const account = (accounts ?? []).find((a) => a.id === accountId); + + const rows = useMemo(() => { + if (!accountId) return []; + let running = 0; + return (entries ?? []) + .filter((entry) => entry.status === 'POSTED') + .flatMap((entry) => + entry.lines + .filter((line) => line.glAccountId === accountId) + .map((line) => ({ entry, line })), + ) + .filter(({ entry }) => (!fromDate || entry.entryDate.slice(0, 10) >= fromDate) && (!toDate || entry.entryDate.slice(0, 10) <= toDate)) + .sort((a, b) => a.entry.entryDate.localeCompare(b.entry.entryDate)) + .map(({ entry, line }) => { + const movement = account?.normalBalance === 'DEBIT' ? Number(line.debit) - Number(line.credit) : Number(line.credit) - Number(line.debit); + running += movement; + return { entry, line, running }; + }); + }, [entries, accountId, fromDate, toDate, account?.normalBalance]); + + return ( + + + + setAccountId(e.target.value ? Number(e.target.value) : '')} + sx={{ minWidth: 280 }} + > + {(accounts ?? []) + .filter((a) => a.isPostable) + .map((a) => ( + + {a.accountCode} — {a.accountName} + + ))} + + setFromDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setToDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + + + + {!accountId ? ( + + Select an account to view its posted transaction history and running balance. + + ) : ( + + + + + Date + Reference + Description + Debit + Credit + Balance + + + + {!isLoading && rows.length === 0 && ( + + + + No posted transactions for this account in the selected range. + + + + )} + {rows.map(({ entry, line, running }) => ( + + {entry.entryDate.slice(0, 10)} + {entry.entryNumber} + {line.description || entry.description} + + + + + + + + + + + + + ))} + +
+
+ )} +
+ ); +} diff --git a/client/src/features/gl/JournalEntryDetailPage.tsx b/client/src/features/gl/JournalEntryDetailPage.tsx new file mode 100644 index 0000000..4decef9 --- /dev/null +++ b/client/src/features/gl/JournalEntryDetailPage.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Alert, + Box, + Button, + CircularProgress, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useJournalEntry, useReverseJournalEntry } from '../../hooks/useJournalEntries'; +import { StatusChip } from '../../components/common/StatusChip'; +import { MoneyCell } from '../../components/common/MoneyCell'; +import { ConfirmDialog } from '../../components/common/ConfirmDialog'; +import { useAuth } from '../../hooks/useAuth'; + +export function JournalEntryDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const { hasPermission } = useAuth(); + const { data: entry, isLoading } = useJournalEntry(id ? Number(id) : undefined); + const reverseEntry = useReverseJournalEntry(); + const [confirmOpen, setConfirmOpen] = useState(false); + const [error, setError] = useState(null); + + if (isLoading || !entry) return ; + + const handleReverse = async () => { + try { + const reversal = await reverseEntry.mutateAsync({ id: entry.id }); + setConfirmOpen(false); + navigate(`/gl/journal-entries/${reversal.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? 'Failed to reverse entry'); + setConfirmOpen(false); + } + }; + + return ( + + + {entry.entryNumber} + + + {entry.status === 'POSTED' && hasPermission('gl.journal.post') && ( + + )} + + + + {error && ( + + {error} + + )} + + + + + + Journal + + {entry.journal?.code} + + + + Date + + {entry.entryDate.slice(0, 10)} + + + + Description + + {entry.description} + + + + + + + + + GL Account + Description + Debit + Credit + + + + {entry.lines.map((line) => ( + + + {line.glAccount.accountCode} — {line.glAccount.accountName} + + {line.description} + + + + + + + + ))} + + + Total + + + + + + + + + + + + + +
+
+ + setConfirmOpen(false)} + loading={reverseEntry.isPending} + /> +
+ ); +} diff --git a/client/src/features/gl/JournalEntryFormPage.tsx b/client/src/features/gl/JournalEntryFormPage.tsx new file mode 100644 index 0000000..187d1d2 --- /dev/null +++ b/client/src/features/gl/JournalEntryFormPage.tsx @@ -0,0 +1,255 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Alert, + Box, + Button, + IconButton, + MenuItem, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { useJournals } from '../../hooks/useJournals'; +import { useGLAccounts } from '../../hooks/useGLAccounts'; +import { useSegmentDefinitions } from '../../hooks/useSegments'; +import { useCreateManualEntry } from '../../hooks/useJournalEntries'; + +interface LineDraft { + key: number; + glAccountId: number | ''; + debit: string; + credit: string; + description: string; + branchSegmentValueId: number | ''; +} + +let keySeed = 0; +const blankLine = (): LineDraft => ({ key: keySeed++, glAccountId: '', debit: '', credit: '', description: '', branchSegmentValueId: '' }); + +export function JournalEntryFormPage() { + const navigate = useNavigate(); + const { data: journals } = useJournals(); + const { data: accounts } = useGLAccounts('MAIN'); + const { data: definitions } = useSegmentDefinitions(); + const branchDef = (definitions ?? []).find((def) => def.isBranchSegment); + const createEntry = useCreateManualEntry(); + + const [journalCode, setJournalCode] = useState('GJ'); + const [entryDate, setEntryDate] = useState(new Date().toISOString().slice(0, 10)); + const [description, setDescription] = useState(''); + const [lines, setLines] = useState([blankLine(), blankLine()]); + const [error, setError] = useState(null); + + const totalDebits = lines.reduce((sum, line) => sum + (Number(line.debit) || 0), 0); + const totalCredits = lines.reduce((sum, line) => sum + (Number(line.credit) || 0), 0); + const isBalanced = totalDebits > 0 && totalDebits === totalCredits; + + const updateLine = (key: number, patch: Partial) => + setLines((prev) => prev.map((line) => (line.key === key ? { ...line, ...patch } : line))); + + const removeLine = (key: number) => setLines((prev) => (prev.length > 2 ? prev.filter((line) => line.key !== key) : prev)); + + const handleSubmit = async () => { + setError(null); + if (!isBalanced) { + setError('Debits and credits must balance before this entry can be posted'); + return; + } + if (lines.some((line) => !line.glAccountId || (Number(line.debit) === 0 && Number(line.credit) === 0))) { + setError('Every line needs a GL account and a debit or credit amount'); + return; + } + try { + const entry = await createEntry.mutateAsync({ + journalCode, + entryDate, + description, + lines: lines.map((line) => ({ + glAccountId: line.glAccountId as number, + debit: Number(line.debit) || 0, + credit: Number(line.credit) || 0, + description: line.description || undefined, + branchSegmentValueId: line.branchSegmentValueId ? (line.branchSegmentValueId as number) : undefined, + })), + }); + navigate(`/gl/journal-entries/${entry.id}`); + } catch (err) { + const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; + setError(message ?? (err instanceof Error ? err.message : 'Failed to post entry')); + } + }; + + return ( + + + New Manual Journal Entry + + + + + setJournalCode(e.target.value)} sx={{ minWidth: 200 }}> + {(journals ?? []).map((journal) => ( + + {journal.code} — {journal.name} + + ))} + + setEntryDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setDescription(e.target.value)} fullWidth /> + + + + + + + + GL Account + Line description + Branch + + Debit + + + Credit + + + + + + {lines.map((line) => ( + + + updateLine(line.key, { glAccountId: Number(e.target.value) })} + > + {(accounts ?? []).map((account) => ( + + {account.accountCode} — {account.accountName} + + ))} + + + + updateLine(line.key, { description: e.target.value })} + /> + + + {branchDef && ( + updateLine(line.key, { branchSegmentValueId: e.target.value ? Number(e.target.value) : '' })} + > + — + {branchDef.values.map((value) => ( + + {value.code} + + ))} + + )} + + + updateLine(line.key, { debit: e.target.value, credit: e.target.value ? '' : line.credit })} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + updateLine(line.key, { credit: e.target.value, debit: e.target.value ? '' : line.debit })} + slotProps={{ htmlInput: { min: 0, step: '0.01', style: { textAlign: 'right' } } }} + /> + + + removeLine(line.key)} disabled={lines.length <= 2}> + + + + + ))} + +
+ + + + + + Total debits: {totalDebits.toFixed(2)} + + + Total credits: {totalCredits.toFixed(2)} + + + {isBalanced ? 'Balanced ✓' : 'Not balanced'} + + +
+ + {error && ( + + {error} + + )} + + + + + Attachments + + + + + + Notes + + + + + + + + + +
+ ); +} diff --git a/client/src/features/gl/JournalEntryListPage.tsx b/client/src/features/gl/JournalEntryListPage.tsx new file mode 100644 index 0000000..887a964 --- /dev/null +++ b/client/src/features/gl/JournalEntryListPage.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Card, Stack, Tab, Tabs, Typography } from '@mui/material'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; +import { useJournalEntries } from '../../hooks/useJournalEntries'; +import { useBranchStore } from '../../store/branchStore'; +import { StatusChip } from '../../components/common/StatusChip'; +import { MoneyCell } from '../../components/common/MoneyCell'; +import { useAuth } from '../../hooks/useAuth'; +import { GLAccountLedgerView } from './GLAccountLedgerView'; +import type { JournalEntry } from '../../types/api'; + +export function JournalEntryListPage() { + const navigate = useNavigate(); + const selected = useBranchStore((state) => state.selected); + const { data: entries, isLoading } = useJournalEntries(selected); + const { hasPermission } = useAuth(); + const [tab, setTab] = useState(0); + + const columns: GridColDef[] = [ + { field: 'entryNumber', headerName: 'Entry #', width: 180 }, + { field: 'entryDate', headerName: 'Date', width: 120, valueFormatter: (value: string) => value?.slice(0, 10) }, + { field: 'description', headerName: 'Description', flex: 1 }, + { field: 'journal', headerName: 'Journal', width: 140, valueGetter: (_v, row) => row.journal?.code }, + { + field: 'totalDebits', + headerName: 'Debits', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { + field: 'totalCredits', + headerName: 'Credits', + width: 120, + align: 'right', + headerAlign: 'right', + renderCell: (params) => , + }, + { field: 'status', headerName: 'Status', width: 120, renderCell: (params) => }, + ]; + + return ( + + + General Ledger + {hasPermission('gl.journal.post') && ( + + )} + + + setTab(value)} sx={{ mb: 2 }}> + + + + + {tab === 0 && ( + + navigate(`/gl/journal-entries/${params.row.id}`)} + sx={{ border: 'none', '& .MuiDataGrid-row': { cursor: 'pointer' } }} + initialState={{ pagination: { paginationModel: { pageSize: 25 } }, sorting: { sortModel: [{ field: 'entryDate', sort: 'desc' }] } }} + /> + + )} + + {tab === 1 && } + + ); +} diff --git a/client/src/features/reports/ReportsPage.tsx b/client/src/features/reports/ReportsPage.tsx new file mode 100644 index 0000000..5822ac5 --- /dev/null +++ b/client/src/features/reports/ReportsPage.tsx @@ -0,0 +1,88 @@ +import { Box, Card, Chip, Stack, Typography } from '@mui/material'; +import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined'; + +interface ReportDef { + name: string; + description: string; +} + +const CATEGORIES: { title: string; reports: ReportDef[] }[] = [ + { + title: 'Financial', + reports: [ + { name: 'Trial Balance', description: 'Debit and credit balances for every account, Main or per-branch.' }, + { name: 'Profit & Loss Statement', description: 'Income and expense performance for a selected period.' }, + { name: 'Balance Sheet', description: 'Assets, liabilities, and equity as at a selected date.' }, + { name: 'General Ledger', description: 'Full transaction detail for any account and date range.' }, + ], + }, + { + title: 'Receivables', + reports: [ + { name: 'Aged Receivables', description: 'Outstanding customer balances by age bucket.' }, + { name: 'Customer Statement', description: 'Transaction history and running balance per customer.' }, + ], + }, + { + title: 'Payables', + reports: [ + { name: 'Aged Payables', description: 'Outstanding supplier balances by age bucket.' }, + { name: 'Payment Run', description: 'Supplier invoices selected for the next payment batch.' }, + ], + }, + { + title: 'Management', + reports: [ + { name: 'Budget vs Actual', description: 'Budgeted figures compared against actual GL movement.' }, + { name: 'Branch Performance', description: 'Key metrics compared across branches.' }, + { name: 'Stock Valuation', description: 'On-hand stock quantities and value by warehouse.' }, + ], + }, +]; + +export function ReportsPage() { + return ( + + + Reports Centre + + + Financial and management reports. Reports become available as each module is built. + + + + {CATEGORIES.map((category) => ( + + + {category.title} + + + {category.reports.map((report) => ( + + + + + {report.name} + + + + {report.description} + + + + ))} + + + ))} + + + ); +} diff --git a/client/src/hooks/useAPDebitNotes.ts b/client/src/hooks/useAPDebitNotes.ts new file mode 100644 index 0000000..351ba82 --- /dev/null +++ b/client/src/hooks/useAPDebitNotes.ts @@ -0,0 +1,55 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + allocateDebitNote, + createDebitNote, + fetchDebitNote, + fetchDebitNotes, + postDebitNote, + voidDebitNote, +} from '../api/apDebitNotes.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useDebitNotes(branchScope: BranchSelection, supplierId?: number) { + return useQuery({ queryKey: ['apDebitNotes', branchScope, supplierId], queryFn: () => fetchDebitNotes(branchScope, supplierId) }); +} + +export function useDebitNote(id: number | undefined) { + return useQuery({ + queryKey: ['apDebitNote', id], + queryFn: () => fetchDebitNote(id as number), + enabled: id !== undefined, + }); +} + +function invalidateDebitNoteQueries(queryClient: ReturnType) { + queryClient.invalidateQueries({ queryKey: ['apDebitNotes'] }); + queryClient.invalidateQueries({ queryKey: ['apDebitNote'] }); + queryClient.invalidateQueries({ queryKey: ['apInvoice'] }); + queryClient.invalidateQueries({ queryKey: ['apInvoices'] }); + queryClient.invalidateQueries({ queryKey: ['supplier'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); +} + +export function useCreateDebitNote() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: createDebitNote, onSuccess: () => invalidateDebitNoteQueries(queryClient) }); +} + +export function usePostDebitNote() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: postDebitNote, onSuccess: () => invalidateDebitNoteQueries(queryClient) }); +} + +export function useVoidDebitNote() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: voidDebitNote, onSuccess: () => invalidateDebitNoteQueries(queryClient) }); +} + +export function useAllocateDebitNote() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, allocations }: { id: number; allocations: { invoiceId: number; amount: number }[] }) => + allocateDebitNote(id, allocations), + onSuccess: () => invalidateDebitNoteQueries(queryClient), + }); +} diff --git a/client/src/hooks/useAPInvoices.ts b/client/src/hooks/useAPInvoices.ts new file mode 100644 index 0000000..564e572 --- /dev/null +++ b/client/src/hooks/useAPInvoices.ts @@ -0,0 +1,37 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createInvoice, fetchInvoice, fetchInvoices, postInvoice, voidInvoice } from '../api/apInvoices.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useInvoices(branchScope: BranchSelection, supplierId?: number) { + return useQuery({ queryKey: ['apInvoices', branchScope, supplierId], queryFn: () => fetchInvoices(branchScope, supplierId) }); +} + +export function useInvoice(id: number | undefined) { + return useQuery({ + queryKey: ['apInvoice', id], + queryFn: () => fetchInvoice(id as number), + enabled: id !== undefined, + }); +} + +function invalidateInvoiceQueries(queryClient: ReturnType) { + queryClient.invalidateQueries({ queryKey: ['apInvoices'] }); + queryClient.invalidateQueries({ queryKey: ['apInvoice'] }); + queryClient.invalidateQueries({ queryKey: ['supplier'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); +} + +export function useCreateInvoice() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: createInvoice, onSuccess: () => invalidateInvoiceQueries(queryClient) }); +} + +export function usePostInvoice() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: postInvoice, onSuccess: () => invalidateInvoiceQueries(queryClient) }); +} + +export function useVoidInvoice() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: voidInvoice, onSuccess: () => invalidateInvoiceQueries(queryClient) }); +} diff --git a/client/src/hooks/useAPPayments.ts b/client/src/hooks/useAPPayments.ts new file mode 100644 index 0000000..4768e77 --- /dev/null +++ b/client/src/hooks/useAPPayments.ts @@ -0,0 +1,48 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { allocatePayment, createPayment, fetchPayment, fetchPayments, postPayment, voidPayment } from '../api/apPayments.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function usePayments(branchScope: BranchSelection, supplierId?: number) { + return useQuery({ queryKey: ['apPayments', branchScope, supplierId], queryFn: () => fetchPayments(branchScope, supplierId) }); +} + +export function usePayment(id: number | undefined) { + return useQuery({ + queryKey: ['apPayment', id], + queryFn: () => fetchPayment(id as number), + enabled: id !== undefined, + }); +} + +function invalidatePaymentQueries(queryClient: ReturnType) { + queryClient.invalidateQueries({ queryKey: ['apPayments'] }); + queryClient.invalidateQueries({ queryKey: ['apPayment'] }); + queryClient.invalidateQueries({ queryKey: ['apInvoice'] }); + queryClient.invalidateQueries({ queryKey: ['apInvoices'] }); + queryClient.invalidateQueries({ queryKey: ['supplier'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); +} + +export function useCreatePayment() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: createPayment, onSuccess: () => invalidatePaymentQueries(queryClient) }); +} + +export function usePostPayment() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: postPayment, onSuccess: () => invalidatePaymentQueries(queryClient) }); +} + +export function useVoidPayment() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: voidPayment, onSuccess: () => invalidatePaymentQueries(queryClient) }); +} + +export function useAllocatePayment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, allocations }: { id: number; allocations: { invoiceId: number; amount: number }[] }) => + allocatePayment(id, allocations), + onSuccess: () => invalidatePaymentQueries(queryClient), + }); +} diff --git a/client/src/hooks/useARCreditNotes.ts b/client/src/hooks/useARCreditNotes.ts new file mode 100644 index 0000000..ca5be4a --- /dev/null +++ b/client/src/hooks/useARCreditNotes.ts @@ -0,0 +1,55 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + allocateCreditNote, + createCreditNote, + fetchCreditNote, + fetchCreditNotes, + postCreditNote, + voidCreditNote, +} from '../api/arCreditNotes.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useCreditNotes(branchScope: BranchSelection, customerId?: number) { + return useQuery({ queryKey: ['arCreditNotes', branchScope, customerId], queryFn: () => fetchCreditNotes(branchScope, customerId) }); +} + +export function useCreditNote(id: number | undefined) { + return useQuery({ + queryKey: ['arCreditNote', id], + queryFn: () => fetchCreditNote(id as number), + enabled: id !== undefined, + }); +} + +function invalidateCreditNoteQueries(queryClient: ReturnType) { + queryClient.invalidateQueries({ queryKey: ['arCreditNotes'] }); + queryClient.invalidateQueries({ queryKey: ['arCreditNote'] }); + queryClient.invalidateQueries({ queryKey: ['arInvoice'] }); + queryClient.invalidateQueries({ queryKey: ['arInvoices'] }); + queryClient.invalidateQueries({ queryKey: ['customer'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); +} + +export function useCreateCreditNote() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: createCreditNote, onSuccess: () => invalidateCreditNoteQueries(queryClient) }); +} + +export function usePostCreditNote() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: postCreditNote, onSuccess: () => invalidateCreditNoteQueries(queryClient) }); +} + +export function useVoidCreditNote() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: voidCreditNote, onSuccess: () => invalidateCreditNoteQueries(queryClient) }); +} + +export function useAllocateCreditNote() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, allocations }: { id: number; allocations: { invoiceId: number; amount: number }[] }) => + allocateCreditNote(id, allocations), + onSuccess: () => invalidateCreditNoteQueries(queryClient), + }); +} diff --git a/client/src/hooks/useARInvoices.ts b/client/src/hooks/useARInvoices.ts new file mode 100644 index 0000000..474bb9e --- /dev/null +++ b/client/src/hooks/useARInvoices.ts @@ -0,0 +1,37 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createInvoice, fetchInvoice, fetchInvoices, postInvoice, voidInvoice } from '../api/arInvoices.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useInvoices(branchScope: BranchSelection, customerId?: number) { + return useQuery({ queryKey: ['arInvoices', branchScope, customerId], queryFn: () => fetchInvoices(branchScope, customerId) }); +} + +export function useInvoice(id: number | undefined) { + return useQuery({ + queryKey: ['arInvoice', id], + queryFn: () => fetchInvoice(id as number), + enabled: id !== undefined, + }); +} + +function invalidateInvoiceQueries(queryClient: ReturnType) { + queryClient.invalidateQueries({ queryKey: ['arInvoices'] }); + queryClient.invalidateQueries({ queryKey: ['arInvoice'] }); + queryClient.invalidateQueries({ queryKey: ['customer'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); +} + +export function useCreateInvoice() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: createInvoice, onSuccess: () => invalidateInvoiceQueries(queryClient) }); +} + +export function usePostInvoice() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: postInvoice, onSuccess: () => invalidateInvoiceQueries(queryClient) }); +} + +export function useVoidInvoice() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: voidInvoice, onSuccess: () => invalidateInvoiceQueries(queryClient) }); +} diff --git a/client/src/hooks/useARReceipts.ts b/client/src/hooks/useARReceipts.ts new file mode 100644 index 0000000..0958af6 --- /dev/null +++ b/client/src/hooks/useARReceipts.ts @@ -0,0 +1,48 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { allocateReceipt, createReceipt, fetchReceipt, fetchReceipts, postReceipt, voidReceipt } from '../api/arReceipts.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useReceipts(branchScope: BranchSelection, customerId?: number) { + return useQuery({ queryKey: ['arReceipts', branchScope, customerId], queryFn: () => fetchReceipts(branchScope, customerId) }); +} + +export function useReceipt(id: number | undefined) { + return useQuery({ + queryKey: ['arReceipt', id], + queryFn: () => fetchReceipt(id as number), + enabled: id !== undefined, + }); +} + +function invalidateReceiptQueries(queryClient: ReturnType) { + queryClient.invalidateQueries({ queryKey: ['arReceipts'] }); + queryClient.invalidateQueries({ queryKey: ['arReceipt'] }); + queryClient.invalidateQueries({ queryKey: ['arInvoice'] }); + queryClient.invalidateQueries({ queryKey: ['arInvoices'] }); + queryClient.invalidateQueries({ queryKey: ['customer'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); +} + +export function useCreateReceipt() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: createReceipt, onSuccess: () => invalidateReceiptQueries(queryClient) }); +} + +export function usePostReceipt() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: postReceipt, onSuccess: () => invalidateReceiptQueries(queryClient) }); +} + +export function useVoidReceipt() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: voidReceipt, onSuccess: () => invalidateReceiptQueries(queryClient) }); +} + +export function useAllocateReceipt() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, allocations }: { id: number; allocations: { invoiceId: number; amount: number }[] }) => + allocateReceipt(id, allocations), + onSuccess: () => invalidateReceiptQueries(queryClient), + }); +} diff --git a/client/src/hooks/useAuth.ts b/client/src/hooks/useAuth.ts new file mode 100644 index 0000000..f20d63b --- /dev/null +++ b/client/src/hooks/useAuth.ts @@ -0,0 +1,22 @@ +import { useMutation } from '@tanstack/react-query'; +import { handoff as handoffRequest } from '../api/auth.api'; +import { useAuthStore } from '../store/authStore'; + +export function useAuth() { + const { user, accessToken, setSession, clearSession, hasPermission } = useAuthStore(); + + const handoffMutation = useMutation({ + mutationFn: (token: string) => handoffRequest(token), + onSuccess: (session) => setSession(session), + }); + + return { + user, + isAuthenticated: Boolean(accessToken && user), + handoff: handoffMutation.mutateAsync, + isHandingOff: handoffMutation.isPending, + handoffError: handoffMutation.error, + logout: clearSession, + hasPermission, + }; +} diff --git a/client/src/hooks/useBranchScope.ts b/client/src/hooks/useBranchScope.ts new file mode 100644 index 0000000..20ef0a8 --- /dev/null +++ b/client/src/hooks/useBranchScope.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchBranchValues } from '../api/segments.api'; +import { useAuthStore } from '../store/authStore'; +import { useBranchStore } from '../store/branchStore'; + +export function useBranchScope() { + const { selected, setSelected } = useBranchStore(); + const allowed = useAuthStore((state) => state.user?.allowedBranchSegmentValueIds ?? null); + + const branchesQuery = useQuery({ queryKey: ['branches'], queryFn: fetchBranchValues }); + const allBranches = branchesQuery.data ?? []; + const visibleBranches = allowed === null ? allBranches : allBranches.filter((branch) => allowed.includes(branch.id)); + const canViewMain = allowed === null; + + return { + selected, + setSelected, + branches: visibleBranches, + canViewMain, + isLoading: branchesQuery.isLoading, + }; +} diff --git a/client/src/hooks/useCustomers.ts b/client/src/hooks/useCustomers.ts new file mode 100644 index 0000000..e889c89 --- /dev/null +++ b/client/src/hooks/useCustomers.ts @@ -0,0 +1,34 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createCustomer, fetchCustomer, fetchCustomers, updateCustomer, type CustomerInput } from '../api/customers.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useCustomers(branchScope: BranchSelection) { + return useQuery({ queryKey: ['customers', branchScope], queryFn: () => fetchCustomers(branchScope) }); +} + +export function useCustomer(id: number | undefined) { + return useQuery({ + queryKey: ['customer', id], + queryFn: () => fetchCustomer(id as number), + enabled: id !== undefined, + }); +} + +export function useCreateCustomer() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createCustomer, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customers'] }), + }); +} + +export function useUpdateCustomer() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, input }: { id: number; input: Partial & { isActive?: boolean } }) => updateCustomer(id, input), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ['customers'] }); + queryClient.invalidateQueries({ queryKey: ['customer', variables.id] }); + }, + }); +} diff --git a/client/src/hooks/useFiscalYears.ts b/client/src/hooks/useFiscalYears.ts new file mode 100644 index 0000000..8472a85 --- /dev/null +++ b/client/src/hooks/useFiscalYears.ts @@ -0,0 +1,14 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createFiscalYear, fetchFiscalYears } from '../api/fiscalPeriods.api'; + +export function useFiscalYears() { + return useQuery({ queryKey: ['fiscalYears'], queryFn: fetchFiscalYears }); +} + +export function useCreateFiscalYear() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createFiscalYear, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['fiscalYears'] }), + }); +} diff --git a/client/src/hooks/useGLAccounts.ts b/client/src/hooks/useGLAccounts.ts new file mode 100644 index 0000000..f555168 --- /dev/null +++ b/client/src/hooks/useGLAccounts.ts @@ -0,0 +1,19 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createGLAccount, fetchGLAccounts } from '../api/glAccounts.api'; +import type { BranchSelection } from '../store/branchStore'; +import type { GLAccountType } from '../types/api'; + +export function useGLAccounts(branchScope: BranchSelection, accountType?: GLAccountType) { + return useQuery({ + queryKey: ['glAccounts', branchScope, accountType], + queryFn: () => fetchGLAccounts(branchScope, accountType), + }); +} + +export function useCreateGLAccount() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createGLAccount, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['glAccounts'] }), + }); +} diff --git a/client/src/hooks/useJournalEntries.ts b/client/src/hooks/useJournalEntries.ts new file mode 100644 index 0000000..9f4d6fe --- /dev/null +++ b/client/src/hooks/useJournalEntries.ts @@ -0,0 +1,37 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createManualEntry, fetchJournalEntries, fetchJournalEntry, reverseJournalEntry } from '../api/journalEntries.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useJournalEntries(branchScope: BranchSelection) { + return useQuery({ queryKey: ['journalEntries', branchScope], queryFn: () => fetchJournalEntries(branchScope) }); +} + +export function useJournalEntry(id: number | undefined) { + return useQuery({ + queryKey: ['journalEntry', id], + queryFn: () => fetchJournalEntry(id as number), + enabled: id !== undefined, + }); +} + +export function useCreateManualEntry() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createManualEntry, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['journalEntries'] }); + queryClient.invalidateQueries({ queryKey: ['glAccounts'] }); + }, + }); +} + +export function useReverseJournalEntry() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, reason }: { id: number; reason?: string }) => reverseJournalEntry(id, reason), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['journalEntries'] }); + queryClient.invalidateQueries({ queryKey: ['journalEntry'] }); + }, + }); +} diff --git a/client/src/hooks/useJournals.ts b/client/src/hooks/useJournals.ts new file mode 100644 index 0000000..384af9d --- /dev/null +++ b/client/src/hooks/useJournals.ts @@ -0,0 +1,14 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createJournal, fetchJournals } from '../api/journals.api'; + +export function useJournals() { + return useQuery({ queryKey: ['journals'], queryFn: fetchJournals }); +} + +export function useCreateJournal() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createJournal, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['journals'] }), + }); +} diff --git a/client/src/hooks/useRoles.ts b/client/src/hooks/useRoles.ts new file mode 100644 index 0000000..54c82f9 --- /dev/null +++ b/client/src/hooks/useRoles.ts @@ -0,0 +1,19 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { fetchPermissions, fetchRoles, setRolePermissions } from '../api/roles.api'; + +export function useRoles() { + return useQuery({ queryKey: ['roles'], queryFn: fetchRoles }); +} + +export function usePermissions() { + return useQuery({ queryKey: ['permissions'], queryFn: fetchPermissions }); +} + +export function useSetRolePermissions() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ roleId, permissionIds }: { roleId: number; permissionIds: number[] }) => + setRolePermissions(roleId, permissionIds), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roles'] }), + }); +} diff --git a/client/src/hooks/useSegments.ts b/client/src/hooks/useSegments.ts new file mode 100644 index 0000000..3d0fe3c --- /dev/null +++ b/client/src/hooks/useSegments.ts @@ -0,0 +1,29 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + createSegmentDefinition, + createSegmentValue, + fetchSegmentDefinitions, +} from '../api/segments.api'; + +export function useSegmentDefinitions() { + return useQuery({ queryKey: ['segmentDefinitions'], queryFn: fetchSegmentDefinitions }); +} + +export function useCreateSegmentDefinition() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createSegmentDefinition, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['segmentDefinitions'] }), + }); +} + +export function useCreateSegmentValue() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createSegmentValue, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['segmentDefinitions'] }); + queryClient.invalidateQueries({ queryKey: ['branches'] }); + }, + }); +} diff --git a/client/src/hooks/useSuppliers.ts b/client/src/hooks/useSuppliers.ts new file mode 100644 index 0000000..862a553 --- /dev/null +++ b/client/src/hooks/useSuppliers.ts @@ -0,0 +1,34 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createSupplier, fetchSupplier, fetchSuppliers, updateSupplier, type SupplierInput } from '../api/suppliers.api'; +import type { BranchSelection } from '../store/branchStore'; + +export function useSuppliers(branchScope: BranchSelection) { + return useQuery({ queryKey: ['suppliers', branchScope], queryFn: () => fetchSuppliers(branchScope) }); +} + +export function useSupplier(id: number | undefined) { + return useQuery({ + queryKey: ['supplier', id], + queryFn: () => fetchSupplier(id as number), + enabled: id !== undefined, + }); +} + +export function useCreateSupplier() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createSupplier, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['suppliers'] }), + }); +} + +export function useUpdateSupplier() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, input }: { id: number; input: Partial & { isActive?: boolean } }) => updateSupplier(id, input), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ['suppliers'] }); + queryClient.invalidateQueries({ queryKey: ['supplier', variables.id] }); + }, + }); +} diff --git a/client/src/hooks/useTaxCodes.ts b/client/src/hooks/useTaxCodes.ts new file mode 100644 index 0000000..9edfee2 --- /dev/null +++ b/client/src/hooks/useTaxCodes.ts @@ -0,0 +1,22 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createTaxCode, fetchTaxCodes, updateTaxCode, type TaxCodeInput } from '../api/taxCodes.api'; + +export function useTaxCodes() { + return useQuery({ queryKey: ['taxCodes'], queryFn: fetchTaxCodes }); +} + +export function useCreateTaxCode() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createTaxCode, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['taxCodes'] }), + }); +} + +export function useUpdateTaxCode() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, input }: { id: number; input: Partial }) => updateTaxCode(id, input), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['taxCodes'] }), + }); +} diff --git a/client/src/hooks/useUsers.ts b/client/src/hooks/useUsers.ts new file mode 100644 index 0000000..c9281ed --- /dev/null +++ b/client/src/hooks/useUsers.ts @@ -0,0 +1,22 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createUser, fetchUsers, setUserActive } from '../api/users.api'; + +export function useUsers() { + return useQuery({ queryKey: ['users'], queryFn: fetchUsers }); +} + +export function useCreateUser() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createUser, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + }); +} + +export function useSetUserActive() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, isActive }: { id: number; isActive: boolean }) => setUserActive(id, isActive), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + }); +} diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..e4a5833 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,7 @@ +html, body, #root { + height: 100%; +} + +body { + margin: 0; +} diff --git a/client/src/layouts/MainLayout.tsx b/client/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..ac58548 --- /dev/null +++ b/client/src/layouts/MainLayout.tsx @@ -0,0 +1,243 @@ +import { useState, type ReactNode } from 'react'; +import { Link as RouterLink, Outlet, useLocation } from 'react-router-dom'; +import { + AppBar, + Avatar, + Box, + Divider, + Drawer, + IconButton, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Stack, + Toolbar, + Typography, +} from '@mui/material'; +import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; +import ListAltOutlinedIcon from '@mui/icons-material/ListAltOutlined'; +import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined'; +import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined'; +import AccountBalanceOutlinedIcon from '@mui/icons-material/AccountBalanceOutlined'; +import MenuBookOutlinedIcon from '@mui/icons-material/MenuBookOutlined'; +import CalendarMonthOutlinedIcon from '@mui/icons-material/CalendarMonthOutlined'; +import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined'; +import AssessmentOutlinedIcon from '@mui/icons-material/AssessmentOutlined'; +import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined'; +import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined'; +import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; +import PercentOutlinedIcon from '@mui/icons-material/PercentOutlined'; +import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'; +import RequestQuoteOutlinedIcon from '@mui/icons-material/RequestQuoteOutlined'; +import PriceCheckOutlinedIcon from '@mui/icons-material/PriceCheckOutlined'; +import { BranchSwitcher } from '../components/common/BranchSwitcher'; +import { useAuth } from '../hooks/useAuth'; +import { NAVY, NAVY_SURFACE, ACCENT_BLUE } from '../theme'; + +const DRAWER_WIDTH = 250; + +interface NavItem { + label: string; + path: string; + icon: ReactNode; + permission?: string; + end?: boolean; +} + +const NAV_SECTIONS: { title: string; items: NavItem[] }[] = [ + { + title: '', + items: [{ label: 'Dashboard', path: '/', icon: , end: true }], + }, + { + title: 'Sales', + items: [ + { label: 'Customers', path: '/ar/customers', icon: }, + { label: 'Invoices', path: '/ar/invoices', icon: }, + { label: 'Receipts', path: '/ar/receipts', icon: }, + ], + }, + { + title: 'Purchasing', + items: [ + { label: 'Suppliers', path: '/ap/suppliers', icon: }, + { label: 'Bills', path: '/ap/invoices', icon: }, + { label: 'Payments', path: '/ap/payments', icon: }, + ], + }, + { + title: 'General Ledger', + items: [ + { label: 'Journal Entries', path: '/gl/journal-entries', icon: }, + { + label: 'New Manual Entry', + path: '/gl/journal-entries/new', + icon: , + permission: 'gl.journal.post', + }, + ], + }, + { + title: 'Reports', + items: [{ label: 'Reports Centre', path: '/reports', icon: }], + }, + { + title: 'Administration', + items: [ + { + label: 'Segments & Branches', + path: '/admin/segments', + icon: , + permission: 'admin.segments.manage', + }, + { + label: 'Chart of Accounts', + path: '/admin/chart-of-accounts', + icon: , + permission: 'gl.account.manage', + }, + { label: 'Journals', path: '/admin/journals', icon: , permission: 'gl.journal.manage' }, + { + label: 'Fiscal Years', + path: '/admin/fiscal-years', + icon: , + permission: 'admin.fiscalPeriods.manage', + }, + { label: 'Users & Roles', path: '/admin/users', icon: , permission: 'admin.users.manage' }, + { label: 'Tax Codes', path: '/admin/tax-codes', icon: , permission: 'admin.tax.manage' }, + ], + }, +]; + +export function MainLayout() { + const { user, logout, hasPermission } = useAuth(); + const location = useLocation(); + const [menuAnchor, setMenuAnchor] = useState(null); + + const initials = (user?.fullName ?? user?.email ?? '?') + .split(/[\s@.]+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(''); + + return ( + + + + + + CF + + + Cross Financial + MANAGER + + + + + {NAV_SECTIONS.map((section) => { + const items = section.items.filter((item) => !item.permission || hasPermission(item.permission)); + if (items.length === 0) return null; + return ( + + {section.title && ( + + {section.title} + + )} + + {items.map((item) => { + const selected = item.end ? location.pathname === item.path : location.pathname.startsWith(item.path); + return ( + + {item.icon} + + + ); + })} + + + ); + })} + + + + + + + + setMenuAnchor(event.currentTarget)}> + {initials} + + setMenuAnchor(null)}> + + + + {user?.fullName} + + + {user?.email} + + + + + Sign out + + + + + + + + + ); +} diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..1374365 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import './index.css'; +import App from './App.tsx'; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/client/src/routes/AppRouter.tsx b/client/src/routes/AppRouter.tsx new file mode 100644 index 0000000..ae5f513 --- /dev/null +++ b/client/src/routes/AppRouter.tsx @@ -0,0 +1,111 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { MainLayout } from '../layouts/MainLayout'; +import { ProtectedRoute } from './ProtectedRoute'; +import { HandoffPage } from '../features/auth/HandoffPage'; +import { DashboardPage } from '../features/dashboard/DashboardPage'; +import { ReportsPage } from '../features/reports/ReportsPage'; +import { JournalEntryListPage } from '../features/gl/JournalEntryListPage'; +import { JournalEntryFormPage } from '../features/gl/JournalEntryFormPage'; +import { JournalEntryDetailPage } from '../features/gl/JournalEntryDetailPage'; +import { AdminSegmentsPage } from '../features/admin/segments/AdminSegmentsPage'; +import { AdminChartOfAccountsPage } from '../features/admin/coa/AdminChartOfAccountsPage'; +import { AdminJournalsPage } from '../features/admin/journals/AdminJournalsPage'; +import { AdminFiscalYearsPage } from '../features/admin/fiscal/AdminFiscalYearsPage'; +import { AdminUsersPage } from '../features/admin/users/AdminUsersPage'; +import { AdminTaxCodesPage } from '../features/admin/tax/AdminTaxCodesPage'; +import { CustomersListPage } from '../features/ar/customers/CustomersListPage'; +import { CustomerDetailPage } from '../features/ar/customers/CustomerDetailPage'; +import { InvoicesListPage } from '../features/ar/invoices/InvoicesListPage'; +import { InvoiceFormPage } from '../features/ar/invoices/InvoiceFormPage'; +import { InvoiceDetailPage } from '../features/ar/invoices/InvoiceDetailPage'; +import { CreditNoteFormPage } from '../features/ar/creditNotes/CreditNoteFormPage'; +import { CreditNoteDetailPage } from '../features/ar/creditNotes/CreditNoteDetailPage'; +import { ReceiptsListPage } from '../features/ar/receipts/ReceiptsListPage'; +import { ReceiptFormPage } from '../features/ar/receipts/ReceiptFormPage'; +import { ReceiptDetailPage } from '../features/ar/receipts/ReceiptDetailPage'; +import { SuppliersListPage } from '../features/ap/suppliers/SuppliersListPage'; +import { SupplierDetailPage } from '../features/ap/suppliers/SupplierDetailPage'; +import { InvoicesListPage as APInvoicesListPage } from '../features/ap/invoices/InvoicesListPage'; +import { InvoiceFormPage as APInvoiceFormPage } from '../features/ap/invoices/InvoiceFormPage'; +import { InvoiceDetailPage as APInvoiceDetailPage } from '../features/ap/invoices/InvoiceDetailPage'; +import { DebitNoteFormPage } from '../features/ap/debitNotes/DebitNoteFormPage'; +import { DebitNoteDetailPage } from '../features/ap/debitNotes/DebitNoteDetailPage'; +import { PaymentsListPage } from '../features/ap/payments/PaymentsListPage'; +import { PaymentFormPage } from '../features/ap/payments/PaymentFormPage'; +import { PaymentDetailPage } from '../features/ap/payments/PaymentDetailPage'; + +export function AppRouter() { + return ( + + } /> + + }> + }> + } /> + } /> + + } /> + } /> + }> + } /> + + + }> + } /> + + }> + } /> + + }> + } /> + + }> + } /> + + }> + } /> + + }> + } /> + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } /> + + }> + } /> + + }> + } /> + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } /> + + }> + } /> + + }> + } /> + + + + + } /> + + ); +} diff --git a/client/src/routes/ProtectedRoute.tsx b/client/src/routes/ProtectedRoute.tsx new file mode 100644 index 0000000..f9e71cf --- /dev/null +++ b/client/src/routes/ProtectedRoute.tsx @@ -0,0 +1,36 @@ +import { useEffect } from 'react'; +import { Outlet } from 'react-router-dom'; +import { Alert, Box, CircularProgress } from '@mui/material'; +import { useAuth } from '../hooks/useAuth'; + +interface ProtectedRouteProps { + permission?: string; +} + +const managerUrl = import.meta.env.VITE_MANAGER_URL ?? 'http://localhost:5174'; + +export function ProtectedRoute({ permission }: ProtectedRouteProps) { + const { isAuthenticated, hasPermission } = useAuth(); + + useEffect(() => { + if (!isAuthenticated) { + window.location.href = managerUrl; + } + }, [isAuthenticated]); + + if (!isAuthenticated) { + return ( + + + + ); + } + if (permission && !hasPermission(permission)) { + return ( + + You do not have permission to view this page. + + ); + } + return ; +} diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts new file mode 100644 index 0000000..41eb3df --- /dev/null +++ b/client/src/store/authStore.ts @@ -0,0 +1,32 @@ +import { create } from 'zustand'; + +export interface AuthUser { + id: number; + email: string; + fullName?: string; + roles: string[]; + permissions: string[]; + allowedBranchSegmentValueIds: number[] | null; +} + +interface AuthState { + accessToken: string | null; + refreshToken: string | null; + user: AuthUser | null; + setSession: (session: { accessToken: string; refreshToken: string; user: AuthUser }) => void; + setAccessToken: (accessToken: string) => void; + clearSession: () => void; + hasPermission: (permissionKey: string) => boolean; +} + +// Tokens are kept in memory only (not localStorage) to limit XSS exposure — a page reload +// requires signing in again in Phase 1. See the implementation plan §4.5. +export const useAuthStore = create((set, get) => ({ + accessToken: null, + refreshToken: null, + user: null, + setSession: ({ accessToken, refreshToken, user }) => set({ accessToken, refreshToken, user }), + setAccessToken: (accessToken) => set({ accessToken }), + clearSession: () => set({ accessToken: null, refreshToken: null, user: null }), + hasPermission: (permissionKey) => get().user?.permissions.includes(permissionKey) ?? false, +})); diff --git a/client/src/store/branchStore.ts b/client/src/store/branchStore.ts new file mode 100644 index 0000000..cadfe95 --- /dev/null +++ b/client/src/store/branchStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export type BranchSelection = 'MAIN' | number; + +interface BranchState { + selected: BranchSelection; + setSelected: (selection: BranchSelection) => void; +} + +// Persisted to localStorage — this is just a per-viewer UI convenience (last-selected +// branch), never anything sensitive, so unlike auth tokens it's fine to persist. +export const useBranchStore = create()( + persist( + (set) => ({ + selected: 'MAIN', + setSelected: (selection) => set({ selected: selection }), + }), + { name: 'cfm-branch-selection' }, + ), +); + +export function branchScopeQueryParam(selection: BranchSelection): string { + return selection === 'MAIN' ? 'MAIN' : String(selection); +} diff --git a/client/src/theme.ts b/client/src/theme.ts new file mode 100644 index 0000000..0d07c12 --- /dev/null +++ b/client/src/theme.ts @@ -0,0 +1,58 @@ +import { createTheme } from '@mui/material/styles'; + +// Mirrors temp/mockups/Cross Financial Manage Mockup.png: navy sidebar, blue accent, +// card-based content on a light page background, soft status pills. +export const NAVY = '#0f1d3d'; +export const NAVY_SURFACE = '#152a52'; +export const ACCENT_BLUE = '#2f6fed'; + +export const theme = createTheme({ + palette: { + mode: 'light', + primary: { main: ACCENT_BLUE, dark: '#1f52c4' }, + secondary: { main: '#2e7d32' }, + background: { default: '#f4f6fb', paper: '#ffffff' }, + text: { primary: '#1a2233', secondary: '#64748b' }, + divider: '#e6e9f2', + }, + shape: { borderRadius: 10 }, + typography: { + fontFamily: ['Inter', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'].join(','), + h5: { fontWeight: 700 }, + h6: { fontWeight: 700 }, + subtitle1: { fontWeight: 600 }, + }, + components: { + MuiPaper: { + styleOverrides: { + root: { backgroundImage: 'none' }, + elevation1: { boxShadow: '0 1px 2px rgba(15, 29, 61, 0.06)', border: '1px solid #e6e9f2' }, + }, + }, + MuiCard: { + styleOverrides: { + root: { boxShadow: '0 1px 2px rgba(15, 29, 61, 0.06)', border: '1px solid #e6e9f2' }, + }, + }, + MuiButton: { + styleOverrides: { + root: { textTransform: 'none', fontWeight: 600, borderRadius: 8 }, + }, + }, + MuiChip: { + styleOverrides: { + root: { fontWeight: 600 }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { boxShadow: 'none', borderBottom: '1px solid #e6e9f2' }, + }, + }, + MuiTableCell: { + styleOverrides: { + head: { fontWeight: 700, color: '#64748b', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: 0.4 }, + }, + }, + }, +}); diff --git a/client/src/types/api.ts b/client/src/types/api.ts new file mode 100644 index 0000000..fd2cd24 --- /dev/null +++ b/client/src/types/api.ts @@ -0,0 +1,379 @@ +export type GLAccountType = 'ASSET' | 'LIABILITY' | 'EQUITY' | 'INCOME' | 'EXPENSE'; +export type DebitCredit = 'DEBIT' | 'CREDIT'; +export type ControlAccountType = 'AR' | 'AP' | 'STOCK' | 'TAX_OUTPUT' | 'TAX_INPUT' | 'BANK' | 'RETAINED_EARNINGS'; +export type SegmentFormat = 'NUMERIC' | 'ALPHA' | 'ALPHANUMERIC'; +export type JournalType = 'GENERAL' | 'SALES' | 'PURCHASES' | 'CASH_RECEIPT' | 'CASH_PAYMENT' | 'STOCK' | 'BANK' | 'CUSTOM'; +export type JournalEntryStatus = 'DRAFT' | 'POSTED' | 'REVERSED'; +export type ARDocumentStatus = 'DRAFT' | 'POSTED' | 'VOID'; +export type APDocumentStatus = 'DRAFT' | 'POSTED' | 'VOID'; + +export interface AccountSegmentValue { + id: number; + segmentDefId: number; + code: string; + description: string; + isActive: boolean; +} + +export interface AccountSegmentDefinition { + id: number; + segmentKey: string; + label: string; + order: number; + length: number; + format: SegmentFormat; + isBranchSegment: boolean; + isActive: boolean; + values: AccountSegmentValue[]; +} + +export interface GLAccountSegmentValue { + segmentDefId: number; + segmentValueId: number; + segmentValue: AccountSegmentValue; + segmentDef: AccountSegmentDefinition; +} + +export interface GLAccount { + id: number; + accountCode: string; + accountName: string; + accountType: GLAccountType; + normalBalance: DebitCredit; + isControlAccount: boolean; + controlType: ControlAccountType | null; + isActive: boolean; + isPostable: boolean; + segmentValues: GLAccountSegmentValue[]; +} + +export interface Journal { + id: number; + code: string; + name: string; + journalType: JournalType; + defaultBranchSegmentValueId: number | null; + isActive: boolean; +} + +export interface JournalEntryLine { + id: number; + journalEntryId: number; + lineNumber: number; + glAccountId: number; + glAccount: GLAccount; + debit: string; + credit: string; + description: string | null; + branchSegmentValueId: number | null; +} + +export interface JournalEntry { + id: number; + entryNumber: string; + journalId: number; + journal: Journal; + entryDate: string; + description: string; + status: JournalEntryStatus; + totalDebits: string; + totalCredits: string; + createdAt: string; + lines: JournalEntryLine[]; +} + +export interface FiscalPeriod { + id: number; + periodNumber: number; + startDate: string; + endDate: string; + status: 'OPEN' | 'CLOSED'; +} + +export interface FiscalYear { + id: number; + year: number; + startDate: string; + endDate: string; + status: 'OPEN' | 'LOCKED' | 'CLOSED'; + periods: FiscalPeriod[]; +} + +export interface Role { + id: number; + name: string; + permissions: { permission: { id: number; key: string } }[]; +} + +export interface Permission { + id: number; + key: string; +} + +export interface TaxCode { + id: number; + code: string; + name: string; + ratePercent: string; + isActive: boolean; +} + +export interface Customer { + id: number; + code: string; + name: string; + email: string | null; + phone: string | null; + address: string | null; + taxNumber: string | null; + branchSegmentValueId: number; + creditLimit: string; + paymentTermsDays: number; + isActive: boolean; + createdAt: string; +} + +export interface ARInvoiceLine { + id: number; + invoiceId: number; + lineNumber: number; + description: string; + quantity: string; + unitPrice: string; + revenueGLAccountId: number; + revenueGLAccount?: GLAccount; + taxCodeId: number | null; + taxCode?: TaxCode | null; + lineSubtotal: string; + lineTax: string; + lineTotal: string; +} + +export interface ARAllocation { + id: number; + invoiceId: number; + invoice?: ARInvoice; + receiptId: number | null; + receipt?: ARReceipt | null; + creditNoteId: number | null; + creditNote?: ARCreditNote | null; + amount: string; + allocatedAt: string; +} + +export interface ARInvoice { + id: number; + invoiceNumber: string; + customerId: number; + customer?: Customer; + branchSegmentValueId: number; + invoiceDate: string; + dueDate: string; + reference: string | null; + status: ARDocumentStatus; + subtotal: string; + taxTotal: string; + total: string; + balance: string; + journalEntryId: number | null; + lines: ARInvoiceLine[]; + allocations?: ARAllocation[]; + createdAt: string; +} + +export interface ARCreditNoteLine { + id: number; + creditNoteId: number; + lineNumber: number; + description: string; + quantity: string; + unitPrice: string; + revenueGLAccountId: number; + revenueGLAccount?: GLAccount; + taxCodeId: number | null; + taxCode?: TaxCode | null; + lineSubtotal: string; + lineTax: string; + lineTotal: string; +} + +export interface ARCreditNote { + id: number; + creditNoteNumber: string; + customerId: number; + customer?: Customer; + branchSegmentValueId: number; + creditNoteDate: string; + reference: string | null; + reason: string | null; + status: ARDocumentStatus; + subtotal: string; + taxTotal: string; + total: string; + balance: string; + journalEntryId: number | null; + lines: ARCreditNoteLine[]; + allocations?: ARAllocation[]; + createdAt: string; +} + +export interface ARReceipt { + id: number; + receiptNumber: string; + customerId: number; + customer?: Customer; + branchSegmentValueId: number; + receiptDate: string; + amount: string; + unallocatedAmount: string; + bankGLAccountId: number; + bankGLAccount?: GLAccount; + reference: string | null; + status: ARDocumentStatus; + journalEntryId: number | null; + allocations?: ARAllocation[]; + createdAt: string; +} + +export interface CustomerDetail { + customer: Customer; + invoices: ARInvoice[]; + creditNotes: ARCreditNote[]; + receipts: ARReceipt[]; + outstandingBalance: number; + overdueBalance: number; +} + +export interface Supplier { + id: number; + code: string; + name: string; + email: string | null; + phone: string | null; + address: string | null; + taxNumber: string | null; + branchSegmentValueId: number; + paymentTermsDays: number; + isActive: boolean; + createdAt: string; +} + +export interface APInvoiceLine { + id: number; + invoiceId: number; + lineNumber: number; + description: string; + quantity: string; + unitPrice: string; + expenseGLAccountId: number; + expenseGLAccount?: GLAccount; + taxCodeId: number | null; + taxCode?: TaxCode | null; + lineSubtotal: string; + lineTax: string; + lineTotal: string; +} + +export interface APAllocation { + id: number; + invoiceId: number; + invoice?: APInvoice; + paymentId: number | null; + payment?: APPayment | null; + debitNoteId: number | null; + debitNote?: APDebitNote | null; + amount: string; + allocatedAt: string; +} + +export interface APInvoice { + id: number; + invoiceNumber: string; + supplierReference: string | null; + supplierId: number; + supplier?: Supplier; + branchSegmentValueId: number; + invoiceDate: string; + dueDate: string; + status: APDocumentStatus; + subtotal: string; + taxTotal: string; + total: string; + balance: string; + journalEntryId: number | null; + lines: APInvoiceLine[]; + allocations?: APAllocation[]; + createdAt: string; +} + +export interface APDebitNoteLine { + id: number; + debitNoteId: number; + lineNumber: number; + description: string; + quantity: string; + unitPrice: string; + expenseGLAccountId: number; + expenseGLAccount?: GLAccount; + taxCodeId: number | null; + taxCode?: TaxCode | null; + lineSubtotal: string; + lineTax: string; + lineTotal: string; +} + +export interface APDebitNote { + id: number; + debitNoteNumber: string; + supplierId: number; + supplier?: Supplier; + branchSegmentValueId: number; + debitNoteDate: string; + reference: string | null; + reason: string | null; + status: APDocumentStatus; + subtotal: string; + taxTotal: string; + total: string; + balance: string; + journalEntryId: number | null; + lines: APDebitNoteLine[]; + allocations?: APAllocation[]; + createdAt: string; +} + +export interface APPayment { + id: number; + paymentNumber: string; + supplierId: number; + supplier?: Supplier; + branchSegmentValueId: number; + paymentDate: string; + amount: string; + unallocatedAmount: string; + bankGLAccountId: number; + bankGLAccount?: GLAccount; + reference: string | null; + status: APDocumentStatus; + journalEntryId: number | null; + allocations?: APAllocation[]; + createdAt: string; +} + +export interface SupplierDetail { + supplier: Supplier; + invoices: APInvoice[]; + debitNotes: APDebitNote[]; + payments: APPayment[]; + outstandingBalance: number; + overdueBalance: number; +} + +export interface AppUser { + id: number; + email: string; + fullName: string; + isActive: boolean; + roles: { role: Role }[]; + branchRestrictions: { branchSegmentValueId: number }[]; +} diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/client/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/client/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 0000000..9982072 --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..2da0204 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testMatch: ['/tests/unit/**/*.spec.ts'], + modulePathIgnorePatterns: ['/client/', '/manager/'], + setupFilesAfterEnv: [], + clearMocks: true, +}; diff --git a/jest.integration.config.js b/jest.integration.config.js new file mode 100644 index 0000000..a09460e --- /dev/null +++ b/jest.integration.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testMatch: ['/tests/integration/**/*.spec.ts'], + modulePathIgnorePatterns: ['/client/', '/manager/'], + clearMocks: true, +}; diff --git a/manager/.env.example b/manager/.env.example new file mode 100644 index 0000000..c0c9f89 --- /dev/null +++ b/manager/.env.example @@ -0,0 +1,30 @@ +DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/cfm_manager?schema=public" +PORT="5100" + +# Manager-internal session tokens (Person sessions on the Manager app itself — never leave the Manager) +JWT_ACCESS_SECRET="change-me-access-secret" +JWT_REFRESH_SECRET="change-me-refresh-secret" +JWT_ACCESS_TTL="15m" +JWT_REFRESH_TTL="7d" + +# RS256 keypair used to sign/verify handoff tokens. Generate once with: +# openssl genrsa -out manager/keys/handoff-private.pem 2048 +# openssl rsa -in manager/keys/handoff-private.pem -pubout -out manager/keys/handoff-public.pem +# Both files are gitignored. Every company app is given a copy of the PUBLIC key only. +HANDOFF_PRIVATE_KEY_PATH="./keys/handoff-private.pem" +HANDOFF_PUBLIC_KEY_PATH="./keys/handoff-public.pem" +HANDOFF_TOKEN_TTL_SECONDS="90" + +TOTP_ISSUER="Cross Financial Manager" + +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" +GOOGLE_REDIRECT_URI="http://localhost:5100/api/auth/oauth/google/callback" + +MICROSOFT_CLIENT_ID="" +MICROSOFT_CLIENT_SECRET="" +MICROSOFT_TENANT="common" +MICROSOFT_REDIRECT_URI="http://localhost:5100/api/auth/oauth/microsoft/callback" + +# Where the Manager's own frontend lives, used to build OAuth-return / redirect URLs +MANAGER_WEB_URL="http://localhost:5174" diff --git a/manager/.eslintrc.json b/manager/.eslintrc.json new file mode 100644 index 0000000..1b11a27 --- /dev/null +++ b/manager/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "env": { + "node": true, + "es2022": true, + "jest": true + }, + "rules": { + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] + } +} diff --git a/manager/.gitignore b/manager/.gitignore new file mode 100644 index 0000000..fd32dd5 --- /dev/null +++ b/manager/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.env +*.log +coverage/ +keys/ +client/node_modules/ +client/dist/ diff --git a/manager/SETUP.md b/manager/SETUP.md new file mode 100644 index 0000000..d27ba31 --- /dev/null +++ b/manager/SETUP.md @@ -0,0 +1,38 @@ +# Setup — Account Manager (Phase A) + +## Prerequisites +- Node.js 18+ (tested on v22) +- A reachable PostgreSQL 15+ database, separate from any company app's database +- The company app checked out as a sibling (`../` from this directory) — Phase A wires this + Manager to exactly one manually-provisioned company + +## First-time setup +1. `npm install` +2. `npm run generate-handoff-keys` — generates `keys/handoff-private.pem` (kept here, gitignored, + never shared) and `keys/handoff-public.pem` (copy this into every company app's + `keys/handoff-public.pem`, referenced by its `HANDOFF_PUBLIC_KEY_PATHS`). +3. Copy `.env.example` to `.env`, fill in a real `DATABASE_URL`, random JWT secrets, and (optional + for now) Google/Microsoft OAuth credentials. +4. `npm run prisma:migrate` then `npm run prisma:seed` — seeds one operator `Person` + (`joosth2002@icloud.com` / `ChangeMe123!` — change this) and one `Company` row pointing at the + company app on `http://localhost:5173` (the frontend, since `/handoff` is a frontend route). +5. `npm run dev` — API on `http://localhost:5100`. +6. `cd client && npm install && npm run dev` — Manager frontend on `http://localhost:5174`. + +## Verifying it works +- `npm test` — unit tests (handoff token sign/verify, tampering, expiry — no DB required). +- `npm run test:integration` — against the real Manager DB: login, list companies, issue a + handoff token, reject a handoff to a company with no membership. +- The company app has its own integration test (`../tests/integration/handoff.spec.ts`) that + mints a token with this Manager's real private key and exchanges it against the real company + app DB — proving both sides of the handoff agree, replay is rejected, and a token minted for a + different `companySlug` is rejected there. +- Manual browser smoke: with both this Manager (`5100`/`5174`) and the company app (`5000`/`5173`) + running, open `http://localhost:5174`, sign in with the seeded operator, pick "Demo Company," + and confirm you land inside the company app already authenticated (and that + `http://localhost:5173/login` no longer exists — it's `/handoff` now). +- `GET /health` — basic liveness check. + +## Adding a second company (still manual in Phase A) +Phase B automates this. Until then: stand up another copy of the company app on a different +port/database by hand, then insert matching `Company` and `PersonCompanyMembership` rows here. diff --git a/manager/client/.env.example b/manager/client/.env.example new file mode 100644 index 0000000..528603d --- /dev/null +++ b/manager/client/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL="http://localhost:5100/api" diff --git a/manager/client/.gitignore b/manager/client/.gitignore new file mode 100644 index 0000000..438657a --- /dev/null +++ b/manager/client/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/manager/client/.oxlintrc.json b/manager/client/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/manager/client/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/manager/client/README.md b/manager/client/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/manager/client/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/manager/client/index.html b/manager/client/index.html new file mode 100644 index 0000000..6476681 --- /dev/null +++ b/manager/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Cross Financial Manager — Account Manager + + +
+ + + diff --git a/manager/client/package-lock.json b/manager/client/package-lock.json new file mode 100644 index 0000000..c5ce6a6 --- /dev/null +++ b/manager/client/package-lock.json @@ -0,0 +1,2663 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^9.4.0", + "@tanstack/react-query": "^5.102.8", + "axios": "^1.20.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.3", + "zustand": "^5.0.15" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "typescript": "~6.0.2", + "vite": "^8.2.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.4.0.tgz", + "integrity": "sha512-d+gIYM1raJP87yrVQcVg+Q3vycVMArMkgu/TjJLi2vTvO9FHKWNRR1xs9nnN4eLpM9nGxxm02UMvc9BrmcUKvQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.4.0.tgz", + "integrity": "sha512-6KKUIvkQ2r/s48s9VZV9JEZ1W2dRa7RndVsF+Ipx4CYxDOQeozeRsS5r9NiheFf8A5hpbqJJIGoX7hjg5X4XUw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/core-downloads-tracker": "^9.4.0", + "@mui/system": "^9.4.0", + "@mui/types": "^9.4.0", + "@mui/utils": "^9.4.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.8", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.4.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.4.0.tgz", + "integrity": "sha512-qQpX/2XPGDz5F2OIWzo4kkorURlDeXGKytRQudXCdLS5TuwKsMKaZyx77LZEVSB73c7o3UrLWwQ/goRuHBdtjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.4.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.4.0.tgz", + "integrity": "sha512-SlqVEYqnyEgXMFW4JuIDMelyhKzoWBJ85v2mNCQd0XtU0uk7U9E1zaJZ06XqodhA8oJrb+4Q+3E/JlwBe4jWkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.4.0.tgz", + "integrity": "sha512-AZz9z13oGS1J0BYeAFHTbV7PegCtPiE/uMEIi7TJ1kTx3Vkt+jUpLyXF/2uV8B6Y79iTh98kVeOH9fX3x3zXXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/private-theming": "^9.4.0", + "@mui/styled-engine": "^9.4.0", + "@mui/types": "^9.4.0", + "@mui/utils": "^9.4.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.4.0.tgz", + "integrity": "sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.4.0.tgz", + "integrity": "sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.4.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz", + "integrity": "sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.81.0.tgz", + "integrity": "sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz", + "integrity": "sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.81.0.tgz", + "integrity": "sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.81.0.tgz", + "integrity": "sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.81.0.tgz", + "integrity": "sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.81.0.tgz", + "integrity": "sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.81.0.tgz", + "integrity": "sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.81.0.tgz", + "integrity": "sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.81.0.tgz", + "integrity": "sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.81.0.tgz", + "integrity": "sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.81.0.tgz", + "integrity": "sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.81.0.tgz", + "integrity": "sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.81.0.tgz", + "integrity": "sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.81.0.tgz", + "integrity": "sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.81.0.tgz", + "integrity": "sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.81.0.tgz", + "integrity": "sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.81.0.tgz", + "integrity": "sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz", + "integrity": "sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", + "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", + "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", + "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", + "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.7.tgz", + "integrity": "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.7.tgz", + "integrity": "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.7.tgz", + "integrity": "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.7.tgz", + "integrity": "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.7.tgz", + "integrity": "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.7.tgz", + "integrity": "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.7.tgz", + "integrity": "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.7.tgz", + "integrity": "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.7.tgz", + "integrity": "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.7.tgz", + "integrity": "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.7.tgz", + "integrity": "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oxlint": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.81.0.tgz", + "integrity": "sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.81.0", + "@oxlint/binding-android-arm64": "1.81.0", + "@oxlint/binding-darwin-arm64": "1.81.0", + "@oxlint/binding-darwin-x64": "1.81.0", + "@oxlint/binding-freebsd-x64": "1.81.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.81.0", + "@oxlint/binding-linux-arm-musleabihf": "1.81.0", + "@oxlint/binding-linux-arm64-gnu": "1.81.0", + "@oxlint/binding-linux-arm64-musl": "1.81.0", + "@oxlint/binding-linux-ppc64-gnu": "1.81.0", + "@oxlint/binding-linux-riscv64-gnu": "1.81.0", + "@oxlint/binding-linux-riscv64-musl": "1.81.0", + "@oxlint/binding-linux-s390x-gnu": "1.81.0", + "@oxlint/binding-linux-x64-gnu": "1.81.0", + "@oxlint/binding-linux-x64-musl": "1.81.0", + "@oxlint/binding-openharmony-arm64": "1.81.0", + "@oxlint/binding-win32-arm64-msvc": "1.81.0", + "@oxlint/binding-win32-ia32-msvc": "1.81.0", + "@oxlint/binding-win32-x64-msvc": "1.81.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", + "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.148.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.7", + "@rolldown/binding-android-arm64": "1.2.7", + "@rolldown/binding-darwin-arm64": "1.2.7", + "@rolldown/binding-darwin-x64": "1.2.7", + "@rolldown/binding-freebsd-x64": "1.2.7", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", + "@rolldown/binding-linux-arm64-gnu": "1.2.7", + "@rolldown/binding-linux-arm64-musl": "1.2.7", + "@rolldown/binding-linux-ppc64-gnu": "1.2.7", + "@rolldown/binding-linux-s390x-gnu": "1.2.7", + "@rolldown/binding-linux-x64-gnu": "1.2.7", + "@rolldown/binding-linux-x64-musl": "1.2.7", + "@rolldown/binding-openharmony-arm64": "1.2.7", + "@rolldown/binding-win32-arm64-msvc": "1.2.7", + "@rolldown/binding-win32-x64-msvc": "1.2.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/manager/client/package.json b/manager/client/package.json new file mode 100644 index 0000000..8f87e4b --- /dev/null +++ b/manager/client/package.json @@ -0,0 +1,32 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^9.4.0", + "@tanstack/react-query": "^5.102.8", + "axios": "^1.20.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.3", + "zustand": "^5.0.15" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "typescript": "~6.0.2", + "vite": "^8.2.2" + } +} diff --git a/manager/client/public/favicon.svg b/manager/client/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/manager/client/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/manager/client/public/icons.svg b/manager/client/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/manager/client/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/manager/client/src/App.tsx b/manager/client/src/App.tsx new file mode 100644 index 0000000..98e26ef --- /dev/null +++ b/manager/client/src/App.tsx @@ -0,0 +1,15 @@ +import { BrowserRouter } from 'react-router-dom'; +import { ThemeProvider, CssBaseline } from '@mui/material'; +import { theme } from './theme'; +import { AppRouter } from './routes/AppRouter'; + +export default function App() { + return ( + + + + + + + ); +} diff --git a/manager/client/src/api/auth.api.ts b/manager/client/src/api/auth.api.ts new file mode 100644 index 0000000..1857e87 --- /dev/null +++ b/manager/client/src/api/auth.api.ts @@ -0,0 +1,39 @@ +import { apiClient } from './client'; +import type { AuthPerson } from '../store/authStore'; + +export type LoginResult = + | { requiresTwoFactor: true; twoFactorToken: string } + | { requiresTwoFactor: false; accessToken: string; refreshToken: string; person: AuthPerson }; + +export async function login(email: string, password: string): Promise { + const response = await apiClient.post('/auth/login', { email, password }); + return response.data; +} + +export async function verifyTwoFactor(twoFactorToken: string, code: string) { + const response = await apiClient.post<{ accessToken: string; refreshToken: string; person: AuthPerson }>( + '/auth/2fa/verify', + { twoFactorToken, code }, + ); + return response.data; +} + +export async function fetchMe(): Promise { + const response = await apiClient.get<{ person: AuthPerson }>('/auth/me'); + return response.data.person; +} + +export async function enrollTwoFactor() { + const response = await apiClient.post<{ secret: string; otpauthUrl: string }>('/auth/2fa/enroll'); + return response.data; +} + +export async function enableTwoFactor(code: string) { + const response = await apiClient.post<{ backupCodes: string[] }>('/auth/2fa/enable', { code }); + return response.data; +} + +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5100/api'; + +export const googleLoginUrl = `${apiBaseUrl}/auth/oauth/google`; +export const microsoftLoginUrl = `${apiBaseUrl}/auth/oauth/microsoft`; diff --git a/manager/client/src/api/client.ts b/manager/client/src/api/client.ts new file mode 100644 index 0000000..359d8d7 --- /dev/null +++ b/manager/client/src/api/client.ts @@ -0,0 +1,53 @@ +import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'; +import { useAuthStore } from '../store/authStore'; + +const baseURL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5100/api'; + +export const apiClient = axios.create({ baseURL }); + +apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => { + const { accessToken } = useAuthStore.getState(); + if (accessToken) { + config.headers.set('Authorization', `Bearer ${accessToken}`); + } + return config; +}); + +let refreshInFlight: Promise | null = null; + +async function refreshAccessToken(): Promise { + const { refreshToken, setAccessToken, clearSession } = useAuthStore.getState(); + if (!refreshToken) { + clearSession(); + throw new Error('No refresh token available'); + } + try { + const response = await axios.post<{ accessToken: string }>(`${baseURL}/auth/refresh`, { refreshToken }); + setAccessToken(response.data.accessToken); + return response.data.accessToken; + } catch (error) { + clearSession(); + throw error; + } +} + +apiClient.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config as (InternalAxiosRequestConfig & { _retried?: boolean }) | undefined; + if (error.response?.status === 401 && originalRequest && !originalRequest._retried) { + originalRequest._retried = true; + try { + refreshInFlight ??= refreshAccessToken().finally(() => { + refreshInFlight = null; + }); + const accessToken = await refreshInFlight; + originalRequest.headers.set('Authorization', `Bearer ${accessToken}`); + return apiClient(originalRequest); + } catch { + return Promise.reject(error); + } + } + return Promise.reject(error); + }, +); diff --git a/manager/client/src/api/companies.api.ts b/manager/client/src/api/companies.api.ts new file mode 100644 index 0000000..ff7b486 --- /dev/null +++ b/manager/client/src/api/companies.api.ts @@ -0,0 +1,18 @@ +import { apiClient } from './client'; + +export interface MyCompany { + companyId: number; + name: string; + slug: string; + status: 'PROVISIONING' | 'ACTIVE' | 'STOPPED' | 'FAILED' | 'DECOMMISSIONED'; +} + +export async function fetchMyCompanies(): Promise { + const response = await apiClient.get('/companies'); + return response.data; +} + +export async function requestHandoff(companyId: number): Promise<{ token: string; redirectUrl: string }> { + const response = await apiClient.post<{ token: string; redirectUrl: string }>(`/companies/${companyId}/handoff`); + return response.data; +} diff --git a/manager/client/src/features/auth/LoginPage.tsx b/manager/client/src/features/auth/LoginPage.tsx new file mode 100644 index 0000000..9771b8e --- /dev/null +++ b/manager/client/src/features/auth/LoginPage.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { Alert, Box, Button, Divider, Paper, Stack, TextField, Typography } from '@mui/material'; +import { useAuth } from '../../hooks/useAuth'; +import { googleLoginUrl, microsoftLoginUrl } from '../../api/auth.api'; + +export function LoginPage() { + const { isAuthenticated, login, isLoggingIn, loginError } = useAuth(); + const navigate = useNavigate(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + if (isAuthenticated) { + return ; + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const result = await login({ email, password }); + if (result.requiresTwoFactor) { + navigate('/2fa', { state: { twoFactorToken: result.twoFactorToken } }); + } else { + navigate('/companies'); + } + } catch { + // surfaced via loginError below + } + }; + + return ( + + + + Cross Financial Manager + + + Sign in to continue + + {loginError && ( + + Invalid email or password + + )} + + setEmail(e.target.value)} /> + setPassword(e.target.value)} /> + + + + or continue with + + + + + + + + ); +} diff --git a/manager/client/src/features/auth/OAuthCallbackPage.tsx b/manager/client/src/features/auth/OAuthCallbackPage.tsx new file mode 100644 index 0000000..364f4f8 --- /dev/null +++ b/manager/client/src/features/auth/OAuthCallbackPage.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Alert, Box, CircularProgress, Typography } from '@mui/material'; +import { fetchMe } from '../../api/auth.api'; +import { useAuthStore } from '../../store/authStore'; + +/** + * The Manager's own Google/Microsoft OAuth callback lands here (redirected by the backend + * after it exchanges the provider's code) carrying either a full session or a 2FA challenge. + */ +export function OAuthCallbackPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { setAccessToken, setSession } = useAuthStore(); + const [error, setError] = useState(null); + const attempted = useRef(false); + + useEffect(() => { + if (attempted.current) return; + attempted.current = true; + + const twoFactorToken = searchParams.get('twoFactorToken'); + if (twoFactorToken) { + navigate('/2fa', { state: { twoFactorToken }, replace: true }); + return; + } + + const accessToken = searchParams.get('accessToken'); + const refreshToken = searchParams.get('refreshToken'); + if (!accessToken || !refreshToken) { + setError('Sign-in did not complete. Please try again.'); + return; + } + + setAccessToken(accessToken); + fetchMe() + .then((person) => { + setSession({ accessToken, refreshToken, person }); + navigate('/companies', { replace: true }); + }) + .catch(() => setError('Sign-in did not complete. Please try again.')); + }, [searchParams, navigate, setAccessToken, setSession]); + + return ( + + {!error && ( + <> + + Completing sign-in… + + )} + {error && ( + + {error} + + )} + + ); +} diff --git a/manager/client/src/features/auth/SecuritySettingsPage.tsx b/manager/client/src/features/auth/SecuritySettingsPage.tsx new file mode 100644 index 0000000..991b7af --- /dev/null +++ b/manager/client/src/features/auth/SecuritySettingsPage.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react'; +import { Alert, Box, Button, Container, Paper, Stack, TextField, Typography } from '@mui/material'; +import { useMutation } from '@tanstack/react-query'; +import { enableTwoFactor, enrollTwoFactor } from '../../api/auth.api'; + +export function SecuritySettingsPage() { + const [otpauthUrl, setOtpauthUrl] = useState(null); + const [secret, setSecret] = useState(null); + const [code, setCode] = useState(''); + const [backupCodes, setBackupCodes] = useState(null); + + const enrollMutation = useMutation({ + mutationFn: enrollTwoFactor, + onSuccess: (result) => { + setOtpauthUrl(result.otpauthUrl); + setSecret(result.secret); + }, + }); + + const enableMutation = useMutation({ + mutationFn: (enteredCode: string) => enableTwoFactor(enteredCode), + onSuccess: (result) => setBackupCodes(result.backupCodes), + }); + + return ( + + + Two-factor authentication + + + {!otpauthUrl && !backupCodes && ( + + )} + + {otpauthUrl && !backupCodes && ( + + + Add this account to your authenticator app, either by scanning a QR code for this URL or entering the + secret manually, then confirm with a generated code: + + {otpauthUrl} + + Secret: {secret} + + + setCode(e.target.value)} size="small" /> + + + {enableMutation.isError && Invalid code — try again.} + + )} + + {backupCodes && ( + + + Two-factor authentication is enabled. + + + Save these one-time backup codes somewhere safe — each can be used once if you lose access to your + authenticator app: + + + {backupCodes.map((backupCode) => ( +
{backupCode}
+ ))} +
+
+ )} +
+ ); +} diff --git a/manager/client/src/features/auth/TwoFactorChallengePage.tsx b/manager/client/src/features/auth/TwoFactorChallengePage.tsx new file mode 100644 index 0000000..467ddd9 --- /dev/null +++ b/manager/client/src/features/auth/TwoFactorChallengePage.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { Navigate, useLocation, useNavigate } from 'react-router-dom'; +import { Alert, Box, Button, Paper, Stack, TextField, Typography } from '@mui/material'; +import { useAuth } from '../../hooks/useAuth'; + +interface LocationState { + twoFactorToken?: string; +} + +export function TwoFactorChallengePage() { + const location = useLocation(); + const navigate = useNavigate(); + const { verifyTwoFactor, isVerifyingTwoFactor, twoFactorError } = useAuth(); + const [code, setCode] = useState(''); + const twoFactorToken = (location.state as LocationState | null)?.twoFactorToken; + + if (!twoFactorToken) { + return ; + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + await verifyTwoFactor({ twoFactorToken, code }); + navigate('/companies'); + } catch { + // surfaced via twoFactorError below + } + }; + + return ( + + + + Two-factor verification + + + Enter the 6-digit code from your authenticator app, or one of your backup codes. + + {twoFactorError && ( + + Invalid code + + )} + + setCode(e.target.value)} + autoFocus + inputMode="numeric" + /> + + + + + ); +} diff --git a/manager/client/src/features/companies/CompanyPickerPage.tsx b/manager/client/src/features/companies/CompanyPickerPage.tsx new file mode 100644 index 0000000..b675e69 --- /dev/null +++ b/manager/client/src/features/companies/CompanyPickerPage.tsx @@ -0,0 +1,123 @@ +import { useState } from 'react'; +import { + AppBar, + Alert, + Avatar, + Box, + Button, + Card, + CardActionArea, + CardContent, + Chip, + CircularProgress, + Container, + IconButton, + Menu, + MenuItem, + Radio, + Stack, + Toolbar, + Typography, +} from '@mui/material'; +import { Link as RouterLink } from 'react-router-dom'; +import { useMyCompanies, useRequestHandoff } from '../../hooks/useCompanies'; +import { useAuth } from '../../hooks/useAuth'; +import type { MyCompany } from '../../api/companies.api'; + +const STATUS_COLOR: Record = { + ACTIVE: 'success', + PROVISIONING: 'warning', + STOPPED: 'default', + FAILED: 'error', + DECOMMISSIONED: 'default', +}; + +export function CompanyPickerPage() { + const { person, logout } = useAuth(); + const { data: companies, isLoading } = useMyCompanies(); + const requestHandoff = useRequestHandoff(); + const [selectedId, setSelectedId] = useState(null); + const [menuAnchor, setMenuAnchor] = useState(null); + const [error, setError] = useState(null); + + const handleContinue = async () => { + if (selectedId === null) return; + setError(null); + try { + const { token, redirectUrl } = await requestHandoff.mutateAsync(selectedId); + window.location.href = `${redirectUrl}/handoff?token=${encodeURIComponent(token)}`; + } catch { + setError('Could not reach that company right now. Please try again.'); + } + }; + + return ( + + + + + Cross Financial Manager + + setMenuAnchor(event.currentTarget)}> + {person?.email?.[0]?.toUpperCase()} + + setMenuAnchor(null)}> + {person?.email} + setMenuAnchor(null)}> + Security (2FA) + + Sign out + + + + + + + Select an organisation + + + Choose which company you want to work in. + + + {isLoading && } + + {!isLoading && (companies?.length ?? 0) === 0 && You are not a member of any company yet.} + + + {(companies ?? []).map((company) => ( + + setSelectedId(company.companyId)} sx={{ p: 0.5 }}> + + + + {company.name} + + {company.slug} + + + + + + + ))} + + + {error && ( + + {error} + + )} + + + + + ); +} diff --git a/manager/client/src/hooks/useAuth.ts b/manager/client/src/hooks/useAuth.ts new file mode 100644 index 0000000..1f1e254 --- /dev/null +++ b/manager/client/src/hooks/useAuth.ts @@ -0,0 +1,33 @@ +import { useMutation } from '@tanstack/react-query'; +import * as authApi from '../api/auth.api'; +import { useAuthStore } from '../store/authStore'; + +export function useAuth() { + const { person, accessToken, setSession, clearSession } = useAuthStore(); + + const loginMutation = useMutation({ + mutationFn: ({ email, password }: { email: string; password: string }) => authApi.login(email, password), + onSuccess: (result) => { + if (!result.requiresTwoFactor) setSession(result); + }, + }); + + const twoFactorMutation = useMutation({ + mutationFn: ({ twoFactorToken, code }: { twoFactorToken: string; code: string }) => + authApi.verifyTwoFactor(twoFactorToken, code), + onSuccess: (session) => setSession(session), + }); + + return { + person, + isAuthenticated: Boolean(accessToken && person), + login: loginMutation.mutateAsync, + isLoggingIn: loginMutation.isPending, + loginError: loginMutation.error, + verifyTwoFactor: twoFactorMutation.mutateAsync, + isVerifyingTwoFactor: twoFactorMutation.isPending, + twoFactorError: twoFactorMutation.error, + setSession, + logout: clearSession, + }; +} diff --git a/manager/client/src/hooks/useCompanies.ts b/manager/client/src/hooks/useCompanies.ts new file mode 100644 index 0000000..7960428 --- /dev/null +++ b/manager/client/src/hooks/useCompanies.ts @@ -0,0 +1,10 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { fetchMyCompanies, requestHandoff } from '../api/companies.api'; + +export function useMyCompanies() { + return useQuery({ queryKey: ['myCompanies'], queryFn: fetchMyCompanies }); +} + +export function useRequestHandoff() { + return useMutation({ mutationFn: (companyId: number) => requestHandoff(companyId) }); +} diff --git a/manager/client/src/index.css b/manager/client/src/index.css new file mode 100644 index 0000000..e4a5833 --- /dev/null +++ b/manager/client/src/index.css @@ -0,0 +1,7 @@ +html, body, #root { + height: 100%; +} + +body { + margin: 0; +} diff --git a/manager/client/src/main.tsx b/manager/client/src/main.tsx new file mode 100644 index 0000000..1374365 --- /dev/null +++ b/manager/client/src/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import './index.css'; +import App from './App.tsx'; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/manager/client/src/routes/AppRouter.tsx b/manager/client/src/routes/AppRouter.tsx new file mode 100644 index 0000000..f78ea75 --- /dev/null +++ b/manager/client/src/routes/AppRouter.tsx @@ -0,0 +1,25 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { ProtectedRoute } from './ProtectedRoute'; +import { LoginPage } from '../features/auth/LoginPage'; +import { TwoFactorChallengePage } from '../features/auth/TwoFactorChallengePage'; +import { OAuthCallbackPage } from '../features/auth/OAuthCallbackPage'; +import { SecuritySettingsPage } from '../features/auth/SecuritySettingsPage'; +import { CompanyPickerPage } from '../features/companies/CompanyPickerPage'; + +export function AppRouter() { + return ( + + } /> + } /> + } /> + + }> + } /> + } /> + } /> + + + } /> + + ); +} diff --git a/manager/client/src/routes/ProtectedRoute.tsx b/manager/client/src/routes/ProtectedRoute.tsx new file mode 100644 index 0000000..e6c1cbf --- /dev/null +++ b/manager/client/src/routes/ProtectedRoute.tsx @@ -0,0 +1,10 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +export function ProtectedRoute() { + const { isAuthenticated } = useAuth(); + if (!isAuthenticated) { + return ; + } + return ; +} diff --git a/manager/client/src/store/authStore.ts b/manager/client/src/store/authStore.ts new file mode 100644 index 0000000..9e139fa --- /dev/null +++ b/manager/client/src/store/authStore.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand'; + +export interface AuthPerson { + id: number; + email: string; + fullName: string; + isOperator: boolean; +} + +interface AuthState { + accessToken: string | null; + refreshToken: string | null; + person: AuthPerson | null; + setSession: (session: { accessToken: string; refreshToken: string; person: AuthPerson }) => void; + setAccessToken: (accessToken: string) => void; + clearSession: () => void; +} + +// Tokens kept in memory only, same rationale as the company app: limits XSS exposure at the +// cost of needing to sign in again on a hard page reload. +export const useAuthStore = create((set) => ({ + accessToken: null, + refreshToken: null, + person: null, + setSession: ({ accessToken, refreshToken, person }) => set({ accessToken, refreshToken, person }), + setAccessToken: (accessToken) => set({ accessToken }), + clearSession: () => set({ accessToken: null, refreshToken: null, person: null }), +})); diff --git a/manager/client/src/theme.ts b/manager/client/src/theme.ts new file mode 100644 index 0000000..ab6b818 --- /dev/null +++ b/manager/client/src/theme.ts @@ -0,0 +1,14 @@ +import { createTheme } from '@mui/material/styles'; + +export const theme = createTheme({ + palette: { + mode: 'light', + primary: { main: '#1e3a5f' }, + secondary: { main: '#2e7d32' }, + background: { default: '#f5f6f8' }, + }, + shape: { borderRadius: 6 }, + typography: { + fontFamily: ['Inter', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'].join(','), + }, +}); diff --git a/manager/client/tsconfig.app.json b/manager/client/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/manager/client/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/manager/client/tsconfig.json b/manager/client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/manager/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/manager/client/tsconfig.node.json b/manager/client/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/manager/client/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/manager/client/vite.config.ts b/manager/client/vite.config.ts new file mode 100644 index 0000000..021686a --- /dev/null +++ b/manager/client/vite.config.ts @@ -0,0 +1,10 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 5174, + }, +}) diff --git a/manager/jest.config.js b/manager/jest.config.js new file mode 100644 index 0000000..16d4b53 --- /dev/null +++ b/manager/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testMatch: ['/tests/unit/**/*.spec.ts'], + clearMocks: true, +}; diff --git a/manager/jest.integration.config.js b/manager/jest.integration.config.js new file mode 100644 index 0000000..1a248d3 --- /dev/null +++ b/manager/jest.integration.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testMatch: ['/tests/integration/**/*.spec.ts'], + clearMocks: true, +}; diff --git a/manager/package-lock.json b/manager/package-lock.json new file mode 100644 index 0000000..cecb706 --- /dev/null +++ b/manager/package-lock.json @@ -0,0 +1,7147 @@ +{ + "name": "cfm-account-manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cfm-account-manager", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.20.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "otplib": "^12.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.13", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.7.4", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/parser": "^8.8.0", + "eslint": "^8.57.1", + "jest": "^29.7.0", + "prisma": "^5.20.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.6.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@otplib/core": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", + "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==", + "license": "MIT" + }, + "node_modules/@otplib/plugin-crypto": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", + "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1" + } + }, + "node_modules/@otplib/plugin-thirty-two": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", + "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "thirty-two": "^1.0.2" + } + }, + "node_modules/@otplib/preset-default": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", + "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@otplib/preset-v11": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", + "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/otplib": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", + "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/preset-default": "^12.0.1", + "@otplib/preset-v11": "^12.0.1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/manager/package.json b/manager/package.json new file mode 100644 index 0000000..e729240 --- /dev/null +++ b/manager/package.json @@ -0,0 +1,53 @@ +{ + "name": "cfm-account-manager", + "version": "1.0.0", + "description": "Central Account Manager — login gateway (2FA, Google/Microsoft SSO), company registry, and handoff issuance for Cross Financial Management", + "main": "dist/server.js", + "private": true, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "test": "jest --runInBand", + "test:integration": "jest --runInBand --config jest.integration.config.js", + "lint": "eslint \"src/**/*.ts\"", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "prisma db seed", + "generate-handoff-keys": "ts-node scripts/generateHandoffKeys.ts" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^5.20.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "otplib": "^12.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.13", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.7.4", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/parser": "^8.8.0", + "eslint": "^8.57.1", + "jest": "^29.7.0", + "prisma": "^5.20.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.6.2" + } +} diff --git a/manager/prisma/migrations/20260902134429_init/migration.sql b/manager/prisma/migrations/20260902134429_init/migration.sql new file mode 100644 index 0000000..48bfdc8 --- /dev/null +++ b/manager/prisma/migrations/20260902134429_init/migration.sql @@ -0,0 +1,155 @@ +-- CreateEnum +CREATE TYPE "OAuthProvider" AS ENUM ('GOOGLE', 'MICROSOFT'); + +-- CreateEnum +CREATE TYPE "CompanyStatus" AS ENUM ('PROVISIONING', 'ACTIVE', 'STOPPED', 'FAILED', 'DECOMMISSIONED'); + +-- CreateEnum +CREATE TYPE "ProvisioningAction" AS ENUM ('COMPANY_CREATED', 'DB_CREATED', 'MIGRATIONS_RUN', 'SEED_RUN', 'PM2_STARTED', 'PM2_STOPPED', 'PM2_RESTARTED', 'FRP_PROXY_REGISTERED', 'IMPORT_FILE_GENERATED', 'HANDOFF_ISSUED', 'COMPANY_DECOMMISSIONED'); + +-- CreateTable +CREATE TABLE "Person" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "isOperator" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Person_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PasswordCredential" ( + "id" SERIAL NOT NULL, + "personId" INTEGER NOT NULL, + "passwordHash" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PasswordCredential_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TwoFactorCredential" ( + "id" SERIAL NOT NULL, + "personId" INTEGER NOT NULL, + "totpSecret" TEXT NOT NULL, + "backupCodes" TEXT[], + "isEnabled" BOOLEAN NOT NULL DEFAULT false, + "enabledAt" TIMESTAMP(3), + + CONSTRAINT "TwoFactorCredential_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OAuthAccount" ( + "id" SERIAL NOT NULL, + "personId" INTEGER NOT NULL, + "provider" "OAuthProvider" NOT NULL, + "providerAccountId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "OAuthAccount_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Company" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "status" "CompanyStatus" NOT NULL DEFAULT 'PROVISIONING', + "port" INTEGER, + "databaseName" TEXT, + "databaseUrl" TEXT, + "pm2ProcessName" TEXT, + "frpProxyName" TEXT, + "frpToken" TEXT, + "publicUrl" TEXT, + "provisionedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Company_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PersonCompanyMembership" ( + "id" SERIAL NOT NULL, + "personId" INTEGER NOT NULL, + "companyId" INTEGER NOT NULL, + "companyUserEmail" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PersonCompanyMembership_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProvisioningAuditLog" ( + "id" SERIAL NOT NULL, + "companyId" INTEGER, + "actorId" INTEGER, + "action" "ProvisioningAction" NOT NULL, + "detail" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ProvisioningAuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Person_email_key" ON "Person"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordCredential_personId_key" ON "PasswordCredential"("personId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TwoFactorCredential_personId_key" ON "TwoFactorCredential"("personId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OAuthAccount_provider_providerAccountId_key" ON "OAuthAccount"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_slug_key" ON "Company"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_port_key" ON "Company"("port"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_databaseName_key" ON "Company"("databaseName"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_pm2ProcessName_key" ON "Company"("pm2ProcessName"); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_frpProxyName_key" ON "Company"("frpProxyName"); + +-- CreateIndex +CREATE UNIQUE INDEX "PersonCompanyMembership_personId_companyId_key" ON "PersonCompanyMembership"("personId", "companyId"); + +-- CreateIndex +CREATE INDEX "ProvisioningAuditLog_companyId_idx" ON "ProvisioningAuditLog"("companyId"); + +-- CreateIndex +CREATE INDEX "ProvisioningAuditLog_action_idx" ON "ProvisioningAuditLog"("action"); + +-- AddForeignKey +ALTER TABLE "PasswordCredential" ADD CONSTRAINT "PasswordCredential_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TwoFactorCredential" ADD CONSTRAINT "TwoFactorCredential_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OAuthAccount" ADD CONSTRAINT "OAuthAccount_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PersonCompanyMembership" ADD CONSTRAINT "PersonCompanyMembership_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PersonCompanyMembership" ADD CONSTRAINT "PersonCompanyMembership_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProvisioningAuditLog" ADD CONSTRAINT "ProvisioningAuditLog_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProvisioningAuditLog" ADD CONSTRAINT "ProvisioningAuditLog_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/manager/prisma/migrations/migration_lock.toml b/manager/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/manager/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/manager/prisma/schema.prisma b/manager/prisma/schema.prisma new file mode 100644 index 0000000..78388b3 --- /dev/null +++ b/manager/prisma/schema.prisma @@ -0,0 +1,129 @@ +// Central Account Manager — a platform-wide identity store (Person) and company registry, +// fully separate from any single company's own isolated database and User table. +// See the implementation plan, "Phase A — Account Manager: foundation & handoff". + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Person { + id Int @id @default(autoincrement()) + email String @unique + fullName String + isActive Boolean @default(true) + isOperator Boolean @default(false) + credential PasswordCredential? + twoFactor TwoFactorCredential? + oauthAccounts OAuthAccount[] + memberships PersonCompanyMembership[] + auditLogs ProvisioningAuditLog[] + createdAt DateTime @default(now()) +} + +model PasswordCredential { + id Int @id @default(autoincrement()) + personId Int @unique + person Person @relation(fields: [personId], references: [id]) + passwordHash String + updatedAt DateTime @updatedAt +} + +model TwoFactorCredential { + id Int @id @default(autoincrement()) + personId Int @unique + person Person @relation(fields: [personId], references: [id]) + totpSecret String + backupCodes String[] // hashed, one-time use + isEnabled Boolean @default(false) + enabledAt DateTime? +} + +enum OAuthProvider { + GOOGLE + MICROSOFT +} + +model OAuthAccount { + id Int @id @default(autoincrement()) + personId Int + person Person @relation(fields: [personId], references: [id]) + provider OAuthProvider + providerAccountId String + email String + createdAt DateTime @default(now()) + + @@unique([provider, providerAccountId]) +} + +enum CompanyStatus { + PROVISIONING + ACTIVE + STOPPED + FAILED + DECOMMISSIONED +} + +model Company { + id Int @id @default(autoincrement()) + name String + slug String @unique + status CompanyStatus @default(PROVISIONING) + port Int? @unique + databaseName String? @unique + databaseUrl String? // plain column for now — see plan's "later hardening" note + pm2ProcessName String? @unique + frpProxyName String? @unique + frpToken String? + publicUrl String? + provisionedAt DateTime? + memberships PersonCompanyMembership[] + auditLogs ProvisioningAuditLog[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model PersonCompanyMembership { + id Int @id @default(autoincrement()) + personId Int + person Person @relation(fields: [personId], references: [id]) + companyId Int + company Company @relation(fields: [companyId], references: [id]) + companyUserEmail String? // override; defaults to Person.email when resolving the company-local User + isActive Boolean @default(true) + createdAt DateTime @default(now()) + + @@unique([personId, companyId]) +} + +enum ProvisioningAction { + COMPANY_CREATED + DB_CREATED + MIGRATIONS_RUN + SEED_RUN + PM2_STARTED + PM2_STOPPED + PM2_RESTARTED + FRP_PROXY_REGISTERED + IMPORT_FILE_GENERATED + HANDOFF_ISSUED + COMPANY_DECOMMISSIONED +} + +model ProvisioningAuditLog { + id Int @id @default(autoincrement()) + companyId Int? + company Company? @relation(fields: [companyId], references: [id]) + actorId Int? + actor Person? @relation(fields: [actorId], references: [id]) + action ProvisioningAction + detail Json? + createdAt DateTime @default(now()) + + @@index([companyId]) + @@index([action]) +} diff --git a/manager/prisma/seed.ts b/manager/prisma/seed.ts new file mode 100644 index 0000000..1bc59d7 --- /dev/null +++ b/manager/prisma/seed.ts @@ -0,0 +1,53 @@ +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Seeding operator Person...'); + const operatorEmail = 'joosth2002@icloud.com'; + const passwordHash = await bcrypt.hash('ChangeMe123!', 12); + + const operator = await prisma.person.upsert({ + where: { email: operatorEmail }, + create: { email: operatorEmail, fullName: 'System Administrator', isOperator: true }, + update: { isOperator: true }, + }); + + await prisma.passwordCredential.upsert({ + where: { personId: operator.id }, + create: { personId: operator.id, passwordHash }, + update: {}, + }); + + console.log('Seeding the one manually-provisioned company (Phase A)...'); + const company = await prisma.company.upsert({ + where: { slug: 'demo' }, + create: { + name: 'Demo Company', + slug: 'demo', + status: 'ACTIVE', + port: 5000, + publicUrl: 'http://localhost:5173', + provisionedAt: new Date(), + }, + update: { status: 'ACTIVE', publicUrl: 'http://localhost:5173' }, + }); + + await prisma.personCompanyMembership.upsert({ + where: { personId_companyId: { personId: operator.id, companyId: company.id } }, + create: { personId: operator.id, companyId: company.id }, + update: { isActive: true }, + }); + + console.log(`Seed complete. Sign in as ${operatorEmail} with temporary password "ChangeMe123!" — change it after first login.`); +} + +main() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/manager/scripts/generateHandoffKeys.ts b/manager/scripts/generateHandoffKeys.ts new file mode 100644 index 0000000..d0ec2bf --- /dev/null +++ b/manager/scripts/generateHandoffKeys.ts @@ -0,0 +1,40 @@ +/** + * Generates the RSA keypair used to sign (Manager) and verify (every company app) handoff + * tokens. Run once per environment: npm run generate-handoff-keys + */ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; + +const keysDir = path.resolve(__dirname, '..', 'keys'); + +function main() { + if (!fs.existsSync(keysDir)) { + fs.mkdirSync(keysDir, { recursive: true }); + } + + const privateKeyPath = path.join(keysDir, 'handoff-private.pem'); + const publicKeyPath = path.join(keysDir, 'handoff-public.pem'); + + if (fs.existsSync(privateKeyPath) || fs.existsSync(publicKeyPath)) { + console.log('Handoff keypair already exists at manager/keys/ — not overwriting.'); + console.log(` Private: ${privateKeyPath}`); + console.log(` Public: ${publicKeyPath}`); + return; + } + + const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + fs.writeFileSync(privateKeyPath, privateKey, { mode: 0o600 }); + fs.writeFileSync(publicKeyPath, publicKey); + + console.log('Generated handoff RS256 keypair:'); + console.log(` Private (Manager only, never share): ${privateKeyPath}`); + console.log(` Public (copy into every company app's HANDOFF_PUBLIC_KEYS): ${publicKeyPath}`); +} + +main(); diff --git a/manager/src/app.ts b/manager/src/app.ts new file mode 100644 index 0000000..b5d8a5f --- /dev/null +++ b/manager/src/app.ts @@ -0,0 +1,24 @@ +import cors from 'cors'; +import express, { Express } from 'express'; +import helmet from 'helmet'; +import { errorHandler } from './middleware/errorHandler'; +import { authRouter } from './modules/auth/auth.routes'; +import { companiesRouter } from './modules/companies/companies.routes'; + +export function createApp(): Express { + const app = express(); + + app.use(helmet()); + app.use(cors()); + app.use(express.json()); + + app.get('/health', (req, res) => res.json({ status: 'ok' })); + + app.use('/api/auth', authRouter); + app.use('/api/companies', companiesRouter); + + app.use((req, res) => res.status(404).json({ error: 'Not found' })); + app.use(errorHandler); + + return app; +} diff --git a/manager/src/config/env.ts b/manager/src/config/env.ts new file mode 100644 index 0000000..d56534d --- /dev/null +++ b/manager/src/config/env.ts @@ -0,0 +1,37 @@ +import 'dotenv/config'; +import path from 'path'; + +function required(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +export const env = { + port: Number(process.env.PORT ?? 5100), + databaseUrl: required('DATABASE_URL'), + + jwtAccessSecret: required('JWT_ACCESS_SECRET'), + jwtRefreshSecret: required('JWT_REFRESH_SECRET'), + jwtAccessTtl: process.env.JWT_ACCESS_TTL ?? '15m', + jwtRefreshTtl: process.env.JWT_REFRESH_TTL ?? '7d', + + handoffPrivateKeyPath: path.resolve(process.cwd(), process.env.HANDOFF_PRIVATE_KEY_PATH ?? './keys/handoff-private.pem'), + handoffPublicKeyPath: path.resolve(process.cwd(), process.env.HANDOFF_PUBLIC_KEY_PATH ?? './keys/handoff-public.pem'), + handoffTokenTtlSeconds: Number(process.env.HANDOFF_TOKEN_TTL_SECONDS ?? 90), + + totpIssuer: process.env.TOTP_ISSUER ?? 'Cross Financial Manager', + + googleClientId: process.env.GOOGLE_CLIENT_ID ?? '', + googleClientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '', + googleRedirectUri: process.env.GOOGLE_REDIRECT_URI ?? '', + + microsoftClientId: process.env.MICROSOFT_CLIENT_ID ?? '', + microsoftClientSecret: process.env.MICROSOFT_CLIENT_SECRET ?? '', + microsoftTenant: process.env.MICROSOFT_TENANT ?? 'common', + microsoftRedirectUri: process.env.MICROSOFT_REDIRECT_URI ?? '', + + managerWebUrl: process.env.MANAGER_WEB_URL ?? 'http://localhost:5174', +}; diff --git a/manager/src/lib/asyncHandler.ts b/manager/src/lib/asyncHandler.ts new file mode 100644 index 0000000..118019e --- /dev/null +++ b/manager/src/lib/asyncHandler.ts @@ -0,0 +1,10 @@ +import { NextFunction, Request, Response } from 'express'; + +type Handler = (req: Request, res: Response, next: NextFunction) => Promise | void; + +/** Wraps an async route/controller handler so a rejected promise reaches errorHandler via next(). */ +export function asyncHandler(handler: Handler) { + return (req: Request, res: Response, next: NextFunction): void => { + Promise.resolve(handler(req, res, next)).catch(next); + }; +} diff --git a/manager/src/lib/handoffToken.ts b/manager/src/lib/handoffToken.ts new file mode 100644 index 0000000..aae29c3 --- /dev/null +++ b/manager/src/lib/handoffToken.ts @@ -0,0 +1,39 @@ +import fs from 'fs'; +import crypto from 'crypto'; +import jwt from 'jsonwebtoken'; +import { env } from '../config/env'; + +export interface HandoffTokenClaims { + sub: string; // resolved company-local email + pid: number; // Person.id + cid: number; // Company.id + companySlug: string; +} + +let cachedPrivateKey: string | null = null; + +function loadPrivateKey(): string { + if (cachedPrivateKey) return cachedPrivateKey; + if (!fs.existsSync(env.handoffPrivateKeyPath)) { + throw new Error( + `Handoff private key not found at ${env.handoffPrivateKeyPath}. Run "npm run generate-handoff-keys" first.`, + ); + } + cachedPrivateKey = fs.readFileSync(env.handoffPrivateKeyPath, 'utf8'); + return cachedPrivateKey; +} + +/** + * Mints a short-lived, single-use RS256 handoff token. Only the Manager ever holds the private + * key — every company app holds only the public key and can verify but never mint, so + * compromising one isolated company instance can't be used to forge a session for any other. + */ +export function signHandoffToken(claims: HandoffTokenClaims): { token: string; jti: string } { + const jti = crypto.randomUUID(); + const token = jwt.sign(claims, loadPrivateKey(), { + algorithm: 'RS256', + expiresIn: env.handoffTokenTtlSeconds, + jwtid: jti, + }); + return { token, jti }; +} diff --git a/manager/src/lib/jwt.ts b/manager/src/lib/jwt.ts new file mode 100644 index 0000000..8f572b5 --- /dev/null +++ b/manager/src/lib/jwt.ts @@ -0,0 +1,52 @@ +import jwt, { SignOptions } from 'jsonwebtoken'; +import { env } from '../config/env'; + +type Purpose = 'access' | 'refresh' | '2fa_challenge'; + +interface SessionTokenPayload { + sub: string; + purpose: Purpose; +} + +function sign(personId: number, purpose: Purpose, secret: string, ttl: string | number): string { + const options: SignOptions = { expiresIn: ttl as SignOptions['expiresIn'] }; + return jwt.sign({ sub: String(personId), purpose } satisfies SessionTokenPayload, secret, options); +} + +/** Verifies the token AND that it was issued for the expected purpose — a 2FA-challenge token + * must never be accepted as a real access token, and vice versa. */ +function verify(token: string, secret: string, expectedPurpose: Purpose): number { + const payload = jwt.verify(token, secret) as unknown as SessionTokenPayload; + if (payload.purpose !== expectedPurpose) { + throw new Error(`Expected a ${expectedPurpose} token, got ${payload.purpose}`); + } + return Number(payload.sub); +} + +export const signAccessToken = (personId: number): string => sign(personId, 'access', env.jwtAccessSecret, env.jwtAccessTtl); +export const signRefreshToken = (personId: number): string => sign(personId, 'refresh', env.jwtRefreshSecret, env.jwtRefreshTtl); +export const verifyAccessToken = (token: string): number => verify(token, env.jwtAccessSecret, 'access'); +export const verifyRefreshToken = (token: string): number => verify(token, env.jwtRefreshSecret, 'refresh'); + +/** A short-lived token identifying a person who passed password auth but still owes a 2FA code. */ +export const signTwoFactorChallengeToken = (personId: number): string => + sign(personId, '2fa_challenge', env.jwtAccessSecret, '5m'); +export const verifyTwoFactorChallengeToken = (token: string): number => verify(token, env.jwtAccessSecret, '2fa_challenge'); + +/** + * CSRF protection for the OAuth authorize/callback round trip, without needing server-side + * session storage: a self-verifying signed nonce, not tied to any person yet. + */ +export function signOAuthStateToken(): string { + return jwt.sign({ purpose: 'oauth_state', nonce: Math.random().toString(36).slice(2) }, env.jwtAccessSecret, { + expiresIn: '10m', + }); +} +export function verifyOAuthStateToken(token: string): boolean { + try { + const payload = jwt.verify(token, env.jwtAccessSecret) as { purpose?: string }; + return payload.purpose === 'oauth_state'; + } catch { + return false; + } +} diff --git a/manager/src/lib/oauth.ts b/manager/src/lib/oauth.ts new file mode 100644 index 0000000..252b148 --- /dev/null +++ b/manager/src/lib/oauth.ts @@ -0,0 +1,102 @@ +import { env } from '../config/env'; +import { signOAuthStateToken, verifyOAuthStateToken } from './jwt'; + +export interface OAuthProfile { + providerAccountId: string; + email: string; + name: string; +} + +export class OAuthStateError extends Error { + constructor() { + super('Invalid or expired OAuth state'); + this.name = 'OAuthStateError'; + } +} + +// --------------------------------------------------------------------------- +// Google +// --------------------------------------------------------------------------- + +export function googleAuthorizeUrl(): string { + const state = signOAuthStateToken(); + const params = new URLSearchParams({ + client_id: env.googleClientId, + redirect_uri: env.googleRedirectUri, + response_type: 'code', + scope: 'openid email profile', + state, + access_type: 'online', + prompt: 'select_account', + }); + return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; +} + +export async function exchangeGoogleCode(code: string, state: string): Promise { + if (!verifyOAuthStateToken(state)) throw new OAuthStateError(); + + const tokenResponse = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: env.googleClientId, + client_secret: env.googleClientSecret, + code, + redirect_uri: env.googleRedirectUri, + grant_type: 'authorization_code', + }), + }); + if (!tokenResponse.ok) throw new Error(`Google token exchange failed: ${await tokenResponse.text()}`); + const { access_token: accessToken } = (await tokenResponse.json()) as { access_token: string }; + + const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!userInfoResponse.ok) throw new Error(`Google userinfo fetch failed: ${await userInfoResponse.text()}`); + const profile = (await userInfoResponse.json()) as { sub: string; email: string; name?: string }; + + return { providerAccountId: profile.sub, email: profile.email, name: profile.name ?? profile.email }; +} + +// --------------------------------------------------------------------------- +// Microsoft (Azure AD v2.0 endpoint) +// --------------------------------------------------------------------------- + +export function microsoftAuthorizeUrl(): string { + const state = signOAuthStateToken(); + const params = new URLSearchParams({ + client_id: env.microsoftClientId, + redirect_uri: env.microsoftRedirectUri, + response_type: 'code', + scope: 'openid email profile', + state, + response_mode: 'query', + }); + return `https://login.microsoftonline.com/${env.microsoftTenant}/oauth2/v2.0/authorize?${params.toString()}`; +} + +export async function exchangeMicrosoftCode(code: string, state: string): Promise { + if (!verifyOAuthStateToken(state)) throw new OAuthStateError(); + + const tokenResponse = await fetch(`https://login.microsoftonline.com/${env.microsoftTenant}/oauth2/v2.0/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: env.microsoftClientId, + client_secret: env.microsoftClientSecret, + code, + redirect_uri: env.microsoftRedirectUri, + grant_type: 'authorization_code', + }), + }); + if (!tokenResponse.ok) throw new Error(`Microsoft token exchange failed: ${await tokenResponse.text()}`); + const { access_token: accessToken } = (await tokenResponse.json()) as { access_token: string }; + + const userInfoResponse = await fetch('https://graph.microsoft.com/oidc/userinfo', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!userInfoResponse.ok) throw new Error(`Microsoft userinfo fetch failed: ${await userInfoResponse.text()}`); + const profile = (await userInfoResponse.json()) as { sub: string; email: string; name?: string }; + + return { providerAccountId: profile.sub, email: profile.email, name: profile.name ?? profile.email }; +} diff --git a/manager/src/lib/prisma.ts b/manager/src/lib/prisma.ts new file mode 100644 index 0000000..769cdc6 --- /dev/null +++ b/manager/src/lib/prisma.ts @@ -0,0 +1,12 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // eslint-disable-next-line no-var + var __managerPrisma__: PrismaClient | undefined; +} + +export const prisma = global.__managerPrisma__ ?? new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + global.__managerPrisma__ = prisma; +} diff --git a/manager/src/lib/totp.ts b/manager/src/lib/totp.ts new file mode 100644 index 0000000..79186b3 --- /dev/null +++ b/manager/src/lib/totp.ts @@ -0,0 +1,40 @@ +import { authenticator } from 'otplib'; +import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; +import { env } from '../config/env'; + +export function generateTotpSecret(): string { + return authenticator.generateSecret(); +} + +export function totpKeyUri(accountEmail: string, secret: string): string { + return authenticator.keyuri(accountEmail, env.totpIssuer, secret); +} + +export function verifyTotpToken(token: string, secret: string): boolean { + try { + return authenticator.check(token, secret); + } catch { + return false; + } +} + +const BACKUP_CODE_COUNT = 10; + +/** Returns { plainCodes, hashedCodes } — plainCodes shown to the person once, hashedCodes stored. */ +export async function generateBackupCodes(): Promise<{ plainCodes: string[]; hashedCodes: string[] }> { + const plainCodes = Array.from({ length: BACKUP_CODE_COUNT }, () => crypto.randomBytes(5).toString('hex')); + const hashedCodes = await Promise.all(plainCodes.map((code) => bcrypt.hash(code, 10))); + return { plainCodes, hashedCodes }; +} + +/** Returns the index of the matching hashed backup code, or -1 if none matched. */ +export async function findMatchingBackupCode(code: string, hashedCodes: string[]): Promise { + for (let i = 0; i < hashedCodes.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + if (await bcrypt.compare(code, hashedCodes[i])) { + return i; + } + } + return -1; +} diff --git a/manager/src/middleware/auth.ts b/manager/src/middleware/auth.ts new file mode 100644 index 0000000..a6bcbbc --- /dev/null +++ b/manager/src/middleware/auth.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from 'express'; +import { prisma } from '../lib/prisma'; +import { verifyAccessToken } from '../lib/jwt'; +import { AuthPerson } from '../types/express'; + +export async function loadAuthPerson(personId: number): Promise { + const person = await prisma.person.findUnique({ where: { id: personId } }); + if (!person || !person.isActive) return null; + return { id: person.id, email: person.email, fullName: person.fullName, isOperator: person.isOperator }; +} + +export async function requireAuth(req: Request, res: Response, next: NextFunction): Promise { + const header = req.headers.authorization; + if (!header?.startsWith('Bearer ')) { + res.status(401).json({ error: 'Missing bearer token' }); + return; + } + + try { + const personId = verifyAccessToken(header.slice('Bearer '.length)); + const person = await loadAuthPerson(personId); + if (!person) { + res.status(401).json({ error: 'Person not found or inactive' }); + return; + } + req.person = person; + next(); + } catch { + res.status(401).json({ error: 'Invalid or expired token' }); + } +} diff --git a/manager/src/middleware/errorHandler.ts b/manager/src/middleware/errorHandler.ts new file mode 100644 index 0000000..bc6658e --- /dev/null +++ b/manager/src/middleware/errorHandler.ts @@ -0,0 +1,20 @@ +import { NextFunction, Request, Response } from 'express'; +import { Prisma } from '@prisma/client'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function errorHandler(error: unknown, req: Request, res: Response, next: NextFunction): void { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + res.status(409).json({ error: 'A record with this value already exists', meta: error.meta }); + return; + } + if (error.code === 'P2025') { + res.status(404).json({ error: 'Record not found' }); + return; + } + } + + // eslint-disable-next-line no-console + console.error(error); + res.status(500).json({ error: 'Internal server error' }); +} diff --git a/manager/src/middleware/requireOperator.ts b/manager/src/middleware/requireOperator.ts new file mode 100644 index 0000000..0f884c6 --- /dev/null +++ b/manager/src/middleware/requireOperator.ts @@ -0,0 +1,13 @@ +import { NextFunction, Request, Response } from 'express'; + +export function requireOperator(req: Request, res: Response, next: NextFunction): void { + if (!req.person) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + if (!req.person.isOperator) { + res.status(403).json({ error: 'Operator access required' }); + return; + } + next(); +} diff --git a/manager/src/middleware/validate.ts b/manager/src/middleware/validate.ts new file mode 100644 index 0000000..4e402fb --- /dev/null +++ b/manager/src/middleware/validate.ts @@ -0,0 +1,19 @@ +import { NextFunction, Request, Response } from 'express'; +import { AnyZodObject, ZodError } from 'zod'; + +type Section = 'body' | 'query' | 'params'; + +export function validate(schema: AnyZodObject, section: Section = 'body') { + return (req: Request, res: Response, next: NextFunction): void => { + try { + req[section] = schema.parse(req[section]); + next(); + } catch (error) { + if (error instanceof ZodError) { + res.status(400).json({ error: 'Validation failed', details: error.flatten() }); + return; + } + next(error); + } + }; +} diff --git a/manager/src/modules/auth/auth.controller.ts b/manager/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..2136a61 --- /dev/null +++ b/manager/src/modules/auth/auth.controller.ts @@ -0,0 +1,122 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as authService from './auth.service'; +import { googleAuthorizeUrl, microsoftAuthorizeUrl, exchangeGoogleCode, exchangeMicrosoftCode } from '../../lib/oauth'; +import { env } from '../../config/env'; + +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); + +export const twoFactorVerifySchema = z.object({ + twoFactorToken: z.string().min(1), + code: z.string().min(1), +}); + +export const twoFactorEnableSchema = z.object({ + code: z.string().min(1), +}); + +export const refreshSchema = z.object({ + refreshToken: z.string().min(1), +}); + +export async function loginHandler(req: Request, res: Response): Promise { + try { + const { email, password } = req.body as z.infer; + res.json(await authService.login(email, password)); + } catch (error) { + if (error instanceof authService.InvalidCredentialsError) { + res.status(401).json({ error: error.message }); + return; + } + throw error; + } +} + +export async function twoFactorVerifyHandler(req: Request, res: Response): Promise { + try { + const { twoFactorToken, code } = req.body as z.infer; + res.json(await authService.completeTwoFactorChallenge(twoFactorToken, code)); + } catch (error) { + if (error instanceof authService.InvalidTwoFactorCodeError) { + res.status(401).json({ error: error.message }); + return; + } + throw error; + } +} + +export function refreshHandler(req: Request, res: Response): void { + try { + const { refreshToken } = req.body as z.infer; + res.json({ accessToken: authService.refreshSessionToken(refreshToken) }); + } catch { + res.status(401).json({ error: 'Invalid or expired refresh token' }); + } +} + +export function meHandler(req: Request, res: Response): void { + res.json({ person: req.person }); +} + +export async function twoFactorEnrollHandler(req: Request, res: Response): Promise { + const enrollment = await authService.startTwoFactorEnrollment(req.person!.id, req.person!.email); + res.json(enrollment); +} + +export async function twoFactorEnableHandler(req: Request, res: Response): Promise { + try { + const { code } = req.body as z.infer; + const backupCodes = await authService.confirmTwoFactorEnrollment(req.person!.id, code); + res.json({ backupCodes }); + } catch (error) { + if (error instanceof authService.InvalidTwoFactorCodeError) { + res.status(401).json({ error: error.message }); + return; + } + throw error; + } +} + +function redirectWithLoginResult(res: Response, result: Awaited>): void { + const url = new URL('/oauth/callback', env.managerWebUrl); + if (result.requiresTwoFactor) { + url.searchParams.set('twoFactorToken', result.twoFactorToken); + } else { + url.searchParams.set('accessToken', result.accessToken); + url.searchParams.set('refreshToken', result.refreshToken); + } + res.redirect(url.toString()); +} + +export function googleAuthorizeHandler(req: Request, res: Response): void { + res.redirect(googleAuthorizeUrl()); +} + +export async function googleCallbackHandler(req: Request, res: Response): Promise { + const { code, state } = req.query as { code?: string; state?: string }; + if (!code || !state) { + res.status(400).json({ error: 'Missing code or state' }); + return; + } + const profile = await exchangeGoogleCode(code, state); + const result = await authService.handleOAuthCallback('GOOGLE', profile); + redirectWithLoginResult(res, result); +} + +export function microsoftAuthorizeHandler(req: Request, res: Response): void { + res.redirect(microsoftAuthorizeUrl()); +} + +export async function microsoftCallbackHandler(req: Request, res: Response): Promise { + const { code, state } = req.query as { code?: string; state?: string }; + if (!code || !state) { + res.status(400).json({ error: 'Missing code or state' }); + return; + } + const profile = await exchangeMicrosoftCode(code, state); + const result = await authService.handleOAuthCallback('MICROSOFT', profile); + redirectWithLoginResult(res, result); +} diff --git a/manager/src/modules/auth/auth.routes.ts b/manager/src/modules/auth/auth.routes.ts new file mode 100644 index 0000000..0a62820 --- /dev/null +++ b/manager/src/modules/auth/auth.routes.ts @@ -0,0 +1,35 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + googleAuthorizeHandler, + googleCallbackHandler, + loginHandler, + loginSchema, + meHandler, + microsoftAuthorizeHandler, + microsoftCallbackHandler, + refreshHandler, + refreshSchema, + twoFactorEnableHandler, + twoFactorEnableSchema, + twoFactorEnrollHandler, + twoFactorVerifyHandler, + twoFactorVerifySchema, +} from './auth.controller'; + +export const authRouter = Router(); + +authRouter.post('/login', validate(loginSchema), asyncHandler(loginHandler)); +authRouter.post('/2fa/verify', validate(twoFactorVerifySchema), asyncHandler(twoFactorVerifyHandler)); +authRouter.post('/refresh', validate(refreshSchema), refreshHandler); +authRouter.get('/me', requireAuth, meHandler); + +authRouter.post('/2fa/enroll', requireAuth, asyncHandler(twoFactorEnrollHandler)); +authRouter.post('/2fa/enable', requireAuth, validate(twoFactorEnableSchema), asyncHandler(twoFactorEnableHandler)); + +authRouter.get('/oauth/google', googleAuthorizeHandler); +authRouter.get('/oauth/google/callback', asyncHandler(googleCallbackHandler)); +authRouter.get('/oauth/microsoft', microsoftAuthorizeHandler); +authRouter.get('/oauth/microsoft/callback', asyncHandler(microsoftCallbackHandler)); diff --git a/manager/src/modules/auth/auth.service.ts b/manager/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..9f8306d --- /dev/null +++ b/manager/src/modules/auth/auth.service.ts @@ -0,0 +1,145 @@ +import bcrypt from 'bcryptjs'; +import { OAuthProvider } from '@prisma/client'; +import { prisma } from '../../lib/prisma'; +import { + signAccessToken, + signRefreshToken, + signTwoFactorChallengeToken, + verifyRefreshToken, + verifyTwoFactorChallengeToken, +} from '../../lib/jwt'; +import { findMatchingBackupCode, generateBackupCodes, generateTotpSecret, totpKeyUri, verifyTotpToken } from '../../lib/totp'; +import { loadAuthPerson } from '../../middleware/auth'; +import { OAuthProfile } from '../../lib/oauth'; + +export class InvalidCredentialsError extends Error { + constructor() { + super('Invalid email or password'); + this.name = 'InvalidCredentialsError'; + } +} + +export class InvalidTwoFactorCodeError extends Error { + constructor() { + super('Invalid two-factor code'); + this.name = 'InvalidTwoFactorCodeError'; + } +} + +type LoginResult = + | { requiresTwoFactor: true; twoFactorToken: string } + | { requiresTwoFactor: false; accessToken: string; refreshToken: string; person: NonNullable>> }; + +async function issueSessionOrChallenge(personId: number): Promise { + const twoFactor = await prisma.twoFactorCredential.findUnique({ where: { personId } }); + if (twoFactor?.isEnabled) { + return { requiresTwoFactor: true, twoFactorToken: signTwoFactorChallengeToken(personId) }; + } + + const person = await loadAuthPerson(personId); + if (!person) throw new InvalidCredentialsError(); + return { + requiresTwoFactor: false, + accessToken: signAccessToken(personId), + refreshToken: signRefreshToken(personId), + person, + }; +} + +export async function login(email: string, password: string): Promise { + const person = await prisma.person.findUnique({ where: { email }, include: { credential: true } }); + if (!person || !person.isActive || !person.credential) { + throw new InvalidCredentialsError(); + } + + const passwordMatches = await bcrypt.compare(password, person.credential.passwordHash); + if (!passwordMatches) { + throw new InvalidCredentialsError(); + } + + return issueSessionOrChallenge(person.id); +} + +export async function completeTwoFactorChallenge(twoFactorToken: string, code: string) { + let personId: number; + try { + personId = verifyTwoFactorChallengeToken(twoFactorToken); + } catch { + throw new InvalidTwoFactorCodeError(); + } + + const twoFactor = await prisma.twoFactorCredential.findUnique({ where: { personId } }); + if (!twoFactor?.isEnabled) { + throw new InvalidTwoFactorCodeError(); + } + + if (verifyTotpToken(code, twoFactor.totpSecret)) { + return finalizeSession(personId); + } + + const backupIndex = await findMatchingBackupCode(code, twoFactor.backupCodes); + if (backupIndex === -1) { + throw new InvalidTwoFactorCodeError(); + } + + const remainingCodes = twoFactor.backupCodes.filter((_, index) => index !== backupIndex); + await prisma.twoFactorCredential.update({ where: { personId }, data: { backupCodes: remainingCodes } }); + + return finalizeSession(personId); +} + +async function finalizeSession(personId: number) { + const person = await loadAuthPerson(personId); + if (!person) throw new InvalidTwoFactorCodeError(); + return { accessToken: signAccessToken(personId), refreshToken: signRefreshToken(personId), person }; +} + +export function refreshSessionToken(refreshToken: string): string { + const personId = verifyRefreshToken(refreshToken); + return signAccessToken(personId); +} + +export async function startTwoFactorEnrollment(personId: number, email: string) { + const secret = generateTotpSecret(); + await prisma.twoFactorCredential.upsert({ + where: { personId }, + create: { personId, totpSecret: secret, backupCodes: [], isEnabled: false }, + update: { totpSecret: secret, isEnabled: false, backupCodes: [] }, + }); + return { secret, otpauthUrl: totpKeyUri(email, secret) }; +} + +export async function confirmTwoFactorEnrollment(personId: number, code: string): Promise { + const twoFactor = await prisma.twoFactorCredential.findUnique({ where: { personId } }); + if (!twoFactor || !verifyTotpToken(code, twoFactor.totpSecret)) { + throw new InvalidTwoFactorCodeError(); + } + const { plainCodes, hashedCodes } = await generateBackupCodes(); + await prisma.twoFactorCredential.update({ + where: { personId }, + data: { isEnabled: true, enabledAt: new Date(), backupCodes: hashedCodes }, + }); + return plainCodes; +} + +export async function handleOAuthCallback(provider: OAuthProvider, profile: OAuthProfile): Promise { + const existingAccount = await prisma.oAuthAccount.findUnique({ + where: { provider_providerAccountId: { provider, providerAccountId: profile.providerAccountId } }, + }); + + if (existingAccount) { + return issueSessionOrChallenge(existingAccount.personId); + } + + const person = await prisma.person.upsert({ + where: { email: profile.email }, + create: { email: profile.email, fullName: profile.name }, + update: {}, + }); + + await prisma.oAuthAccount.create({ + data: { personId: person.id, provider, providerAccountId: profile.providerAccountId, email: profile.email }, + }); + + return issueSessionOrChallenge(person.id); +} diff --git a/manager/src/modules/companies/companies.controller.ts b/manager/src/modules/companies/companies.controller.ts new file mode 100644 index 0000000..04e1c41 --- /dev/null +++ b/manager/src/modules/companies/companies.controller.ts @@ -0,0 +1,32 @@ +import { Request, Response } from 'express'; +import * as companiesService from './companies.service'; + +export async function listMyCompaniesHandler(req: Request, res: Response): Promise { + const memberships = await companiesService.listMyCompanies(req.person!.id); + res.json( + memberships.map((membership) => ({ + companyId: membership.company.id, + name: membership.company.name, + slug: membership.company.slug, + status: membership.company.status, + })), + ); +} + +export async function handoffHandler(req: Request, res: Response): Promise { + try { + const companyId = Number(req.params.id); + const result = await companiesService.issueHandoff(req.person!.id, companyId); + res.json(result); + } catch (error) { + if (error instanceof companiesService.MembershipNotFoundError) { + res.status(403).json({ error: error.message }); + return; + } + if (error instanceof companiesService.CompanyNotReadyError) { + res.status(409).json({ error: error.message }); + return; + } + throw error; + } +} diff --git a/manager/src/modules/companies/companies.routes.ts b/manager/src/modules/companies/companies.routes.ts new file mode 100644 index 0000000..b659664 --- /dev/null +++ b/manager/src/modules/companies/companies.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { handoffHandler, listMyCompaniesHandler } from './companies.controller'; + +export const companiesRouter = Router(); + +companiesRouter.use(requireAuth); + +companiesRouter.get('/', asyncHandler(listMyCompaniesHandler)); +companiesRouter.post('/:id/handoff', asyncHandler(handoffHandler)); diff --git a/manager/src/modules/companies/companies.service.ts b/manager/src/modules/companies/companies.service.ts new file mode 100644 index 0000000..6a820a7 --- /dev/null +++ b/manager/src/modules/companies/companies.service.ts @@ -0,0 +1,60 @@ +import { prisma } from '../../lib/prisma'; +import { signHandoffToken } from '../../lib/handoffToken'; + +export class MembershipNotFoundError extends Error { + constructor() { + super('You do not have access to this company'); + this.name = 'MembershipNotFoundError'; + } +} + +export class CompanyNotReadyError extends Error { + constructor() { + super('This company is not currently reachable'); + this.name = 'CompanyNotReadyError'; + } +} + +export function listMyCompanies(personId: number) { + return prisma.personCompanyMembership.findMany({ + where: { personId, isActive: true }, + include: { company: true }, + orderBy: { company: { name: 'asc' } }, + }); +} + +export async function issueHandoff(personId: number, companyId: number) { + const [person, membership] = await Promise.all([ + prisma.person.findUniqueOrThrow({ where: { id: personId } }), + prisma.personCompanyMembership.findUnique({ + where: { personId_companyId: { personId, companyId } }, + include: { company: true }, + }), + ]); + + if (!membership || !membership.isActive) { + throw new MembershipNotFoundError(); + } + if (!membership.company.publicUrl || membership.company.status !== 'ACTIVE') { + throw new CompanyNotReadyError(); + } + + const resolvedEmail = membership.companyUserEmail ?? person.email; + const { token, jti } = signHandoffToken({ + sub: resolvedEmail, + pid: person.id, + cid: membership.company.id, + companySlug: membership.company.slug, + }); + + await prisma.provisioningAuditLog.create({ + data: { + companyId: membership.company.id, + actorId: person.id, + action: 'HANDOFF_ISSUED', + detail: { jti, resolvedEmail }, + }, + }); + + return { token, redirectUrl: membership.company.publicUrl }; +} diff --git a/manager/src/server.ts b/manager/src/server.ts new file mode 100644 index 0000000..703b7ed --- /dev/null +++ b/manager/src/server.ts @@ -0,0 +1,9 @@ +import { createApp } from './app'; +import { env } from './config/env'; + +const app = createApp(); + +app.listen(env.port, () => { + // eslint-disable-next-line no-console + console.log(`Account Manager API listening on http://localhost:${env.port}`); +}); diff --git a/manager/src/types/express.d.ts b/manager/src/types/express.d.ts new file mode 100644 index 0000000..7f09a10 --- /dev/null +++ b/manager/src/types/express.d.ts @@ -0,0 +1,14 @@ +export interface AuthPerson { + id: number; + email: string; + fullName: string; + isOperator: boolean; +} + +declare global { + namespace Express { + interface Request { + person?: AuthPerson; + } + } +} diff --git a/manager/tests/integration/handoff.spec.ts b/manager/tests/integration/handoff.spec.ts new file mode 100644 index 0000000..8aba6eb --- /dev/null +++ b/manager/tests/integration/handoff.spec.ts @@ -0,0 +1,59 @@ +/** + * Requires the real, migrated, seeded Manager database (see SETUP.md) reachable via DATABASE_URL. + * Run with: npm run test:integration + */ +import request from 'supertest'; +import { createApp } from '../../src/app'; +import { prisma } from '../../src/lib/prisma'; + +const app = createApp(); + +const OPERATOR_EMAIL = 'joosth2002@icloud.com'; +const OPERATOR_PASSWORD = 'ChangeMe123!'; + +describe('Manager auth + handoff (real DB)', () => { + afterAll(async () => { + await prisma.$disconnect(); + }); + + it('logs in with the seeded operator credentials', async () => { + const response = await request(app).post('/api/auth/login').send({ email: OPERATOR_EMAIL, password: OPERATOR_PASSWORD }); + expect(response.status).toBe(200); + expect(response.body.requiresTwoFactor).toBe(false); + expect(response.body.accessToken).toBeDefined(); + expect(response.body.person.email).toBe(OPERATOR_EMAIL); + }); + + it('rejects the wrong password', async () => { + const response = await request(app).post('/api/auth/login').send({ email: OPERATOR_EMAIL, password: 'wrong' }); + expect(response.status).toBe(401); + }); + + it('lists the seeded demo company as a membership and issues a working handoff token', async () => { + const login = await request(app).post('/api/auth/login').send({ email: OPERATOR_EMAIL, password: OPERATOR_PASSWORD }); + const accessToken = login.body.accessToken as string; + + const myCompanies = await request(app).get('/api/companies').set('Authorization', `Bearer ${accessToken}`); + expect(myCompanies.status).toBe(200); + expect(myCompanies.body.length).toBeGreaterThanOrEqual(1); + const demo = myCompanies.body.find((c: { slug: string }) => c.slug === 'demo'); + expect(demo).toBeDefined(); + + const handoff = await request(app) + .post(`/api/companies/${demo.companyId}/handoff`) + .set('Authorization', `Bearer ${accessToken}`); + expect(handoff.status).toBe(200); + expect(handoff.body.token).toBeDefined(); + expect(handoff.body.redirectUrl).toBe('http://localhost:5173'); + }); + + it('refuses a handoff for a company the person has no membership in', async () => { + const login = await request(app).post('/api/auth/login').send({ email: OPERATOR_EMAIL, password: OPERATOR_PASSWORD }); + const accessToken = login.body.accessToken as string; + + const response = await request(app) + .post('/api/companies/999999/handoff') + .set('Authorization', `Bearer ${accessToken}`); + expect(response.status).toBe(403); + }); +}); diff --git a/manager/tests/unit/handoffToken.spec.ts b/manager/tests/unit/handoffToken.spec.ts new file mode 100644 index 0000000..fd796a3 --- /dev/null +++ b/manager/tests/unit/handoffToken.spec.ts @@ -0,0 +1,52 @@ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import jwt from 'jsonwebtoken'; +import { signHandoffToken } from '../../src/lib/handoffToken'; + +const publicKeyPath = path.resolve(__dirname, '../../keys/handoff-public.pem'); +const privateKeyPath = path.resolve(__dirname, '../../keys/handoff-private.pem'); +const publicKey = fs.readFileSync(publicKeyPath, 'utf8'); +const privateKey = fs.readFileSync(privateKeyPath, 'utf8'); + +const claims = { sub: 'person@example.com', pid: 1, cid: 1, companySlug: 'demo' }; + +describe('signHandoffToken', () => { + it('produces an RS256 token whose claims round-trip through the public key', () => { + const { token, jti } = signHandoffToken(claims); + + const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as typeof claims & { jti: string }; + expect(decoded.sub).toBe(claims.sub); + expect(decoded.pid).toBe(claims.pid); + expect(decoded.cid).toBe(claims.cid); + expect(decoded.companySlug).toBe(claims.companySlug); + expect(decoded.jti).toBe(jti); + }); + + it('mints a distinct jti on every call (single-use tokens)', () => { + const first = signHandoffToken(claims); + const second = signHandoffToken(claims); + expect(first.jti).not.toBe(second.jti); + }); + + it('cannot be verified against a different keypair', () => { + const { publicKey: wrongPublicKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + const { token } = signHandoffToken(claims); + expect(() => jwt.verify(token, wrongPublicKey, { algorithms: ['RS256'] })).toThrow(); + }); + + it('rejects a tampered token', () => { + const { token } = signHandoffToken(claims); + const tampered = `${token.slice(0, -4)}abcd`; + expect(() => jwt.verify(tampered, publicKey, { algorithms: ['RS256'] })).toThrow(); + }); + + it('rejects an expired token', () => { + const expired = jwt.sign(claims, privateKey, { algorithm: 'RS256', expiresIn: -10, jwtid: 'expired-test' }); + expect(() => jwt.verify(expired, publicKey, { algorithms: ['RS256'] })).toThrow(/expired/i); + }); +}); diff --git a/manager/tsconfig.json b/manager/tsconfig.json new file mode 100644 index 0000000..ce0ad22 --- /dev/null +++ b/manager/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*.ts", "prisma/seed.ts", "scripts/**/*.ts"], + "exclude": ["node_modules", "dist", "client"] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1c210f9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8785 @@ +{ + "name": "cross-financial-management", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cross-financial-management", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.20.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.13", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.7.4", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/parser": "^8.8.0", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.31.0", + "jest": "^29.7.0", + "prisma": "^5.20.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.6.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.418", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz", + "integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d1b4b2a --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "cross-financial-management", + "version": "1.0.0", + "description": "Financial management (accounting) system API — GL, AR, AP, stock control", + "main": "dist/server.js", + "private": true, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "test": "jest --runInBand", + "test:watch": "jest --runInBand --watch", + "test:integration": "jest --runInBand --config jest.integration.config.js", + "lint": "eslint \"src/**/*.ts\"", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "prisma db seed", + "smoke": "ts-node scripts/smokeTest.ts" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^5.20.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.13", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.7.4", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/parser": "^8.8.0", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.31.0", + "jest": "^29.7.0", + "prisma": "^5.20.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.6.2" + } +} diff --git a/prisma/manual-sql/add_journal_balance_trigger.sql b/prisma/manual-sql/add_journal_balance_trigger.sql new file mode 100644 index 0000000..b23de5d --- /dev/null +++ b/prisma/manual-sql/add_journal_balance_trigger.sql @@ -0,0 +1,53 @@ +-- Layer 2 + Layer 3 of the double-entry enforcement design (see the implementation plan, +-- section "Double-Entry Enforcement"). Layer 1 is PostingService.postEntry() in +-- src/modules/gl/posting.service.ts, which is the primary guard. These are the DB-level +-- backstops that make an unbalanced journal impossible to persist even via a bypass of +-- that service (raw SQL, a future bug, a manual DB edit). +-- +-- Not expressible in Prisma's schema.prisma DSL, so this is applied as hand-written SQL +-- inside an otherwise-empty Prisma migration. See SETUP.md for how to wire this in. + +-- Layer 2: row-level guards on JournalEntryLine. +ALTER TABLE "JournalEntryLine" + ADD CONSTRAINT chk_line_nonnegative + CHECK (debit >= 0 AND credit >= 0); + +ALTER TABLE "JournalEntryLine" + ADD CONSTRAINT chk_line_debit_xor_credit + CHECK ((debit > 0 AND credit = 0) OR (credit > 0 AND debit = 0)); + +-- Layer 3: deferred trigger that re-sums debits/credits per journal_entry_id at commit +-- and rejects the transaction if they don't match. This is the real hard guarantee. +CREATE OR REPLACE FUNCTION trg_check_journal_entry_balance() RETURNS trigger AS $$ +DECLARE + affected_entry_id INTEGER; + total_debit NUMERIC(18,2); + total_credit NUMERIC(18,2); +BEGIN + affected_entry_id := COALESCE(NEW."journalEntryId", OLD."journalEntryId"); + + SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0) + INTO total_debit, total_credit + FROM "JournalEntryLine" + WHERE "journalEntryId" = affected_entry_id; + + IF total_debit <> total_credit THEN + RAISE EXCEPTION 'JournalEntry % does not balance: debits % != credits %', + affected_entry_id, total_debit, total_credit; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE CONSTRAINT TRIGGER journal_entry_line_balance_check + AFTER INSERT OR UPDATE OR DELETE ON "JournalEntryLine" + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW + EXECUTE FUNCTION trg_check_journal_entry_balance(); + +-- Extra guard: at most one AccountSegmentDefinition may be flagged as the branch segment. +-- Prisma's schema DSL can't express a partial unique index, so it's added here. +CREATE UNIQUE INDEX one_branch_segment_only + ON "AccountSegmentDefinition" (("isBranchSegment")) + WHERE "isBranchSegment" = true; diff --git a/prisma/migrations/20260901183415_init/migration.sql b/prisma/migrations/20260901183415_init/migration.sql new file mode 100644 index 0000000..e7a2f18 --- /dev/null +++ b/prisma/migrations/20260901183415_init/migration.sql @@ -0,0 +1,353 @@ +-- CreateEnum +CREATE TYPE "FiscalYearStatus" AS ENUM ('OPEN', 'LOCKED', 'CLOSED'); + +-- CreateEnum +CREATE TYPE "PeriodStatus" AS ENUM ('OPEN', 'CLOSED'); + +-- CreateEnum +CREATE TYPE "SegmentFormat" AS ENUM ('NUMERIC', 'ALPHA', 'ALPHANUMERIC'); + +-- CreateEnum +CREATE TYPE "GLAccountType" AS ENUM ('ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE'); + +-- CreateEnum +CREATE TYPE "DebitCredit" AS ENUM ('DEBIT', 'CREDIT'); + +-- CreateEnum +CREATE TYPE "ControlAccountType" AS ENUM ('AR', 'AP', 'STOCK', 'TAX', 'BANK', 'RETAINED_EARNINGS'); + +-- CreateEnum +CREATE TYPE "JournalType" AS ENUM ('GENERAL', 'SALES', 'PURCHASES', 'CASH_RECEIPT', 'CASH_PAYMENT', 'STOCK', 'BANK', 'CUSTOM'); + +-- CreateEnum +CREATE TYPE "SourceModule" AS ENUM ('GL', 'AR', 'AP', 'STOCK', 'SYSTEM'); + +-- CreateEnum +CREATE TYPE "JournalEntryStatus" AS ENUM ('DRAFT', 'POSTED', 'REVERSED'); + +-- CreateTable +CREATE TABLE "CompanySettings" ( + "id" INTEGER NOT NULL DEFAULT 1, + "companyName" TEXT NOT NULL, + "baseCurrencyCode" TEXT NOT NULL DEFAULT 'ZAR', + "fiscalYearStartMonth" INTEGER NOT NULL DEFAULT 1, + "currentFiscalYear" INTEGER NOT NULL, + "accountCodeSeparator" TEXT NOT NULL DEFAULT '-', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CompanySettings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FiscalYear" ( + "id" SERIAL NOT NULL, + "year" INTEGER NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "status" "FiscalYearStatus" NOT NULL DEFAULT 'OPEN', + + CONSTRAINT "FiscalYear_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FiscalPeriod" ( + "id" SERIAL NOT NULL, + "fiscalYearId" INTEGER NOT NULL, + "periodNumber" INTEGER NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "status" "PeriodStatus" NOT NULL DEFAULT 'OPEN', + + CONSTRAINT "FiscalPeriod_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AccountSegmentDefinition" ( + "id" SERIAL NOT NULL, + "segmentKey" TEXT NOT NULL, + "label" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "length" INTEGER NOT NULL, + "format" "SegmentFormat" NOT NULL DEFAULT 'NUMERIC', + "isBranchSegment" BOOLEAN NOT NULL DEFAULT false, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "AccountSegmentDefinition_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AccountSegmentValue" ( + "id" SERIAL NOT NULL, + "segmentDefId" INTEGER NOT NULL, + "code" TEXT NOT NULL, + "description" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "AccountSegmentValue_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BranchProfile" ( + "id" SERIAL NOT NULL, + "segmentValueId" INTEGER NOT NULL, + "address" TEXT, + "contactName" TEXT, + "isConsolidatable" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "BranchProfile_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GLAccount" ( + "id" SERIAL NOT NULL, + "accountCode" TEXT NOT NULL, + "accountName" TEXT NOT NULL, + "accountType" "GLAccountType" NOT NULL, + "normalBalance" "DebitCredit" NOT NULL, + "isControlAccount" BOOLEAN NOT NULL DEFAULT false, + "controlType" "ControlAccountType", + "isActive" BOOLEAN NOT NULL DEFAULT true, + "isPostable" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GLAccount_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GLAccountSegmentValue" ( + "id" SERIAL NOT NULL, + "glAccountId" INTEGER NOT NULL, + "segmentDefId" INTEGER NOT NULL, + "segmentValueId" INTEGER NOT NULL, + + CONSTRAINT "GLAccountSegmentValue_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Journal" ( + "id" SERIAL NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "journalType" "JournalType" NOT NULL, + "defaultBranchSegmentValueId" INTEGER, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Journal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JournalEntry" ( + "id" SERIAL NOT NULL, + "entryNumber" TEXT NOT NULL, + "journalId" INTEGER NOT NULL, + "fiscalPeriodId" INTEGER NOT NULL, + "entryDate" TIMESTAMP(3) NOT NULL, + "description" TEXT NOT NULL, + "sourceModule" "SourceModule" NOT NULL, + "sourceType" TEXT, + "sourceId" INTEGER, + "status" "JournalEntryStatus" NOT NULL DEFAULT 'POSTED', + "reversalOfId" INTEGER, + "totalDebits" DECIMAL(18,2) NOT NULL, + "totalCredits" DECIMAL(18,2) NOT NULL, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "JournalEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JournalEntryLine" ( + "id" SERIAL NOT NULL, + "journalEntryId" INTEGER NOT NULL, + "lineNumber" INTEGER NOT NULL, + "glAccountId" INTEGER NOT NULL, + "debit" DECIMAL(18,2) NOT NULL DEFAULT 0, + "credit" DECIMAL(18,2) NOT NULL DEFAULT 0, + "description" TEXT, + "branchSegmentValueId" INTEGER, + "arInvoiceId" INTEGER, + "apInvoiceId" INTEGER, + "stockMovementId" INTEGER, + "currencyCode" TEXT NOT NULL DEFAULT 'BASE', + "exchangeRateToBase" DECIMAL(18,6) NOT NULL DEFAULT 1, + + CONSTRAINT "JournalEntryLine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Sequence" ( + "id" SERIAL NOT NULL, + "key" TEXT NOT NULL, + "lastValue" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "Sequence_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Role" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "id" SERIAL NOT NULL, + "key" TEXT NOT NULL, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RolePermission" ( + "roleId" INTEGER NOT NULL, + "permissionId" INTEGER NOT NULL, + + CONSTRAINT "RolePermission_pkey" PRIMARY KEY ("roleId","permissionId") +); + +-- CreateTable +CREATE TABLE "UserRole" ( + "userId" INTEGER NOT NULL, + "roleId" INTEGER NOT NULL, + + CONSTRAINT "UserRole_pkey" PRIMARY KEY ("userId","roleId") +); + +-- CreateTable +CREATE TABLE "UserBranchRestriction" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + + CONSTRAINT "UserBranchRestriction_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "FiscalYear_year_key" ON "FiscalYear"("year"); + +-- CreateIndex +CREATE UNIQUE INDEX "FiscalPeriod_fiscalYearId_periodNumber_key" ON "FiscalPeriod"("fiscalYearId", "periodNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "AccountSegmentDefinition_segmentKey_key" ON "AccountSegmentDefinition"("segmentKey"); + +-- CreateIndex +CREATE INDEX "AccountSegmentDefinition_order_idx" ON "AccountSegmentDefinition"("order"); + +-- CreateIndex +CREATE UNIQUE INDEX "AccountSegmentValue_segmentDefId_code_key" ON "AccountSegmentValue"("segmentDefId", "code"); + +-- CreateIndex +CREATE UNIQUE INDEX "BranchProfile_segmentValueId_key" ON "BranchProfile"("segmentValueId"); + +-- CreateIndex +CREATE UNIQUE INDEX "GLAccount_accountCode_key" ON "GLAccount"("accountCode"); + +-- CreateIndex +CREATE INDEX "GLAccount_accountType_idx" ON "GLAccount"("accountType"); + +-- CreateIndex +CREATE UNIQUE INDEX "GLAccountSegmentValue_glAccountId_segmentDefId_key" ON "GLAccountSegmentValue"("glAccountId", "segmentDefId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Journal_code_key" ON "Journal"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "JournalEntry_entryNumber_key" ON "JournalEntry"("entryNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "JournalEntry_reversalOfId_key" ON "JournalEntry"("reversalOfId"); + +-- CreateIndex +CREATE INDEX "JournalEntry_fiscalPeriodId_idx" ON "JournalEntry"("fiscalPeriodId"); + +-- CreateIndex +CREATE INDEX "JournalEntry_sourceModule_sourceType_sourceId_idx" ON "JournalEntry"("sourceModule", "sourceType", "sourceId"); + +-- CreateIndex +CREATE INDEX "JournalEntryLine_journalEntryId_idx" ON "JournalEntryLine"("journalEntryId"); + +-- CreateIndex +CREATE INDEX "JournalEntryLine_glAccountId_idx" ON "JournalEntryLine"("glAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Sequence_key_key" ON "Sequence"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_key_key" ON "Permission"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserBranchRestriction_userId_branchSegmentValueId_key" ON "UserBranchRestriction"("userId", "branchSegmentValueId"); + +-- AddForeignKey +ALTER TABLE "FiscalPeriod" ADD CONSTRAINT "FiscalPeriod_fiscalYearId_fkey" FOREIGN KEY ("fiscalYearId") REFERENCES "FiscalYear"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AccountSegmentValue" ADD CONSTRAINT "AccountSegmentValue_segmentDefId_fkey" FOREIGN KEY ("segmentDefId") REFERENCES "AccountSegmentDefinition"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BranchProfile" ADD CONSTRAINT "BranchProfile_segmentValueId_fkey" FOREIGN KEY ("segmentValueId") REFERENCES "AccountSegmentValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GLAccountSegmentValue" ADD CONSTRAINT "GLAccountSegmentValue_glAccountId_fkey" FOREIGN KEY ("glAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GLAccountSegmentValue" ADD CONSTRAINT "GLAccountSegmentValue_segmentDefId_fkey" FOREIGN KEY ("segmentDefId") REFERENCES "AccountSegmentDefinition"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GLAccountSegmentValue" ADD CONSTRAINT "GLAccountSegmentValue_segmentValueId_fkey" FOREIGN KEY ("segmentValueId") REFERENCES "AccountSegmentValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalEntry" ADD CONSTRAINT "JournalEntry_journalId_fkey" FOREIGN KEY ("journalId") REFERENCES "Journal"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalEntry" ADD CONSTRAINT "JournalEntry_fiscalPeriodId_fkey" FOREIGN KEY ("fiscalPeriodId") REFERENCES "FiscalPeriod"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalEntry" ADD CONSTRAINT "JournalEntry_reversalOfId_fkey" FOREIGN KEY ("reversalOfId") REFERENCES "JournalEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalEntry" ADD CONSTRAINT "JournalEntry_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalEntryLine" ADD CONSTRAINT "JournalEntryLine_journalEntryId_fkey" FOREIGN KEY ("journalEntryId") REFERENCES "JournalEntry"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalEntryLine" ADD CONSTRAINT "JournalEntryLine_glAccountId_fkey" FOREIGN KEY ("glAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserBranchRestriction" ADD CONSTRAINT "UserBranchRestriction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260901183438_add_journal_balance_trigger/migration.sql b/prisma/migrations/20260901183438_add_journal_balance_trigger/migration.sql new file mode 100644 index 0000000..b30319a --- /dev/null +++ b/prisma/migrations/20260901183438_add_journal_balance_trigger/migration.sql @@ -0,0 +1,54 @@ +-- Layer 2 + Layer 3 of the double-entry enforcement design (see the implementation plan, +-- section "Double-Entry Enforcement"). Layer 1 is PostingService.postEntry() in +-- src/modules/gl/posting.service.ts, which is the primary guard. These are the DB-level +-- backstops that make an unbalanced journal impossible to persist even via a bypass of +-- that service (raw SQL, a future bug, a manual DB edit). +-- +-- Not expressible in Prisma's schema.prisma DSL, so this is applied as hand-written SQL +-- inside an otherwise-empty Prisma migration. Source copy kept at +-- prisma/manual-sql/add_journal_balance_trigger.sql for reference. + +-- Layer 2: row-level guards on JournalEntryLine. +ALTER TABLE "JournalEntryLine" + ADD CONSTRAINT chk_line_nonnegative + CHECK (debit >= 0 AND credit >= 0); + +ALTER TABLE "JournalEntryLine" + ADD CONSTRAINT chk_line_debit_xor_credit + CHECK ((debit > 0 AND credit = 0) OR (credit > 0 AND debit = 0)); + +-- Layer 3: deferred trigger that re-sums debits/credits per journal_entry_id at commit +-- and rejects the transaction if they don't match. This is the real hard guarantee. +CREATE OR REPLACE FUNCTION trg_check_journal_entry_balance() RETURNS trigger AS $$ +DECLARE + affected_entry_id INTEGER; + total_debit NUMERIC(18,2); + total_credit NUMERIC(18,2); +BEGIN + affected_entry_id := COALESCE(NEW."journalEntryId", OLD."journalEntryId"); + + SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0) + INTO total_debit, total_credit + FROM "JournalEntryLine" + WHERE "journalEntryId" = affected_entry_id; + + IF total_debit <> total_credit THEN + RAISE EXCEPTION 'JournalEntry % does not balance: debits % != credits %', + affected_entry_id, total_debit, total_credit; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE CONSTRAINT TRIGGER journal_entry_line_balance_check + AFTER INSERT OR UPDATE OR DELETE ON "JournalEntryLine" + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW + EXECUTE FUNCTION trg_check_journal_entry_balance(); + +-- Extra guard: at most one AccountSegmentDefinition may be flagged as the branch segment. +-- Prisma's schema DSL can't express a partial unique index, so it's added here. +CREATE UNIQUE INDEX one_branch_segment_only + ON "AccountSegmentDefinition" (("isBranchSegment")) + WHERE "isBranchSegment" = true; diff --git a/prisma/migrations/20260902134528_add_handoff_auth/migration.sql b/prisma/migrations/20260902134528_add_handoff_auth/migration.sql new file mode 100644 index 0000000..c660f37 --- /dev/null +++ b/prisma/migrations/20260902134528_add_handoff_auth/migration.sql @@ -0,0 +1,17 @@ +-- AlterTable +ALTER TABLE "CompanySettings" ADD COLUMN "autoProvisionOnHandoff" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "passwordHash" DROP NOT NULL; + +-- CreateTable +CREATE TABLE "HandoffTokenUse" ( + "id" SERIAL NOT NULL, + "jti" TEXT NOT NULL, + "usedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "HandoffTokenUse_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "HandoffTokenUse_jti_key" ON "HandoffTokenUse"("jti"); diff --git a/prisma/migrations/20260903100927_add_ar_module/migration.sql b/prisma/migrations/20260903100927_add_ar_module/migration.sql new file mode 100644 index 0000000..977cf34 --- /dev/null +++ b/prisma/migrations/20260903100927_add_ar_module/migration.sql @@ -0,0 +1,271 @@ +-- CreateEnum +CREATE TYPE "ARDocumentStatus" AS ENUM ('DRAFT', 'POSTED', 'VOID'); + +-- AlterEnum +-- Splits the generic TAX control type into TAX_OUTPUT/TAX_INPUT (AR needs Output Tax +-- Payable, AP will need Input Tax Receivable — a single "TAX" value was ambiguous for +-- control-account lookup). Existing rows are remapped by their seeded account code +-- (2100 = Output Tax Payable, 2200 = Input Tax Receivable) rather than dropped blind. +BEGIN; +CREATE TYPE "ControlAccountType_new" AS ENUM ('AR', 'AP', 'STOCK', 'TAX_OUTPUT', 'TAX_INPUT', 'BANK', 'RETAINED_EARNINGS'); +ALTER TABLE "GLAccount" ALTER COLUMN "controlType" TYPE "ControlAccountType_new" USING ( + CASE + WHEN "controlType"::text = 'TAX' AND "accountCode" LIKE '2100-%' THEN 'TAX_OUTPUT' + WHEN "controlType"::text = 'TAX' AND "accountCode" LIKE '2200-%' THEN 'TAX_INPUT' + ELSE "controlType"::text + END::"ControlAccountType_new" +); +ALTER TYPE "ControlAccountType" RENAME TO "ControlAccountType_old"; +ALTER TYPE "ControlAccountType_new" RENAME TO "ControlAccountType"; +DROP TYPE "ControlAccountType_old"; +COMMIT; + +-- CreateTable +CREATE TABLE "TaxCode" ( + "id" SERIAL NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "ratePercent" DECIMAL(6,3) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TaxCode_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Customer" ( + "id" SERIAL NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "address" TEXT, + "taxNumber" TEXT, + "branchSegmentValueId" INTEGER NOT NULL, + "creditLimit" DECIMAL(18,2) NOT NULL DEFAULT 0, + "paymentTermsDays" INTEGER NOT NULL DEFAULT 30, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Customer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ARInvoice" ( + "id" SERIAL NOT NULL, + "invoiceNumber" TEXT NOT NULL, + "customerId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + "invoiceDate" TIMESTAMP(3) NOT NULL, + "dueDate" TIMESTAMP(3) NOT NULL, + "reference" TEXT, + "status" "ARDocumentStatus" NOT NULL DEFAULT 'DRAFT', + "subtotal" DECIMAL(18,2) NOT NULL, + "taxTotal" DECIMAL(18,2) NOT NULL, + "total" DECIMAL(18,2) NOT NULL, + "balance" DECIMAL(18,2) NOT NULL, + "journalEntryId" INTEGER, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ARInvoice_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ARInvoiceLine" ( + "id" SERIAL NOT NULL, + "invoiceId" INTEGER NOT NULL, + "lineNumber" INTEGER NOT NULL, + "description" TEXT NOT NULL, + "quantity" DECIMAL(18,4) NOT NULL DEFAULT 1, + "unitPrice" DECIMAL(18,4) NOT NULL, + "revenueGLAccountId" INTEGER NOT NULL, + "taxCodeId" INTEGER, + "lineSubtotal" DECIMAL(18,2) NOT NULL, + "lineTax" DECIMAL(18,2) NOT NULL, + "lineTotal" DECIMAL(18,2) NOT NULL, + + CONSTRAINT "ARInvoiceLine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ARCreditNote" ( + "id" SERIAL NOT NULL, + "creditNoteNumber" TEXT NOT NULL, + "customerId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + "creditNoteDate" TIMESTAMP(3) NOT NULL, + "reference" TEXT, + "reason" TEXT, + "status" "ARDocumentStatus" NOT NULL DEFAULT 'DRAFT', + "subtotal" DECIMAL(18,2) NOT NULL, + "taxTotal" DECIMAL(18,2) NOT NULL, + "total" DECIMAL(18,2) NOT NULL, + "balance" DECIMAL(18,2) NOT NULL, + "journalEntryId" INTEGER, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ARCreditNote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ARCreditNoteLine" ( + "id" SERIAL NOT NULL, + "creditNoteId" INTEGER NOT NULL, + "lineNumber" INTEGER NOT NULL, + "description" TEXT NOT NULL, + "quantity" DECIMAL(18,4) NOT NULL DEFAULT 1, + "unitPrice" DECIMAL(18,4) NOT NULL, + "revenueGLAccountId" INTEGER NOT NULL, + "taxCodeId" INTEGER, + "lineSubtotal" DECIMAL(18,2) NOT NULL, + "lineTax" DECIMAL(18,2) NOT NULL, + "lineTotal" DECIMAL(18,2) NOT NULL, + + CONSTRAINT "ARCreditNoteLine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ARReceipt" ( + "id" SERIAL NOT NULL, + "receiptNumber" TEXT NOT NULL, + "customerId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + "receiptDate" TIMESTAMP(3) NOT NULL, + "amount" DECIMAL(18,2) NOT NULL, + "unallocatedAmount" DECIMAL(18,2) NOT NULL, + "bankGLAccountId" INTEGER NOT NULL, + "reference" TEXT, + "status" "ARDocumentStatus" NOT NULL DEFAULT 'DRAFT', + "journalEntryId" INTEGER, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ARReceipt_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ARAllocation" ( + "id" SERIAL NOT NULL, + "invoiceId" INTEGER NOT NULL, + "receiptId" INTEGER, + "creditNoteId" INTEGER, + "amount" DECIMAL(18,2) NOT NULL, + "allocatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ARAllocation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TaxCode_code_key" ON "TaxCode"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Customer_code_key" ON "Customer"("code"); + +-- CreateIndex +CREATE INDEX "Customer_branchSegmentValueId_idx" ON "Customer"("branchSegmentValueId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ARInvoice_invoiceNumber_key" ON "ARInvoice"("invoiceNumber"); + +-- CreateIndex +CREATE INDEX "ARInvoice_customerId_idx" ON "ARInvoice"("customerId"); + +-- CreateIndex +CREATE INDEX "ARInvoice_branchSegmentValueId_idx" ON "ARInvoice"("branchSegmentValueId"); + +-- CreateIndex +CREATE INDEX "ARInvoice_status_idx" ON "ARInvoice"("status"); + +-- CreateIndex +CREATE INDEX "ARInvoiceLine_invoiceId_idx" ON "ARInvoiceLine"("invoiceId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ARCreditNote_creditNoteNumber_key" ON "ARCreditNote"("creditNoteNumber"); + +-- CreateIndex +CREATE INDEX "ARCreditNote_customerId_idx" ON "ARCreditNote"("customerId"); + +-- CreateIndex +CREATE INDEX "ARCreditNote_branchSegmentValueId_idx" ON "ARCreditNote"("branchSegmentValueId"); + +-- CreateIndex +CREATE INDEX "ARCreditNote_status_idx" ON "ARCreditNote"("status"); + +-- CreateIndex +CREATE INDEX "ARCreditNoteLine_creditNoteId_idx" ON "ARCreditNoteLine"("creditNoteId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ARReceipt_receiptNumber_key" ON "ARReceipt"("receiptNumber"); + +-- CreateIndex +CREATE INDEX "ARReceipt_customerId_idx" ON "ARReceipt"("customerId"); + +-- CreateIndex +CREATE INDEX "ARReceipt_branchSegmentValueId_idx" ON "ARReceipt"("branchSegmentValueId"); + +-- CreateIndex +CREATE INDEX "ARReceipt_status_idx" ON "ARReceipt"("status"); + +-- CreateIndex +CREATE INDEX "ARAllocation_invoiceId_idx" ON "ARAllocation"("invoiceId"); + +-- CreateIndex +CREATE INDEX "ARAllocation_receiptId_idx" ON "ARAllocation"("receiptId"); + +-- CreateIndex +CREATE INDEX "ARAllocation_creditNoteId_idx" ON "ARAllocation"("creditNoteId"); + +-- AddForeignKey +ALTER TABLE "JournalEntryLine" ADD CONSTRAINT "JournalEntryLine_arInvoiceId_fkey" FOREIGN KEY ("arInvoiceId") REFERENCES "ARInvoice"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Customer" ADD CONSTRAINT "Customer_branchSegmentValueId_fkey" FOREIGN KEY ("branchSegmentValueId") REFERENCES "AccountSegmentValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARInvoice" ADD CONSTRAINT "ARInvoice_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARInvoice" ADD CONSTRAINT "ARInvoice_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARInvoiceLine" ADD CONSTRAINT "ARInvoiceLine_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "ARInvoice"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARInvoiceLine" ADD CONSTRAINT "ARInvoiceLine_revenueGLAccountId_fkey" FOREIGN KEY ("revenueGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARInvoiceLine" ADD CONSTRAINT "ARInvoiceLine_taxCodeId_fkey" FOREIGN KEY ("taxCodeId") REFERENCES "TaxCode"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARCreditNote" ADD CONSTRAINT "ARCreditNote_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARCreditNote" ADD CONSTRAINT "ARCreditNote_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARCreditNoteLine" ADD CONSTRAINT "ARCreditNoteLine_creditNoteId_fkey" FOREIGN KEY ("creditNoteId") REFERENCES "ARCreditNote"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARCreditNoteLine" ADD CONSTRAINT "ARCreditNoteLine_revenueGLAccountId_fkey" FOREIGN KEY ("revenueGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARCreditNoteLine" ADD CONSTRAINT "ARCreditNoteLine_taxCodeId_fkey" FOREIGN KEY ("taxCodeId") REFERENCES "TaxCode"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARReceipt" ADD CONSTRAINT "ARReceipt_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARReceipt" ADD CONSTRAINT "ARReceipt_bankGLAccountId_fkey" FOREIGN KEY ("bankGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARReceipt" ADD CONSTRAINT "ARReceipt_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARAllocation" ADD CONSTRAINT "ARAllocation_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "ARInvoice"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARAllocation" ADD CONSTRAINT "ARAllocation_receiptId_fkey" FOREIGN KEY ("receiptId") REFERENCES "ARReceipt"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ARAllocation" ADD CONSTRAINT "ARAllocation_creditNoteId_fkey" FOREIGN KEY ("creditNoteId") REFERENCES "ARCreditNote"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260903104749_add_ap_module/migration.sql b/prisma/migrations/20260903104749_add_ap_module/migration.sql new file mode 100644 index 0000000..65d4ebe --- /dev/null +++ b/prisma/migrations/20260903104749_add_ap_module/migration.sql @@ -0,0 +1,237 @@ +-- CreateEnum +CREATE TYPE "APDocumentStatus" AS ENUM ('DRAFT', 'POSTED', 'VOID'); + +-- CreateTable +CREATE TABLE "Supplier" ( + "id" SERIAL NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "address" TEXT, + "taxNumber" TEXT, + "branchSegmentValueId" INTEGER NOT NULL, + "paymentTermsDays" INTEGER NOT NULL DEFAULT 30, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Supplier_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APInvoice" ( + "id" SERIAL NOT NULL, + "invoiceNumber" TEXT NOT NULL, + "supplierReference" TEXT, + "supplierId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + "invoiceDate" TIMESTAMP(3) NOT NULL, + "dueDate" TIMESTAMP(3) NOT NULL, + "status" "APDocumentStatus" NOT NULL DEFAULT 'DRAFT', + "subtotal" DECIMAL(18,2) NOT NULL, + "taxTotal" DECIMAL(18,2) NOT NULL, + "total" DECIMAL(18,2) NOT NULL, + "balance" DECIMAL(18,2) NOT NULL, + "journalEntryId" INTEGER, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "APInvoice_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APInvoiceLine" ( + "id" SERIAL NOT NULL, + "invoiceId" INTEGER NOT NULL, + "lineNumber" INTEGER NOT NULL, + "description" TEXT NOT NULL, + "quantity" DECIMAL(18,4) NOT NULL DEFAULT 1, + "unitPrice" DECIMAL(18,4) NOT NULL, + "expenseGLAccountId" INTEGER NOT NULL, + "taxCodeId" INTEGER, + "lineSubtotal" DECIMAL(18,2) NOT NULL, + "lineTax" DECIMAL(18,2) NOT NULL, + "lineTotal" DECIMAL(18,2) NOT NULL, + + CONSTRAINT "APInvoiceLine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APDebitNote" ( + "id" SERIAL NOT NULL, + "debitNoteNumber" TEXT NOT NULL, + "supplierId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + "debitNoteDate" TIMESTAMP(3) NOT NULL, + "reference" TEXT, + "reason" TEXT, + "status" "APDocumentStatus" NOT NULL DEFAULT 'DRAFT', + "subtotal" DECIMAL(18,2) NOT NULL, + "taxTotal" DECIMAL(18,2) NOT NULL, + "total" DECIMAL(18,2) NOT NULL, + "balance" DECIMAL(18,2) NOT NULL, + "journalEntryId" INTEGER, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "APDebitNote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APDebitNoteLine" ( + "id" SERIAL NOT NULL, + "debitNoteId" INTEGER NOT NULL, + "lineNumber" INTEGER NOT NULL, + "description" TEXT NOT NULL, + "quantity" DECIMAL(18,4) NOT NULL DEFAULT 1, + "unitPrice" DECIMAL(18,4) NOT NULL, + "expenseGLAccountId" INTEGER NOT NULL, + "taxCodeId" INTEGER, + "lineSubtotal" DECIMAL(18,2) NOT NULL, + "lineTax" DECIMAL(18,2) NOT NULL, + "lineTotal" DECIMAL(18,2) NOT NULL, + + CONSTRAINT "APDebitNoteLine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APPayment" ( + "id" SERIAL NOT NULL, + "paymentNumber" TEXT NOT NULL, + "supplierId" INTEGER NOT NULL, + "branchSegmentValueId" INTEGER NOT NULL, + "paymentDate" TIMESTAMP(3) NOT NULL, + "amount" DECIMAL(18,2) NOT NULL, + "unallocatedAmount" DECIMAL(18,2) NOT NULL, + "bankGLAccountId" INTEGER NOT NULL, + "reference" TEXT, + "status" "APDocumentStatus" NOT NULL DEFAULT 'DRAFT', + "journalEntryId" INTEGER, + "createdById" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "APPayment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APAllocation" ( + "id" SERIAL NOT NULL, + "invoiceId" INTEGER NOT NULL, + "paymentId" INTEGER, + "debitNoteId" INTEGER, + "amount" DECIMAL(18,2) NOT NULL, + "allocatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "APAllocation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Supplier_code_key" ON "Supplier"("code"); + +-- CreateIndex +CREATE INDEX "Supplier_branchSegmentValueId_idx" ON "Supplier"("branchSegmentValueId"); + +-- CreateIndex +CREATE UNIQUE INDEX "APInvoice_invoiceNumber_key" ON "APInvoice"("invoiceNumber"); + +-- CreateIndex +CREATE INDEX "APInvoice_supplierId_idx" ON "APInvoice"("supplierId"); + +-- CreateIndex +CREATE INDEX "APInvoice_branchSegmentValueId_idx" ON "APInvoice"("branchSegmentValueId"); + +-- CreateIndex +CREATE INDEX "APInvoice_status_idx" ON "APInvoice"("status"); + +-- CreateIndex +CREATE INDEX "APInvoiceLine_invoiceId_idx" ON "APInvoiceLine"("invoiceId"); + +-- CreateIndex +CREATE UNIQUE INDEX "APDebitNote_debitNoteNumber_key" ON "APDebitNote"("debitNoteNumber"); + +-- CreateIndex +CREATE INDEX "APDebitNote_supplierId_idx" ON "APDebitNote"("supplierId"); + +-- CreateIndex +CREATE INDEX "APDebitNote_branchSegmentValueId_idx" ON "APDebitNote"("branchSegmentValueId"); + +-- CreateIndex +CREATE INDEX "APDebitNote_status_idx" ON "APDebitNote"("status"); + +-- CreateIndex +CREATE INDEX "APDebitNoteLine_debitNoteId_idx" ON "APDebitNoteLine"("debitNoteId"); + +-- CreateIndex +CREATE UNIQUE INDEX "APPayment_paymentNumber_key" ON "APPayment"("paymentNumber"); + +-- CreateIndex +CREATE INDEX "APPayment_supplierId_idx" ON "APPayment"("supplierId"); + +-- CreateIndex +CREATE INDEX "APPayment_branchSegmentValueId_idx" ON "APPayment"("branchSegmentValueId"); + +-- CreateIndex +CREATE INDEX "APPayment_status_idx" ON "APPayment"("status"); + +-- CreateIndex +CREATE INDEX "APAllocation_invoiceId_idx" ON "APAllocation"("invoiceId"); + +-- CreateIndex +CREATE INDEX "APAllocation_paymentId_idx" ON "APAllocation"("paymentId"); + +-- CreateIndex +CREATE INDEX "APAllocation_debitNoteId_idx" ON "APAllocation"("debitNoteId"); + +-- AddForeignKey +ALTER TABLE "JournalEntryLine" ADD CONSTRAINT "JournalEntryLine_apInvoiceId_fkey" FOREIGN KEY ("apInvoiceId") REFERENCES "APInvoice"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Supplier" ADD CONSTRAINT "Supplier_branchSegmentValueId_fkey" FOREIGN KEY ("branchSegmentValueId") REFERENCES "AccountSegmentValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APInvoice" ADD CONSTRAINT "APInvoice_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "Supplier"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APInvoice" ADD CONSTRAINT "APInvoice_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APInvoiceLine" ADD CONSTRAINT "APInvoiceLine_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "APInvoice"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APInvoiceLine" ADD CONSTRAINT "APInvoiceLine_expenseGLAccountId_fkey" FOREIGN KEY ("expenseGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APInvoiceLine" ADD CONSTRAINT "APInvoiceLine_taxCodeId_fkey" FOREIGN KEY ("taxCodeId") REFERENCES "TaxCode"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APDebitNote" ADD CONSTRAINT "APDebitNote_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "Supplier"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APDebitNote" ADD CONSTRAINT "APDebitNote_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APDebitNoteLine" ADD CONSTRAINT "APDebitNoteLine_debitNoteId_fkey" FOREIGN KEY ("debitNoteId") REFERENCES "APDebitNote"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APDebitNoteLine" ADD CONSTRAINT "APDebitNoteLine_expenseGLAccountId_fkey" FOREIGN KEY ("expenseGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APDebitNoteLine" ADD CONSTRAINT "APDebitNoteLine_taxCodeId_fkey" FOREIGN KEY ("taxCodeId") REFERENCES "TaxCode"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APPayment" ADD CONSTRAINT "APPayment_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "Supplier"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APPayment" ADD CONSTRAINT "APPayment_bankGLAccountId_fkey" FOREIGN KEY ("bankGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APPayment" ADD CONSTRAINT "APPayment_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APAllocation" ADD CONSTRAINT "APAllocation_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "APInvoice"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APAllocation" ADD CONSTRAINT "APAllocation_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "APPayment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APAllocation" ADD CONSTRAINT "APAllocation_debitNoteId_fkey" FOREIGN KEY ("debitNoteId") REFERENCES "APDebitNote"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..e029536 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,681 @@ +// Phase 1: Foundation — auth, company/fiscal setup, segmented Chart of Accounts, GL posting engine, journals. +// AR / AP / Tax / Stock models are added in later phases per the implementation plan. + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// --------------------------------------------------------------------------- +// Company / fiscal setup +// --------------------------------------------------------------------------- + +model CompanySettings { + id Int @id @default(1) + companyName String + baseCurrencyCode String @default("ZAR") + fiscalYearStartMonth Int @default(1) + currentFiscalYear Int + accountCodeSeparator String @default("-") + // Login now happens at the central Account Manager, which hands this deployment a signed + // token instead of a password (see src/lib/handoffVerify.ts). If false (the default), a + // handoff for an email with no matching User here is rejected rather than auto-creating one. + autoProvisionOnHandoff Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Records each handoff token's jti once used, so a captured token can't be replayed. +model HandoffTokenUse { + id Int @id @default(autoincrement()) + jti String @unique + usedAt DateTime @default(now()) +} + +enum FiscalYearStatus { + OPEN + LOCKED + CLOSED +} + +enum PeriodStatus { + OPEN + CLOSED +} + +model FiscalYear { + id Int @id @default(autoincrement()) + year Int @unique + startDate DateTime + endDate DateTime + status FiscalYearStatus @default(OPEN) + periods FiscalPeriod[] +} + +model FiscalPeriod { + id Int @id @default(autoincrement()) + fiscalYearId Int + fiscalYear FiscalYear @relation(fields: [fiscalYearId], references: [id]) + periodNumber Int + startDate DateTime + endDate DateTime + status PeriodStatus @default(OPEN) + journalEntries JournalEntry[] + + @@unique([fiscalYearId, periodNumber]) +} + +// --------------------------------------------------------------------------- +// Segmented Chart of Accounts (Sage 300-style account segments) +// --------------------------------------------------------------------------- + +enum SegmentFormat { + NUMERIC + ALPHA + ALPHANUMERIC +} + +model AccountSegmentDefinition { + id Int @id @default(autoincrement()) + segmentKey String @unique + label String + order Int + length Int + format SegmentFormat @default(NUMERIC) + isBranchSegment Boolean @default(false) + isActive Boolean @default(true) + values AccountSegmentValue[] + glAccountLinks GLAccountSegmentValue[] + + @@index([order]) +} + +model AccountSegmentValue { + id Int @id @default(autoincrement()) + segmentDefId Int + segmentDef AccountSegmentDefinition @relation(fields: [segmentDefId], references: [id]) + code String + description String + isActive Boolean @default(true) + branchProfile BranchProfile? + glAccountLinks GLAccountSegmentValue[] + customers Customer[] + suppliers Supplier[] + + @@unique([segmentDefId, code]) +} + +model BranchProfile { + id Int @id @default(autoincrement()) + segmentValueId Int @unique + segmentValue AccountSegmentValue @relation(fields: [segmentValueId], references: [id]) + address String? + contactName String? + isConsolidatable Boolean @default(true) +} + +enum GLAccountType { + ASSET + LIABILITY + EQUITY + INCOME + EXPENSE +} + +enum DebitCredit { + DEBIT + CREDIT +} + +enum ControlAccountType { + AR + AP + STOCK + TAX_OUTPUT + TAX_INPUT + BANK + RETAINED_EARNINGS +} + +model GLAccount { + id Int @id @default(autoincrement()) + accountCode String @unique + accountName String + accountType GLAccountType + normalBalance DebitCredit + isControlAccount Boolean @default(false) + controlType ControlAccountType? + isActive Boolean @default(true) + isPostable Boolean @default(true) + segmentValues GLAccountSegmentValue[] + journalLines JournalEntryLine[] + arInvoiceLines ARInvoiceLine[] @relation("ARInvoiceLineRevenue") + arCreditNoteLines ARCreditNoteLine[] @relation("ARCreditNoteLineRevenue") + arReceipts ARReceipt[] @relation("ARReceiptBank") + apInvoiceLines APInvoiceLine[] @relation("APInvoiceLineExpense") + apDebitNoteLines APDebitNoteLine[] @relation("APDebitNoteLineExpense") + apPayments APPayment[] @relation("APPaymentBank") + createdAt DateTime @default(now()) + + @@index([accountType]) +} + +model GLAccountSegmentValue { + id Int @id @default(autoincrement()) + glAccountId Int + glAccount GLAccount @relation(fields: [glAccountId], references: [id]) + segmentDefId Int + segmentDef AccountSegmentDefinition @relation(fields: [segmentDefId], references: [id]) + segmentValueId Int + segmentValue AccountSegmentValue @relation(fields: [segmentValueId], references: [id]) + + @@unique([glAccountId, segmentDefId]) +} + +// --------------------------------------------------------------------------- +// Journals / General Ledger posting core +// --------------------------------------------------------------------------- + +enum JournalType { + GENERAL + SALES + PURCHASES + CASH_RECEIPT + CASH_PAYMENT + STOCK + BANK + CUSTOM +} + +model Journal { + id Int @id @default(autoincrement()) + code String @unique + name String + journalType JournalType + defaultBranchSegmentValueId Int? + isActive Boolean @default(true) + entries JournalEntry[] +} + +enum SourceModule { + GL + AR + AP + STOCK + SYSTEM +} + +enum JournalEntryStatus { + DRAFT + POSTED + REVERSED +} + +model JournalEntry { + id Int @id @default(autoincrement()) + entryNumber String @unique + journalId Int + journal Journal @relation(fields: [journalId], references: [id]) + fiscalPeriodId Int + fiscalPeriod FiscalPeriod @relation(fields: [fiscalPeriodId], references: [id]) + entryDate DateTime + description String + sourceModule SourceModule + sourceType String? + sourceId Int? + status JournalEntryStatus @default(POSTED) + reversalOfId Int? @unique + reversalOf JournalEntry? @relation("Reversal", fields: [reversalOfId], references: [id]) + reversedBy JournalEntry? @relation("Reversal") + totalDebits Decimal @db.Decimal(18, 2) + totalCredits Decimal @db.Decimal(18, 2) + createdById Int + createdBy User @relation(fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + lines JournalEntryLine[] + + @@index([fiscalPeriodId]) + @@index([sourceModule, sourceType, sourceId]) +} + +model JournalEntryLine { + id Int @id @default(autoincrement()) + journalEntryId Int + journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id]) + lineNumber Int + glAccountId Int + glAccount GLAccount @relation(fields: [glAccountId], references: [id]) + debit Decimal @default(0) @db.Decimal(18, 2) + credit Decimal @default(0) @db.Decimal(18, 2) + description String? + branchSegmentValueId Int? + // Sub-ledger cross-references for drill-down. arInvoiceId/apInvoiceId are real FKs now that + // AR/AP exist; stockMovementId stays a plain id until Stock lands in its own phase. + arInvoiceId Int? + arInvoice ARInvoice? @relation(fields: [arInvoiceId], references: [id]) + apInvoiceId Int? + apInvoice APInvoice? @relation(fields: [apInvoiceId], references: [id]) + stockMovementId Int? + currencyCode String @default("BASE") + exchangeRateToBase Decimal @default(1) @db.Decimal(18, 6) + + @@index([journalEntryId]) + @@index([glAccountId]) +} + +// --------------------------------------------------------------------------- +// Document numbering +// --------------------------------------------------------------------------- + +// One row per document-number series (e.g. "JE-2026", "ARINV-2026"). Numbers are +// issued via an atomic increment inside the same transaction as the document create, +// so they stay gap-free/race-free under concurrent posting. +model Sequence { + id Int @id @default(autoincrement()) + key String @unique + lastValue Int @default(0) +} + +// --------------------------------------------------------------------------- +// Accounts Receivable +// --------------------------------------------------------------------------- + +model TaxCode { + id Int @id @default(autoincrement()) + code String @unique + name String + ratePercent Decimal @db.Decimal(6, 3) + isActive Boolean @default(true) + arInvoiceLines ARInvoiceLine[] + arCreditNoteLines ARCreditNoteLine[] + apInvoiceLines APInvoiceLine[] + apDebitNoteLines APDebitNoteLine[] +} + +model Customer { + id Int @id @default(autoincrement()) + code String @unique + name String + email String? + phone String? + address String? + taxNumber String? + branchSegmentValueId Int + branchSegmentValue AccountSegmentValue @relation(fields: [branchSegmentValueId], references: [id]) + creditLimit Decimal @default(0) @db.Decimal(18, 2) + paymentTermsDays Int @default(30) + isActive Boolean @default(true) + invoices ARInvoice[] + creditNotes ARCreditNote[] + receipts ARReceipt[] + createdAt DateTime @default(now()) + + @@index([branchSegmentValueId]) +} + +enum ARDocumentStatus { + DRAFT + POSTED + VOID +} + +model ARInvoice { + id Int @id @default(autoincrement()) + invoiceNumber String @unique + customerId Int + customer Customer @relation(fields: [customerId], references: [id]) + branchSegmentValueId Int + invoiceDate DateTime + dueDate DateTime + reference String? + status ARDocumentStatus @default(DRAFT) + subtotal Decimal @db.Decimal(18, 2) + taxTotal Decimal @db.Decimal(18, 2) + total Decimal @db.Decimal(18, 2) + balance Decimal @db.Decimal(18, 2) + journalEntryId Int? + lines ARInvoiceLine[] + allocations ARAllocation[] + journalLines JournalEntryLine[] + createdById Int + createdBy User @relation("ARInvoiceCreatedBy", fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + + @@index([customerId]) + @@index([branchSegmentValueId]) + @@index([status]) +} + +model ARInvoiceLine { + id Int @id @default(autoincrement()) + invoiceId Int + invoice ARInvoice @relation(fields: [invoiceId], references: [id]) + lineNumber Int + description String + quantity Decimal @default(1) @db.Decimal(18, 4) + unitPrice Decimal @db.Decimal(18, 4) + revenueGLAccountId Int + revenueGLAccount GLAccount @relation("ARInvoiceLineRevenue", fields: [revenueGLAccountId], references: [id]) + taxCodeId Int? + taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) + lineSubtotal Decimal @db.Decimal(18, 2) + lineTax Decimal @db.Decimal(18, 2) + lineTotal Decimal @db.Decimal(18, 2) + + @@index([invoiceId]) +} + +model ARCreditNote { + id Int @id @default(autoincrement()) + creditNoteNumber String @unique + customerId Int + customer Customer @relation(fields: [customerId], references: [id]) + branchSegmentValueId Int + creditNoteDate DateTime + reference String? + reason String? + status ARDocumentStatus @default(DRAFT) + subtotal Decimal @db.Decimal(18, 2) + taxTotal Decimal @db.Decimal(18, 2) + total Decimal @db.Decimal(18, 2) + balance Decimal @db.Decimal(18, 2) + journalEntryId Int? + lines ARCreditNoteLine[] + allocations ARAllocation[] + createdById Int + createdBy User @relation("ARCreditNoteCreatedBy", fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + + @@index([customerId]) + @@index([branchSegmentValueId]) + @@index([status]) +} + +model ARCreditNoteLine { + id Int @id @default(autoincrement()) + creditNoteId Int + creditNote ARCreditNote @relation(fields: [creditNoteId], references: [id]) + lineNumber Int + description String + quantity Decimal @default(1) @db.Decimal(18, 4) + unitPrice Decimal @db.Decimal(18, 4) + revenueGLAccountId Int + revenueGLAccount GLAccount @relation("ARCreditNoteLineRevenue", fields: [revenueGLAccountId], references: [id]) + taxCodeId Int? + taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) + lineSubtotal Decimal @db.Decimal(18, 2) + lineTax Decimal @db.Decimal(18, 2) + lineTotal Decimal @db.Decimal(18, 2) + + @@index([creditNoteId]) +} + +model ARReceipt { + id Int @id @default(autoincrement()) + receiptNumber String @unique + customerId Int + customer Customer @relation(fields: [customerId], references: [id]) + branchSegmentValueId Int + receiptDate DateTime + amount Decimal @db.Decimal(18, 2) + unallocatedAmount Decimal @db.Decimal(18, 2) + bankGLAccountId Int + bankGLAccount GLAccount @relation("ARReceiptBank", fields: [bankGLAccountId], references: [id]) + reference String? + status ARDocumentStatus @default(DRAFT) + journalEntryId Int? + allocations ARAllocation[] + createdById Int + createdBy User @relation("ARReceiptCreatedBy", fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + + @@index([customerId]) + @@index([branchSegmentValueId]) + @@index([status]) +} + +model ARAllocation { + id Int @id @default(autoincrement()) + invoiceId Int + invoice ARInvoice @relation(fields: [invoiceId], references: [id]) + receiptId Int? + receipt ARReceipt? @relation(fields: [receiptId], references: [id]) + creditNoteId Int? + creditNote ARCreditNote? @relation(fields: [creditNoteId], references: [id]) + amount Decimal @db.Decimal(18, 2) + allocatedAt DateTime @default(now()) + + @@index([invoiceId]) + @@index([receiptId]) + @@index([creditNoteId]) +} + +// --------------------------------------------------------------------------- +// Accounts Payable +// --------------------------------------------------------------------------- + +model Supplier { + id Int @id @default(autoincrement()) + code String @unique + name String + email String? + phone String? + address String? + taxNumber String? + branchSegmentValueId Int + branchSegmentValue AccountSegmentValue @relation(fields: [branchSegmentValueId], references: [id]) + paymentTermsDays Int @default(30) + isActive Boolean @default(true) + invoices APInvoice[] + debitNotes APDebitNote[] + payments APPayment[] + createdAt DateTime @default(now()) + + @@index([branchSegmentValueId]) +} + +enum APDocumentStatus { + DRAFT + POSTED + VOID +} + +model APInvoice { + id Int @id @default(autoincrement()) + invoiceNumber String @unique + supplierReference String? + supplierId Int + supplier Supplier @relation(fields: [supplierId], references: [id]) + branchSegmentValueId Int + invoiceDate DateTime + dueDate DateTime + status APDocumentStatus @default(DRAFT) + subtotal Decimal @db.Decimal(18, 2) + taxTotal Decimal @db.Decimal(18, 2) + total Decimal @db.Decimal(18, 2) + balance Decimal @db.Decimal(18, 2) + journalEntryId Int? + lines APInvoiceLine[] + allocations APAllocation[] + journalLines JournalEntryLine[] + createdById Int + createdBy User @relation("APInvoiceCreatedBy", fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + + @@index([supplierId]) + @@index([branchSegmentValueId]) + @@index([status]) +} + +model APInvoiceLine { + id Int @id @default(autoincrement()) + invoiceId Int + invoice APInvoice @relation(fields: [invoiceId], references: [id]) + lineNumber Int + description String + quantity Decimal @default(1) @db.Decimal(18, 4) + unitPrice Decimal @db.Decimal(18, 4) + expenseGLAccountId Int + expenseGLAccount GLAccount @relation("APInvoiceLineExpense", fields: [expenseGLAccountId], references: [id]) + taxCodeId Int? + taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) + lineSubtotal Decimal @db.Decimal(18, 2) + lineTax Decimal @db.Decimal(18, 2) + lineTotal Decimal @db.Decimal(18, 2) + + @@index([invoiceId]) +} + +model APDebitNote { + id Int @id @default(autoincrement()) + debitNoteNumber String @unique + supplierId Int + supplier Supplier @relation(fields: [supplierId], references: [id]) + branchSegmentValueId Int + debitNoteDate DateTime + reference String? + reason String? + status APDocumentStatus @default(DRAFT) + subtotal Decimal @db.Decimal(18, 2) + taxTotal Decimal @db.Decimal(18, 2) + total Decimal @db.Decimal(18, 2) + balance Decimal @db.Decimal(18, 2) + journalEntryId Int? + lines APDebitNoteLine[] + allocations APAllocation[] + createdById Int + createdBy User @relation("APDebitNoteCreatedBy", fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + + @@index([supplierId]) + @@index([branchSegmentValueId]) + @@index([status]) +} + +model APDebitNoteLine { + id Int @id @default(autoincrement()) + debitNoteId Int + debitNote APDebitNote @relation(fields: [debitNoteId], references: [id]) + lineNumber Int + description String + quantity Decimal @default(1) @db.Decimal(18, 4) + unitPrice Decimal @db.Decimal(18, 4) + expenseGLAccountId Int + expenseGLAccount GLAccount @relation("APDebitNoteLineExpense", fields: [expenseGLAccountId], references: [id]) + taxCodeId Int? + taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) + lineSubtotal Decimal @db.Decimal(18, 2) + lineTax Decimal @db.Decimal(18, 2) + lineTotal Decimal @db.Decimal(18, 2) + + @@index([debitNoteId]) +} + +model APPayment { + id Int @id @default(autoincrement()) + paymentNumber String @unique + supplierId Int + supplier Supplier @relation(fields: [supplierId], references: [id]) + branchSegmentValueId Int + paymentDate DateTime + amount Decimal @db.Decimal(18, 2) + unallocatedAmount Decimal @db.Decimal(18, 2) + bankGLAccountId Int + bankGLAccount GLAccount @relation("APPaymentBank", fields: [bankGLAccountId], references: [id]) + reference String? + status APDocumentStatus @default(DRAFT) + journalEntryId Int? + allocations APAllocation[] + createdById Int + createdBy User @relation("APPaymentCreatedBy", fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + + @@index([supplierId]) + @@index([branchSegmentValueId]) + @@index([status]) +} + +model APAllocation { + id Int @id @default(autoincrement()) + invoiceId Int + invoice APInvoice @relation(fields: [invoiceId], references: [id]) + paymentId Int? + payment APPayment? @relation(fields: [paymentId], references: [id]) + debitNoteId Int? + debitNote APDebitNote? @relation(fields: [debitNoteId], references: [id]) + amount Decimal @db.Decimal(18, 2) + allocatedAt DateTime @default(now()) + + @@index([invoiceId]) + @@index([paymentId]) + @@index([debitNoteId]) +} + +// --------------------------------------------------------------------------- +// Auth / RBAC +// --------------------------------------------------------------------------- + +model User { + id Int @id @default(autoincrement()) + email String @unique + // Nullable: login happens at the central Account Manager now, not here (see + // src/modules/auth/auth.service.ts verifyHandoff()). Kept, not dropped, in case a fallback + // local-login path is ever needed again. + passwordHash String? + fullName String + isActive Boolean @default(true) + roles UserRole[] + branchRestrictions UserBranchRestriction[] + journalEntries JournalEntry[] + arInvoices ARInvoice[] @relation("ARInvoiceCreatedBy") + arCreditNotes ARCreditNote[] @relation("ARCreditNoteCreatedBy") + arReceipts ARReceipt[] @relation("ARReceiptCreatedBy") + apInvoices APInvoice[] @relation("APInvoiceCreatedBy") + apDebitNotes APDebitNote[] @relation("APDebitNoteCreatedBy") + apPayments APPayment[] @relation("APPaymentCreatedBy") + createdAt DateTime @default(now()) +} + +model Role { + id Int @id @default(autoincrement()) + name String @unique + permissions RolePermission[] + users UserRole[] +} + +model Permission { + id Int @id @default(autoincrement()) + key String @unique + roles RolePermission[] +} + +model RolePermission { + roleId Int + role Role @relation(fields: [roleId], references: [id]) + permissionId Int + permission Permission @relation(fields: [permissionId], references: [id]) + + @@id([roleId, permissionId]) +} + +model UserRole { + userId Int + user User @relation(fields: [userId], references: [id]) + roleId Int + role Role @relation(fields: [roleId], references: [id]) + + @@id([userId, roleId]) +} + +model UserBranchRestriction { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id]) + branchSegmentValueId Int + + @@unique([userId, branchSegmentValueId]) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..15faabd --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,315 @@ +import { PrismaClient, GLAccountType, DebitCredit, ControlAccountType, JournalType } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const PERMISSIONS = [ + 'admin.segments.manage', + 'admin.fiscalPeriods.manage', + 'admin.users.manage', + 'admin.tax.manage', + 'gl.account.manage', + 'gl.journal.manage', + 'gl.journal.post', + 'ar.customer.manage', + 'ar.invoice.manage', + 'ar.creditnote.manage', + 'ar.receipt.manage', + 'ap.supplier.manage', + 'ap.invoice.manage', + 'ap.debitnote.manage', + 'ap.payment.manage', +] as const; + +const ROLE_PERMISSIONS: Record = { + ADMIN: PERMISSIONS, + ACCOUNTANT: [ + 'gl.account.manage', + 'gl.journal.manage', + 'gl.journal.post', + 'admin.tax.manage', + 'ar.customer.manage', + 'ar.invoice.manage', + 'ar.creditnote.manage', + 'ar.receipt.manage', + 'ap.supplier.manage', + 'ap.invoice.manage', + 'ap.debitnote.manage', + 'ap.payment.manage', + ], + AR_AP_CLERK: [ + 'ar.customer.manage', + 'ar.invoice.manage', + 'ar.creditnote.manage', + 'ar.receipt.manage', + 'ap.supplier.manage', + 'ap.invoice.manage', + 'ap.debitnote.manage', + 'ap.payment.manage', + ], + BRANCH_USER: [], + READ_ONLY: [], +}; + +async function main() { + console.log('Seeding company settings...'); + await prisma.companySettings.upsert({ + where: { id: 1 }, + create: { id: 1, companyName: 'Demo Company', baseCurrencyCode: 'ZAR', fiscalYearStartMonth: 1, currentFiscalYear: 2026 }, + update: {}, + }); + + console.log('Seeding permissions and roles...'); + for (const key of PERMISSIONS) { + await prisma.permission.upsert({ where: { key }, create: { key }, update: {} }); + } + for (const roleName of Object.keys(ROLE_PERMISSIONS)) { + await prisma.role.upsert({ where: { name: roleName }, create: { name: roleName }, update: {} }); + } + for (const [roleName, permissionKeys] of Object.entries(ROLE_PERMISSIONS)) { + const role = await prisma.role.findUniqueOrThrow({ where: { name: roleName } }); + for (const key of permissionKeys) { + const permission = await prisma.permission.findUniqueOrThrow({ where: { key } }); + await prisma.rolePermission.upsert({ + where: { roleId_permissionId: { roleId: role.id, permissionId: permission.id } }, + create: { roleId: role.id, permissionId: permission.id }, + update: {}, + }); + } + } + + console.log('Seeding default account segment structure...'); + const segmentDefs = [ + { segmentKey: 'MASTER', label: 'Master Account', order: 0, length: 4, format: 'NUMERIC' as const, isBranchSegment: false }, + { segmentKey: 'SUB', label: 'Sub Account', order: 1, length: 3, format: 'NUMERIC' as const, isBranchSegment: false }, + { segmentKey: 'SUBSUB', label: 'Sub-Sub Account', order: 2, length: 3, format: 'NUMERIC' as const, isBranchSegment: false }, + { segmentKey: 'BRANCH', label: 'Branch', order: 3, length: 3, format: 'ALPHA' as const, isBranchSegment: true }, + ]; + const defByKey: Record = {}; + for (const def of segmentDefs) { + defByKey[def.segmentKey] = await prisma.accountSegmentDefinition.upsert({ + where: { segmentKey: def.segmentKey }, + create: def, + update: def, + }); + } + + const masterValues = [ + { code: '1000', description: 'Bank / Cash' }, + { code: '1100', description: 'Accounts Receivable Control' }, + { code: '1200', description: 'Inventory / Stock Control' }, + { code: '2000', description: 'Accounts Payable Control' }, + { code: '2100', description: 'Output Tax Payable' }, + { code: '2200', description: 'Input Tax Receivable' }, + { code: '3000', description: 'Retained Earnings' }, + { code: '3100', description: 'Share Capital' }, + { code: '4000', description: 'Sales Income' }, + { code: '5000', description: 'Cost of Goods Sold' }, + { code: '5100', description: 'Operating Expenses' }, + { code: '5200', description: 'Stock Variance' }, + ]; + const masterByCode: Record = {}; + for (const value of masterValues) { + masterByCode[value.code] = await prisma.accountSegmentValue.upsert({ + where: { segmentDefId_code: { segmentDefId: defByKey.MASTER.id, code: value.code } }, + create: { segmentDefId: defByKey.MASTER.id, ...value }, + update: value, + }); + } + + const subDefault = await prisma.accountSegmentValue.upsert({ + where: { segmentDefId_code: { segmentDefId: defByKey.SUB.id, code: '000' } }, + create: { segmentDefId: defByKey.SUB.id, code: '000', description: 'Default' }, + update: {}, + }); + const subsubDefault = await prisma.accountSegmentValue.upsert({ + where: { segmentDefId_code: { segmentDefId: defByKey.SUBSUB.id, code: '000' } }, + create: { segmentDefId: defByKey.SUBSUB.id, code: '000', description: 'Default' }, + update: {}, + }); + + const branchDefs = [ + { code: 'HQ', description: 'Head Office' }, + { code: 'AMA', description: 'Branch AMA (example branch)' }, + ]; + const branchByCode: Record = {}; + for (const branch of branchDefs) { + const value = await prisma.accountSegmentValue.upsert({ + where: { segmentDefId_code: { segmentDefId: defByKey.BRANCH.id, code: branch.code } }, + create: { segmentDefId: defByKey.BRANCH.id, code: branch.code, description: branch.description }, + update: {}, + }); + branchByCode[branch.code] = value; + await prisma.branchProfile.upsert({ + where: { segmentValueId: value.id }, + create: { segmentValueId: value.id }, + update: {}, + }); + } + + console.log('Seeding default Chart of Accounts...'); + const operationalAccounts: Array<{ + masterCode: string; + accountName: string; + accountType: GLAccountType; + normalBalance: DebitCredit; + isControlAccount?: boolean; + controlType?: ControlAccountType; + }> = [ + { masterCode: '1000', accountName: 'Bank / Cash', accountType: 'ASSET', normalBalance: 'DEBIT', controlType: 'BANK' }, + { masterCode: '1100', accountName: 'Accounts Receivable Control', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'AR' }, + { masterCode: '1200', accountName: 'Inventory / Stock Control', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'STOCK' }, + { masterCode: '2000', accountName: 'Accounts Payable Control', accountType: 'LIABILITY', normalBalance: 'CREDIT', isControlAccount: true, controlType: 'AP' }, + { masterCode: '2100', accountName: 'Output Tax Payable', accountType: 'LIABILITY', normalBalance: 'CREDIT', isControlAccount: true, controlType: 'TAX_OUTPUT' }, + { masterCode: '2200', accountName: 'Input Tax Receivable', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'TAX_INPUT' }, + { masterCode: '4000', accountName: 'Sales Income', accountType: 'INCOME', normalBalance: 'CREDIT' }, + { masterCode: '5000', accountName: 'Cost of Goods Sold', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, + { masterCode: '5100', accountName: 'Operating Expenses', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, + { masterCode: '5200', accountName: 'Stock Variance', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, + ]; + + for (const branchCode of Object.keys(branchByCode)) { + for (const account of operationalAccounts) { + const accountCode = [masterByCode[account.masterCode].id, subDefault.id, subsubDefault.id, branchByCode[branchCode].id]; + const fullCode = `${account.masterCode}-000-000-${branchCode}`; + await prisma.gLAccount.upsert({ + where: { accountCode: fullCode }, + create: { + accountCode: fullCode, + accountName: `${account.accountName} — ${branchCode}`, + accountType: account.accountType, + normalBalance: account.normalBalance, + isControlAccount: account.isControlAccount ?? false, + controlType: account.controlType, + segmentValues: { + create: [ + { segmentDefId: defByKey.MASTER.id, segmentValueId: accountCode[0] }, + { segmentDefId: defByKey.SUB.id, segmentValueId: accountCode[1] }, + { segmentDefId: defByKey.SUBSUB.id, segmentValueId: accountCode[2] }, + { segmentDefId: defByKey.BRANCH.id, segmentValueId: accountCode[3] }, + ], + }, + }, + update: {}, + }); + } + } + + // Equity accounts are booked at Head Office only. + const equityAccounts: Array<{ masterCode: string; accountName: string; controlType?: ControlAccountType }> = [ + { masterCode: '3000', accountName: 'Retained Earnings', controlType: 'RETAINED_EARNINGS' }, + { masterCode: '3100', accountName: 'Share Capital' }, + ]; + for (const account of equityAccounts) { + const fullCode = `${account.masterCode}-000-000-HQ`; + await prisma.gLAccount.upsert({ + where: { accountCode: fullCode }, + create: { + accountCode: fullCode, + accountName: account.accountName, + accountType: 'EQUITY', + normalBalance: 'CREDIT', + controlType: account.controlType, + segmentValues: { + create: [ + { segmentDefId: defByKey.MASTER.id, segmentValueId: masterByCode[account.masterCode].id }, + { segmentDefId: defByKey.SUB.id, segmentValueId: subDefault.id }, + { segmentDefId: defByKey.SUBSUB.id, segmentValueId: subsubDefault.id }, + { segmentDefId: defByKey.BRANCH.id, segmentValueId: branchByCode.HQ.id }, + ], + }, + }, + update: {}, + }); + } + + console.log('Seeding journals...'); + await prisma.journal.upsert({ + where: { code: 'GJ' }, + create: { code: 'GJ', name: 'General Journal', journalType: JournalType.GENERAL }, + update: {}, + }); + await prisma.journal.upsert({ + where: { code: 'PC-HQ' }, + create: { + code: 'PC-HQ', + name: 'Petty Cash — Head Office', + journalType: JournalType.CUSTOM, + defaultBranchSegmentValueId: branchByCode.HQ.id, + }, + update: {}, + }); + await prisma.journal.upsert({ + where: { code: 'SALES' }, + create: { code: 'SALES', name: 'Sales Journal', journalType: JournalType.SALES }, + update: {}, + }); + await prisma.journal.upsert({ + where: { code: 'CASH_RECEIPT' }, + create: { code: 'CASH_RECEIPT', name: 'Cash Receipts Journal', journalType: JournalType.CASH_RECEIPT }, + update: {}, + }); + await prisma.journal.upsert({ + where: { code: 'PURCHASES' }, + create: { code: 'PURCHASES', name: 'Purchases Journal', journalType: JournalType.PURCHASES }, + update: {}, + }); + await prisma.journal.upsert({ + where: { code: 'CASH_PAYMENT' }, + create: { code: 'CASH_PAYMENT', name: 'Cash Payments Journal', journalType: JournalType.CASH_PAYMENT }, + update: {}, + }); + + console.log('Seeding tax codes...'); + const taxCodes = [ + { code: 'STD', name: 'Standard Rate (15%)', ratePercent: 15 }, + { code: 'ZERO', name: 'Zero-Rated (0%)', ratePercent: 0 }, + ]; + for (const taxCode of taxCodes) { + await prisma.taxCode.upsert({ where: { code: taxCode.code }, create: taxCode, update: taxCode }); + } + + console.log('Seeding current fiscal year...'); + const fiscalYear = await prisma.fiscalYear.upsert({ + where: { year: 2026 }, + create: { year: 2026, startDate: new Date(Date.UTC(2026, 0, 1)), endDate: new Date(Date.UTC(2026, 11, 31)) }, + update: {}, + }); + for (let month = 0; month < 12; month += 1) { + const startDate = new Date(Date.UTC(2026, month, 1)); + const endDate = new Date(Date.UTC(2026, month + 1, 0)); + await prisma.fiscalPeriod.upsert({ + where: { fiscalYearId_periodNumber: { fiscalYearId: fiscalYear.id, periodNumber: month + 1 } }, + create: { fiscalYearId: fiscalYear.id, periodNumber: month + 1, startDate, endDate }, + update: {}, + }); + } + + console.log('Seeding admin user...'); + // No password here — identity is established by the Account Manager's handoff token and + // matched to this User by email (see src/modules/auth/auth.service.ts verifyHandoff()). + // This email must match the Person who logs into the Manager for the mapping to resolve. + const adminEmail = 'joosth2002@icloud.com'; + const adminRole = await prisma.role.findUniqueOrThrow({ where: { name: 'ADMIN' } }); + const existingAdmin = await prisma.user.findUnique({ where: { email: adminEmail } }); + if (!existingAdmin) { + await prisma.user.create({ + data: { + email: adminEmail, + fullName: 'System Administrator', + roles: { create: { roleId: adminRole.id } }, + }, + }); + console.log(`Created admin user ${adminEmail} — signs in via the Account Manager, no local password.`); + } + + console.log('Seed complete.'); +} + +main() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/smokeTest.ts b/scripts/smokeTest.ts new file mode 100644 index 0000000..25b3a46 --- /dev/null +++ b/scripts/smokeTest.ts @@ -0,0 +1,95 @@ +/** + * Phase 1 smoke test: posts a balanced manual journal entry, confirms an unbalanced one is + * rejected, then prints a simple Main + per-branch debit/credit sum to confirm everything + * still balances. Run after `npm run prisma:seed` against a real database: + * npm run smoke + */ +import { PrismaClient } from '@prisma/client'; +import { PostingService } from '../src/modules/gl/posting.service'; +import { UnbalancedEntryError } from '../src/modules/gl/posting.types'; + +const prisma = new PrismaClient(); + +async function getAccount(accountCode: string) { + return prisma.gLAccount.findUniqueOrThrow({ where: { accountCode } }); +} + +async function main() { + const admin = await prisma.user.findUniqueOrThrow({ where: { email: 'joosth2002@icloud.com' } }); + const bankHQ = await getAccount('1000-000-000-HQ'); + const salesHQ = await getAccount('4000-000-000-HQ'); + const bankAMA = await getAccount('1000-000-000-AMA'); + const salesAMA = await getAccount('4000-000-000-AMA'); + + console.log('Posting a balanced entry at HQ...'); + const hqEntry = await PostingService.postEntry({ + journalCode: 'GJ', + entryDate: new Date(), + description: 'Smoke test cash sale — HQ', + sourceModule: 'GL', + sourceType: 'SmokeTest', + createdById: admin.id, + lines: [ + { glAccountId: bankHQ.id, debit: 1000, credit: 0, branchSegmentValueId: bankHQ.id }, + { glAccountId: salesHQ.id, debit: 0, credit: 1000, branchSegmentValueId: salesHQ.id }, + ], + }); + console.log(` Posted ${hqEntry.entryNumber}: debits ${hqEntry.totalDebits} / credits ${hqEntry.totalCredits}`); + + console.log('Posting a balanced entry at AMA...'); + const amaEntry = await PostingService.postEntry({ + journalCode: 'GJ', + entryDate: new Date(), + description: 'Smoke test cash sale — AMA', + sourceModule: 'GL', + sourceType: 'SmokeTest', + createdById: admin.id, + lines: [ + { glAccountId: bankAMA.id, debit: 400, credit: 0 }, + { glAccountId: salesAMA.id, debit: 0, credit: 400 }, + ], + }); + console.log(` Posted ${amaEntry.entryNumber}: debits ${amaEntry.totalDebits} / credits ${amaEntry.totalCredits}`); + + console.log('Attempting an unbalanced entry (should be rejected)...'); + try { + await PostingService.postEntry({ + journalCode: 'GJ', + entryDate: new Date(), + description: 'This should never persist', + sourceModule: 'GL', + sourceType: 'SmokeTest', + createdById: admin.id, + lines: [ + { glAccountId: bankHQ.id, debit: 100, credit: 0 }, + { glAccountId: salesHQ.id, debit: 0, credit: 50 }, + ], + }); + throw new Error('Unbalanced entry was accepted — this is a bug in PostingService'); + } catch (error) { + if (error instanceof UnbalancedEntryError) { + console.log(` Correctly rejected: ${error.message}`); + } else { + throw error; + } + } + + console.log('Trial balance check (sum of all posted lines, grouped by account type)...'); + const lines = await prisma.journalEntryLine.findMany({ include: { glAccount: true } }); + const totalDebits = lines.reduce((total, line) => total + Number(line.debit), 0); + const totalCredits = lines.reduce((total, line) => total + Number(line.credit), 0); + console.log(` Main: total debits ${totalDebits.toFixed(2)}, total credits ${totalCredits.toFixed(2)}`); + if (totalDebits !== totalCredits) { + throw new Error('Trial balance does not sum to zero — double-entry invariant violated'); + } + console.log('Smoke test passed: every posted entry balances.'); +} + +main() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..f014e75 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,54 @@ +import cors from 'cors'; +import express, { Express } from 'express'; +import helmet from 'helmet'; +import { errorHandler } from './middleware/errorHandler'; +import { authRouter } from './modules/auth/auth.routes'; +import { segmentsRouter } from './modules/company/segments.routes'; +import { fiscalPeriodsRouter } from './modules/company/fiscalPeriods.routes'; +import { glAccountsRouter } from './modules/gl/glAccounts.routes'; +import { journalsRouter } from './modules/gl/journals.routes'; +import { journalEntriesRouter } from './modules/gl/journalEntries.routes'; +import { usersRouter } from './modules/users/users.routes'; +import { rolesRouter } from './modules/users/roles.routes'; +import { taxCodesRouter } from './modules/tax/taxCodes.routes'; +import { customersRouter } from './modules/ar/customers.routes'; +import { arInvoicesRouter } from './modules/ar/arInvoices.routes'; +import { arCreditNotesRouter } from './modules/ar/arCreditNotes.routes'; +import { arReceiptsRouter } from './modules/ar/arReceipts.routes'; +import { suppliersRouter } from './modules/ap/suppliers.routes'; +import { apInvoicesRouter } from './modules/ap/apInvoices.routes'; +import { apDebitNotesRouter } from './modules/ap/apDebitNotes.routes'; +import { apPaymentsRouter } from './modules/ap/apPayments.routes'; + +export function createApp(): Express { + const app = express(); + + app.use(helmet()); + app.use(cors()); + app.use(express.json()); + + app.get('/health', (req, res) => res.json({ status: 'ok' })); + + app.use('/api/auth', authRouter); + app.use('/api/company', segmentsRouter); + app.use('/api/company', fiscalPeriodsRouter); + app.use('/api/gl/accounts', glAccountsRouter); + app.use('/api/gl/journals', journalsRouter); + app.use('/api/gl/journal-entries', journalEntriesRouter); + app.use('/api/users', usersRouter); + app.use('/api/roles', rolesRouter); + app.use('/api/tax/codes', taxCodesRouter); + app.use('/api/ar/customers', customersRouter); + app.use('/api/ar/invoices', arInvoicesRouter); + app.use('/api/ar/credit-notes', arCreditNotesRouter); + app.use('/api/ar/receipts', arReceiptsRouter); + app.use('/api/ap/suppliers', suppliersRouter); + app.use('/api/ap/invoices', apInvoicesRouter); + app.use('/api/ap/debit-notes', apDebitNotesRouter); + app.use('/api/ap/payments', apPaymentsRouter); + + app.use((req, res) => res.status(404).json({ error: 'Not found' })); + app.use(errorHandler); + + return app; +} diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..5b35cc6 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,34 @@ +import 'dotenv/config'; +import fs from 'fs'; +import path from 'path'; + +function required(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +function loadHandoffPublicKeys(): string[] { + const rawPaths = required('HANDOFF_PUBLIC_KEY_PATHS'); + return rawPaths + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => fs.readFileSync(path.resolve(process.cwd(), entry), 'utf8')); +} + +export const env = { + port: Number(process.env.PORT ?? 4000), + databaseUrl: required('DATABASE_URL'), + jwtAccessSecret: required('JWT_ACCESS_SECRET'), + jwtRefreshSecret: required('JWT_REFRESH_SECRET'), + jwtAccessTtl: process.env.JWT_ACCESS_TTL ?? '15m', + jwtRefreshTtl: process.env.JWT_REFRESH_TTL ?? '7d', + companySlug: required('COMPANY_SLUG'), + // Public keys this deployment trusts to verify a handoff token's RS256 signature — a comma + // separated list of PEM file paths, so a key can be rotated by trusting the old and new key + // simultaneously during the overlap window. The Manager alone holds the matching private key. + handoffPublicKeys: loadHandoffPublicKeys(), +}; diff --git a/src/lib/asyncHandler.ts b/src/lib/asyncHandler.ts new file mode 100644 index 0000000..118019e --- /dev/null +++ b/src/lib/asyncHandler.ts @@ -0,0 +1,10 @@ +import { NextFunction, Request, Response } from 'express'; + +type Handler = (req: Request, res: Response, next: NextFunction) => Promise | void; + +/** Wraps an async route/controller handler so a rejected promise reaches errorHandler via next(). */ +export function asyncHandler(handler: Handler) { + return (req: Request, res: Response, next: NextFunction): void => { + Promise.resolve(handler(req, res, next)).catch(next); + }; +} diff --git a/src/lib/branchFilter.ts b/src/lib/branchFilter.ts new file mode 100644 index 0000000..fe22ab8 --- /dev/null +++ b/src/lib/branchFilter.ts @@ -0,0 +1,41 @@ +import { Request } from 'express'; + +export type BranchScope = { mode: 'MAIN' } | { mode: 'BRANCH'; branchSegmentValueId: number }; + +export class ForbiddenBranchScopeError extends Error { + constructor(message = 'You are not permitted to view this branch') { + super(message); + this.name = 'ForbiddenBranchScopeError'; + } +} + +/** + * Resolves the requested "Main" (all branches) vs single-branch scope from the + * `branchScope` query param, then forces it down to what the user is actually + * permitted to see. A BRANCH_USER can never widen their view to MAIN or to a + * branch outside their UserBranchRestriction rows, regardless of what the client asked for. + */ +export function resolveBranchScope(req: Request): BranchScope { + const requested = req.query.branchScope; + const allowed = req.user?.allowedBranchSegmentValueIds ?? null; + + if (requested === undefined || requested === 'MAIN') { + if (allowed === null) return { mode: 'MAIN' }; + if (allowed.length === 1) return { mode: 'BRANCH', branchSegmentValueId: allowed[0] }; + throw new ForbiddenBranchScopeError('Select a single branch — you do not have access to the Main consolidated view'); + } + + const branchSegmentValueId = Number(requested); + if (!Number.isInteger(branchSegmentValueId)) { + throw new ForbiddenBranchScopeError('Invalid branchScope value'); + } + if (allowed !== null && !allowed.includes(branchSegmentValueId)) { + throw new ForbiddenBranchScopeError(); + } + return { mode: 'BRANCH', branchSegmentValueId }; +} + +/** Prisma `where` fragment for a column-level branch filter (AR/AP/Stock tables). */ +export function branchWhere(scope: BranchScope, column = 'branchSegmentValueId'): Record { + return scope.mode === 'MAIN' ? {} : { [column]: scope.branchSegmentValueId }; +} diff --git a/src/lib/controlAccounts.ts b/src/lib/controlAccounts.ts new file mode 100644 index 0000000..d898acb --- /dev/null +++ b/src/lib/controlAccounts.ts @@ -0,0 +1,35 @@ +import { ControlAccountType, GLAccount, Prisma } from '@prisma/client'; + +type TxClient = Prisma.TransactionClient; + +export class ControlAccountNotFoundError extends Error { + constructor(controlType: ControlAccountType, branchSegmentValueId: number) { + super(`No active ${controlType} control account is configured for branch segment value ${branchSegmentValueId}`); + this.name = 'ControlAccountNotFoundError'; + } +} + +/** + * Every branch gets its own full set of control accounts (see prisma/seed.ts — accounts are + * created per branch segment value), so AR/AP/Stock postings resolve "the AR control account" + * as "the control account of this type whose segments include this branch," not a single + * global account. + */ +export async function resolveControlAccount( + tx: TxClient, + branchSegmentValueId: number, + controlType: ControlAccountType, +): Promise { + const account = await tx.gLAccount.findFirst({ + where: { + controlType, + isControlAccount: true, + isActive: true, + segmentValues: { some: { segmentValueId: branchSegmentValueId } }, + }, + }); + if (!account) { + throw new ControlAccountNotFoundError(controlType, branchSegmentValueId); + } + return account; +} diff --git a/src/lib/handoffVerify.ts b/src/lib/handoffVerify.ts new file mode 100644 index 0000000..97dcce9 --- /dev/null +++ b/src/lib/handoffVerify.ts @@ -0,0 +1,50 @@ +import jwt from 'jsonwebtoken'; +import { env } from '../config/env'; + +export interface HandoffClaims { + sub: string; + pid: number; + cid: number; + companySlug: string; + jti: string; +} + +export class InvalidHandoffTokenError extends Error { + constructor(message = 'Invalid or expired handoff token') { + super(message); + this.name = 'InvalidHandoffTokenError'; + } +} + +export class WrongCompanyHandoffError extends Error { + constructor() { + super('This handoff token was not issued for this company'); + this.name = 'WrongCompanyHandoffError'; + } +} + +/** + * Verifies a handoff token minted by the Account Manager. Tries every trusted public key (see + * env.handoffPublicKeys) so a key can be rotated without a hard cutover. Only signature/expiry + * and audience (companySlug) are checked here — membership/authorization already happened at + * the Manager before the token was minted. + */ +export function verifyHandoffToken(token: string): HandoffClaims { + for (const publicKey of env.handoffPublicKeys) { + try { + const claims = jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as HandoffClaims & { jti?: string }; + if (!claims.jti) { + throw new InvalidHandoffTokenError(); + } + if (claims.companySlug !== env.companySlug) { + throw new WrongCompanyHandoffError(); + } + return claims as HandoffClaims; + } catch (error) { + if (error instanceof WrongCompanyHandoffError) throw error; + // Signature/expiry failure against this key — try the next trusted key, if any, rather + // than leaking the underlying jsonwebtoken library message to the API response. + } + } + throw new InvalidHandoffTokenError(); +} diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts new file mode 100644 index 0000000..bd61b41 --- /dev/null +++ b/src/lib/jwt.ts @@ -0,0 +1,21 @@ +import jwt, { SignOptions } from 'jsonwebtoken'; +import { env } from '../config/env'; + +export interface TokenPayload { + sub: string; +} + +function sign(userId: number, secret: string, ttl: string): string { + const options: SignOptions = { expiresIn: ttl as SignOptions['expiresIn'] }; + return jwt.sign({ sub: String(userId) } satisfies TokenPayload, secret, options); +} + +function verify(token: string, secret: string): number { + const payload = jwt.verify(token, secret) as unknown as TokenPayload; + return Number(payload.sub); +} + +export const signAccessToken = (userId: number): string => sign(userId, env.jwtAccessSecret, env.jwtAccessTtl); +export const signRefreshToken = (userId: number): string => sign(userId, env.jwtRefreshSecret, env.jwtRefreshTtl); +export const verifyAccessToken = (token: string): number => verify(token, env.jwtAccessSecret); +export const verifyRefreshToken = (token: string): number => verify(token, env.jwtRefreshSecret); diff --git a/src/lib/money.ts b/src/lib/money.ts new file mode 100644 index 0000000..8fb60c4 --- /dev/null +++ b/src/lib/money.ts @@ -0,0 +1,12 @@ +import { Prisma } from '@prisma/client'; + +export type Money = Prisma.Decimal; + +export const money = (value: Prisma.Decimal.Value): Money => new Prisma.Decimal(value); + +export const ZERO = money(0); + +export const sum = (values: Money[]): Money => + values.reduce((total, value) => total.plus(value), ZERO); + +export const isEqual = (a: Money, b: Money): boolean => a.equals(b); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..6e7d688 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,12 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // eslint-disable-next-line no-var + var __prisma__: PrismaClient | undefined; +} + +export const prisma = global.__prisma__ ?? new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + global.__prisma__ = prisma; +} diff --git a/src/lib/sequence.ts b/src/lib/sequence.ts new file mode 100644 index 0000000..1133519 --- /dev/null +++ b/src/lib/sequence.ts @@ -0,0 +1,18 @@ +import { Prisma } from '@prisma/client'; + +type TxClient = Prisma.TransactionClient; + +/** + * Issues the next gap-free number in a named series, e.g. nextNumber(tx, 'JE', 2026) -> "JE-2026-000123". + * Must be called from inside the same transaction as the document it numbers, so a rolled-back + * document doesn't leave a permanent gap-causing side effect outside the transaction boundary. + */ +export async function nextNumber(tx: TxClient, prefix: string, fiscalYear: number): Promise { + const key = `${prefix}-${fiscalYear}`; + const sequence = await tx.sequence.upsert({ + where: { key }, + create: { key, lastValue: 1 }, + update: { lastValue: { increment: 1 } }, + }); + return `${prefix}-${fiscalYear}-${String(sequence.lastValue).padStart(6, '0')}`; +} diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..bf4b257 --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,54 @@ +import { NextFunction, Request, Response } from 'express'; +import { prisma } from '../lib/prisma'; +import { verifyAccessToken } from '../lib/jwt'; +import { AuthUser } from '../types/express'; + +export async function loadAuthUser(userId: number): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } }, + branchRestrictions: true, + }, + }); + if (!user || !user.isActive) return null; + + const roles = user.roles.map((userRole) => userRole.role.name); + const permissions = Array.from( + new Set( + user.roles.flatMap((userRole) => userRole.role.permissions.map((rolePermission) => rolePermission.permission.key)), + ), + ); + const isBranchRestricted = roles.includes('BRANCH_USER') && user.branchRestrictions.length > 0; + + return { + id: user.id, + email: user.email, + roles, + permissions, + allowedBranchSegmentValueIds: isBranchRestricted + ? user.branchRestrictions.map((restriction) => restriction.branchSegmentValueId) + : null, + }; +} + +export async function requireAuth(req: Request, res: Response, next: NextFunction): Promise { + const header = req.headers.authorization; + if (!header?.startsWith('Bearer ')) { + res.status(401).json({ error: 'Missing bearer token' }); + return; + } + + try { + const userId = verifyAccessToken(header.slice('Bearer '.length)); + const authUser = await loadAuthUser(userId); + if (!authUser) { + res.status(401).json({ error: 'User not found or inactive' }); + return; + } + req.user = authUser; + next(); + } catch { + res.status(401).json({ error: 'Invalid or expired token' }); + } +} diff --git a/src/middleware/branchScope.ts b/src/middleware/branchScope.ts new file mode 100644 index 0000000..c0729d8 --- /dev/null +++ b/src/middleware/branchScope.ts @@ -0,0 +1,15 @@ +import { NextFunction, Request, Response } from 'express'; +import { ForbiddenBranchScopeError, resolveBranchScope } from '../lib/branchFilter'; + +export function attachBranchScope(req: Request, res: Response, next: NextFunction): void { + try { + req.branchScope = resolveBranchScope(req); + next(); + } catch (error) { + if (error instanceof ForbiddenBranchScopeError) { + res.status(403).json({ error: error.message }); + return; + } + next(error); + } +} diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts new file mode 100644 index 0000000..5802250 --- /dev/null +++ b/src/middleware/errorHandler.ts @@ -0,0 +1,43 @@ +import { NextFunction, Request, Response } from 'express'; +import { Prisma } from '@prisma/client'; +import { UnbalancedEntryError } from '../modules/gl/posting.types'; +import { ForbiddenBranchScopeError } from '../lib/branchFilter'; +import { ControlAccountNotFoundError } from '../lib/controlAccounts'; +import { ARDocumentStateError, ARAllocationError } from '../modules/ar/ar.errors'; +import { APDocumentStateError, APAllocationError } from '../modules/ap/ap.errors'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function errorHandler(error: unknown, req: Request, res: Response, next: NextFunction): void { + if (error instanceof UnbalancedEntryError) { + res.status(422).json({ error: error.message }); + return; + } + if (error instanceof ForbiddenBranchScopeError) { + res.status(403).json({ error: error.message }); + return; + } + if ( + error instanceof ControlAccountNotFoundError || + error instanceof ARDocumentStateError || + error instanceof ARAllocationError || + error instanceof APDocumentStateError || + error instanceof APAllocationError + ) { + res.status(422).json({ error: error.message }); + return; + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + res.status(409).json({ error: 'A record with this value already exists', meta: error.meta }); + return; + } + if (error.code === 'P2025') { + res.status(404).json({ error: 'Record not found' }); + return; + } + } + + // eslint-disable-next-line no-console + console.error(error); + res.status(500).json({ error: 'Internal server error' }); +} diff --git a/src/middleware/rbac.ts b/src/middleware/rbac.ts new file mode 100644 index 0000000..a840369 --- /dev/null +++ b/src/middleware/rbac.ts @@ -0,0 +1,15 @@ +import { NextFunction, Request, Response } from 'express'; + +export function requirePermission(permissionKey: string) { + return (req: Request, res: Response, next: NextFunction): void => { + if (!req.user) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + if (!req.user.permissions.includes(permissionKey)) { + res.status(403).json({ error: `Missing permission: ${permissionKey}` }); + return; + } + next(); + }; +} diff --git a/src/middleware/validate.ts b/src/middleware/validate.ts new file mode 100644 index 0000000..4e402fb --- /dev/null +++ b/src/middleware/validate.ts @@ -0,0 +1,19 @@ +import { NextFunction, Request, Response } from 'express'; +import { AnyZodObject, ZodError } from 'zod'; + +type Section = 'body' | 'query' | 'params'; + +export function validate(schema: AnyZodObject, section: Section = 'body') { + return (req: Request, res: Response, next: NextFunction): void => { + try { + req[section] = schema.parse(req[section]); + next(); + } catch (error) { + if (error instanceof ZodError) { + res.status(400).json({ error: 'Validation failed', details: error.flatten() }); + return; + } + next(error); + } + }; +} diff --git a/src/modules/ap/ap.errors.ts b/src/modules/ap/ap.errors.ts new file mode 100644 index 0000000..970703c --- /dev/null +++ b/src/modules/ap/ap.errors.ts @@ -0,0 +1,13 @@ +export class APDocumentStateError extends Error { + constructor(message: string) { + super(message); + this.name = 'APDocumentStateError'; + } +} + +export class APAllocationError extends Error { + constructor(message: string) { + super(message); + this.name = 'APAllocationError'; + } +} diff --git a/src/modules/ap/apAllocations.service.ts b/src/modules/ap/apAllocations.service.ts new file mode 100644 index 0000000..47e7fe3 --- /dev/null +++ b/src/modules/ap/apAllocations.service.ts @@ -0,0 +1,67 @@ +import { prisma } from '../../lib/prisma'; +import { money } from '../../lib/money'; +import { APAllocationError, APDocumentStateError } from './ap.errors'; + +export interface AllocationInput { + invoiceId: number; + amount: number; +} + +/** + * Applies one or more (invoiceId, amount) allocations against a single payment or debit note. + * Purely sub-ledger bookkeeping — the cash/credit movement already hit the AP control account + * in aggregate when the payment/debit note was posted; this only records which invoice(s) it + * settles and reduces their outstanding balances. No GL entries are created here. + */ +export async function allocate(source: { paymentId?: number; debitNoteId?: number }, allocations: AllocationInput[]) { + if (allocations.length === 0) { + throw new APAllocationError('At least one allocation is required'); + } + + return prisma.$transaction(async (tx) => { + let remaining = money(0); + if (source.paymentId) { + const payment = await tx.aPPayment.findUniqueOrThrow({ where: { id: source.paymentId } }); + if (payment.status !== 'POSTED') throw new APDocumentStateError('Only a posted payment can be allocated'); + remaining = money(payment.unallocatedAmount); + } else if (source.debitNoteId) { + const debitNote = await tx.aPDebitNote.findUniqueOrThrow({ where: { id: source.debitNoteId } }); + if (debitNote.status !== 'POSTED') throw new APDocumentStateError('Only a posted debit note can be allocated'); + remaining = money(debitNote.balance); + } else { + throw new APAllocationError('An allocation must reference a payment or a debit note'); + } + + const totalRequested = allocations.reduce((sum, a) => sum.plus(money(a.amount)), money(0)); + if (totalRequested.greaterThan(remaining)) { + throw new APAllocationError(`Allocation total (${totalRequested.toFixed(2)}) exceeds the unallocated amount (${remaining.toFixed(2)})`); + } + + for (const line of allocations) { + const amount = money(line.amount); + if (amount.lessThanOrEqualTo(0)) { + throw new APAllocationError('Each allocation amount must be greater than zero'); + } + const invoice = await tx.aPInvoice.findUniqueOrThrow({ where: { id: line.invoiceId } }); + if (invoice.status !== 'POSTED') { + throw new APDocumentStateError(`Invoice ${invoice.invoiceNumber} is not posted and cannot receive an allocation`); + } + if (amount.greaterThan(invoice.balance)) { + throw new APAllocationError(`Allocation of ${amount.toFixed(2)} exceeds invoice ${invoice.invoiceNumber}'s balance of ${money(invoice.balance).toFixed(2)}`); + } + + await tx.aPAllocation.create({ + data: { invoiceId: line.invoiceId, paymentId: source.paymentId, debitNoteId: source.debitNoteId, amount }, + }); + await tx.aPInvoice.update({ where: { id: line.invoiceId }, data: { balance: { decrement: amount } } }); + } + + if (source.paymentId) { + await tx.aPPayment.update({ where: { id: source.paymentId }, data: { unallocatedAmount: { decrement: totalRequested } } }); + } else if (source.debitNoteId) { + await tx.aPDebitNote.update({ where: { id: source.debitNoteId }, data: { balance: { decrement: totalRequested } } }); + } + + return tx.aPAllocation.findMany({ where: source.paymentId ? { paymentId: source.paymentId } : { debitNoteId: source.debitNoteId } }); + }); +} diff --git a/src/modules/ap/apDebitNotes.controller.ts b/src/modules/ap/apDebitNotes.controller.ts new file mode 100644 index 0000000..bc4b114 --- /dev/null +++ b/src/modules/ap/apDebitNotes.controller.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as apDebitNotesService from './apDebitNotes.service'; + +const debitNoteLineSchema = z.object({ + description: z.string().min(1), + quantity: z.number().positive(), + unitPrice: z.number().min(0), + expenseGLAccountId: z.number().int(), + taxCodeId: z.number().int().optional(), +}); + +export const createDebitNoteSchema = z.object({ + supplierId: z.number().int(), + debitNoteDate: z.string().datetime().or(z.string().date()), + reference: z.string().optional(), + reason: z.string().optional(), + lines: z.array(debitNoteLineSchema).min(1), +}); + +export const allocateDebitNoteSchema = z.object({ + allocations: z.array(z.object({ invoiceId: z.number().int(), amount: z.number().positive() })).min(1), +}); + +export async function createDebitNoteHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const debitNote = await apDebitNotesService.createDebitNoteDraft(input, req.user!.id); + res.status(201).json(debitNote); +} + +export async function postDebitNoteHandler(req: Request, res: Response): Promise { + const debitNote = await apDebitNotesService.postDebitNote(Number(req.params.id), req.user!.id); + res.json(debitNote); +} + +export async function voidDebitNoteHandler(req: Request, res: Response): Promise { + const debitNote = await apDebitNotesService.voidDebitNoteDraft(Number(req.params.id)); + res.json(debitNote); +} + +export async function allocateDebitNoteHandler(req: Request, res: Response): Promise { + const { allocations } = req.body as z.infer; + const result = await apDebitNotesService.allocateDebitNote(Number(req.params.id), allocations); + res.status(201).json(result); +} + +export async function listDebitNotesHandler(req: Request, res: Response): Promise { + const supplierId = req.query.supplierId ? Number(req.query.supplierId) : undefined; + res.json(await apDebitNotesService.listDebitNotes(req.branchScope!, supplierId)); +} + +export async function getDebitNoteHandler(req: Request, res: Response): Promise { + const debitNote = await apDebitNotesService.getDebitNote(Number(req.params.id)); + if (!debitNote) { + res.status(404).json({ error: 'Debit note not found' }); + return; + } + res.json(debitNote); +} diff --git a/src/modules/ap/apDebitNotes.routes.ts b/src/modules/ap/apDebitNotes.routes.ts new file mode 100644 index 0000000..08b34d6 --- /dev/null +++ b/src/modules/ap/apDebitNotes.routes.ts @@ -0,0 +1,37 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + allocateDebitNoteHandler, + allocateDebitNoteSchema, + createDebitNoteHandler, + createDebitNoteSchema, + getDebitNoteHandler, + listDebitNotesHandler, + postDebitNoteHandler, + voidDebitNoteHandler, +} from './apDebitNotes.controller'; + +export const apDebitNotesRouter = Router(); + +apDebitNotesRouter.use(requireAuth); + +apDebitNotesRouter.get('/', attachBranchScope, asyncHandler(listDebitNotesHandler)); +apDebitNotesRouter.get('/:id', asyncHandler(getDebitNoteHandler)); +apDebitNotesRouter.post( + '/', + requirePermission('ap.debitnote.manage'), + validate(createDebitNoteSchema), + asyncHandler(createDebitNoteHandler), +); +apDebitNotesRouter.post('/:id/post', requirePermission('ap.debitnote.manage'), asyncHandler(postDebitNoteHandler)); +apDebitNotesRouter.post('/:id/void', requirePermission('ap.debitnote.manage'), asyncHandler(voidDebitNoteHandler)); +apDebitNotesRouter.post( + '/:id/allocations', + requirePermission('ap.debitnote.manage'), + validate(allocateDebitNoteSchema), + asyncHandler(allocateDebitNoteHandler), +); diff --git a/src/modules/ap/apDebitNotes.service.ts b/src/modules/ap/apDebitNotes.service.ts new file mode 100644 index 0000000..71aace9 --- /dev/null +++ b/src/modules/ap/apDebitNotes.service.ts @@ -0,0 +1,119 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; +import { sumLines, calcLine, postDebitNoteToGL } from './apPosting.service'; +import { nextNumber } from '../../lib/sequence'; +import { APDocumentStateError } from './ap.errors'; +import { allocate, AllocationInput } from './apAllocations.service'; + +export interface DebitNoteLineInput { + description: string; + quantity: number; + unitPrice: number; + expenseGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateDebitNoteInput { + supplierId: number; + debitNoteDate: string; + reference?: string; + reason?: string; + lines: DebitNoteLineInput[]; +} + +export async function createDebitNoteDraft(input: CreateDebitNoteInput, createdById: number) { + const supplier = await prisma.supplier.findUniqueOrThrow({ where: { id: input.supplierId } }); + + const taxCodeIds = input.lines.map((line) => line.taxCodeId).filter((id): id is number => id != null); + const taxCodes = await prisma.taxCode.findMany({ where: { id: { in: taxCodeIds } } }); + const taxCodeById = new Map(taxCodes.map((taxCode) => [taxCode.id, taxCode])); + + const computedLines = input.lines.map((line, index) => { + const rate = line.taxCodeId ? taxCodeById.get(line.taxCodeId) : undefined; + const { subtotal, tax, total } = calcLine(line.quantity, line.unitPrice, rate ? Number(rate.ratePercent) : null); + return { + lineNumber: index + 1, + description: line.description, + quantity: line.quantity, + unitPrice: line.unitPrice, + expenseGLAccountId: line.expenseGLAccountId, + taxCodeId: line.taxCodeId, + lineSubtotal: subtotal, + lineTax: tax, + lineTotal: total, + }; + }); + + const subtotal = sumLines(computedLines.map((line) => line.lineSubtotal)); + const taxTotal = sumLines(computedLines.map((line) => line.lineTax)); + const total = subtotal.plus(taxTotal); + + return prisma.$transaction(async (tx) => { + const debitNoteNumber = await nextNumber(tx, 'APDN', new Date(input.debitNoteDate).getFullYear()); + return tx.aPDebitNote.create({ + data: { + debitNoteNumber, + supplierId: supplier.id, + branchSegmentValueId: supplier.branchSegmentValueId, + debitNoteDate: new Date(input.debitNoteDate), + reference: input.reference, + reason: input.reason, + status: 'DRAFT', + subtotal, + taxTotal, + total, + balance: total, + createdById, + lines: { create: computedLines }, + }, + include: { lines: true, supplier: true }, + }); + }); +} + +export async function postDebitNote(id: number, createdById: number) { + const debitNote = await prisma.aPDebitNote.findUniqueOrThrow({ where: { id }, include: { lines: true, supplier: true } }); + if (debitNote.status !== 'DRAFT') { + throw new APDocumentStateError('Only draft debit notes can be posted'); + } + + const entry = await postDebitNoteToGL(debitNote, createdById); + + return prisma.aPDebitNote.update({ + where: { id }, + data: { status: 'POSTED', journalEntryId: entry.id }, + include: { lines: true, supplier: true }, + }); +} + +export async function voidDebitNoteDraft(id: number) { + const debitNote = await prisma.aPDebitNote.findUniqueOrThrow({ where: { id } }); + if (debitNote.status !== 'DRAFT') { + throw new APDocumentStateError('Only draft debit notes can be voided'); + } + return prisma.aPDebitNote.update({ where: { id }, data: { status: 'VOID' } }); +} + +export function allocateDebitNote(debitNoteId: number, allocations: AllocationInput[]) { + return allocate({ debitNoteId }, allocations); +} + +export function listDebitNotes(branchScope: BranchScope, supplierId?: number) { + return prisma.aPDebitNote.findMany({ + where: { ...branchWhere(branchScope), ...(supplierId ? { supplierId } : {}) }, + include: { supplier: true }, + orderBy: { debitNoteDate: 'desc' }, + take: 200, + }); +} + +export function getDebitNote(id: number) { + return prisma.aPDebitNote.findUnique({ + where: { id }, + include: { + lines: { include: { expenseGLAccount: true, taxCode: true } }, + supplier: true, + allocations: { include: { invoice: true } }, + }, + }); +} diff --git a/src/modules/ap/apInvoices.controller.ts b/src/modules/ap/apInvoices.controller.ts new file mode 100644 index 0000000..a225527 --- /dev/null +++ b/src/modules/ap/apInvoices.controller.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as apInvoicesService from './apInvoices.service'; + +const invoiceLineSchema = z.object({ + description: z.string().min(1), + quantity: z.number().positive(), + unitPrice: z.number().min(0), + expenseGLAccountId: z.number().int(), + taxCodeId: z.number().int().optional(), +}); + +export const createInvoiceSchema = z.object({ + supplierId: z.number().int(), + invoiceDate: z.string().datetime().or(z.string().date()), + dueDate: z.string().datetime().or(z.string().date()), + supplierReference: z.string().optional(), + lines: z.array(invoiceLineSchema).min(1), +}); + +export async function createInvoiceHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const invoice = await apInvoicesService.createInvoiceDraft(input, req.user!.id); + res.status(201).json(invoice); +} + +export async function postInvoiceHandler(req: Request, res: Response): Promise { + const invoice = await apInvoicesService.postInvoice(Number(req.params.id), req.user!.id); + res.json(invoice); +} + +export async function voidInvoiceHandler(req: Request, res: Response): Promise { + const invoice = await apInvoicesService.voidInvoiceDraft(Number(req.params.id)); + res.json(invoice); +} + +export async function listInvoicesHandler(req: Request, res: Response): Promise { + const supplierId = req.query.supplierId ? Number(req.query.supplierId) : undefined; + res.json(await apInvoicesService.listInvoices(req.branchScope!, supplierId)); +} + +export async function getInvoiceHandler(req: Request, res: Response): Promise { + const invoice = await apInvoicesService.getInvoice(Number(req.params.id)); + if (!invoice) { + res.status(404).json({ error: 'Invoice not found' }); + return; + } + res.json(invoice); +} diff --git a/src/modules/ap/apInvoices.routes.ts b/src/modules/ap/apInvoices.routes.ts new file mode 100644 index 0000000..125e79e --- /dev/null +++ b/src/modules/ap/apInvoices.routes.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createInvoiceHandler, + createInvoiceSchema, + getInvoiceHandler, + listInvoicesHandler, + postInvoiceHandler, + voidInvoiceHandler, +} from './apInvoices.controller'; + +export const apInvoicesRouter = Router(); + +apInvoicesRouter.use(requireAuth); + +apInvoicesRouter.get('/', attachBranchScope, asyncHandler(listInvoicesHandler)); +apInvoicesRouter.get('/:id', asyncHandler(getInvoiceHandler)); +apInvoicesRouter.post('/', requirePermission('ap.invoice.manage'), validate(createInvoiceSchema), asyncHandler(createInvoiceHandler)); +apInvoicesRouter.post('/:id/post', requirePermission('ap.invoice.manage'), asyncHandler(postInvoiceHandler)); +apInvoicesRouter.post('/:id/void', requirePermission('ap.invoice.manage'), asyncHandler(voidInvoiceHandler)); diff --git a/src/modules/ap/apInvoices.service.ts b/src/modules/ap/apInvoices.service.ts new file mode 100644 index 0000000..1d6b726 --- /dev/null +++ b/src/modules/ap/apInvoices.service.ts @@ -0,0 +1,114 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; +import { sumLines, calcLine, postSupplierInvoiceToGL } from './apPosting.service'; +import { nextNumber } from '../../lib/sequence'; +import { APDocumentStateError } from './ap.errors'; + +export interface InvoiceLineInput { + description: string; + quantity: number; + unitPrice: number; + expenseGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateInvoiceInput { + supplierId: number; + invoiceDate: string; + dueDate: string; + supplierReference?: string; + lines: InvoiceLineInput[]; +} + +export async function createInvoiceDraft(input: CreateInvoiceInput, createdById: number) { + const supplier = await prisma.supplier.findUniqueOrThrow({ where: { id: input.supplierId } }); + + const taxCodeIds = input.lines.map((line) => line.taxCodeId).filter((id): id is number => id != null); + const taxCodes = await prisma.taxCode.findMany({ where: { id: { in: taxCodeIds } } }); + const taxCodeById = new Map(taxCodes.map((taxCode) => [taxCode.id, taxCode])); + + const computedLines = input.lines.map((line, index) => { + const rate = line.taxCodeId ? taxCodeById.get(line.taxCodeId) : undefined; + const { subtotal, tax, total } = calcLine(line.quantity, line.unitPrice, rate ? Number(rate.ratePercent) : null); + return { + lineNumber: index + 1, + description: line.description, + quantity: line.quantity, + unitPrice: line.unitPrice, + expenseGLAccountId: line.expenseGLAccountId, + taxCodeId: line.taxCodeId, + lineSubtotal: subtotal, + lineTax: tax, + lineTotal: total, + }; + }); + + const subtotal = sumLines(computedLines.map((line) => line.lineSubtotal)); + const taxTotal = sumLines(computedLines.map((line) => line.lineTax)); + const total = subtotal.plus(taxTotal); + + return prisma.$transaction(async (tx) => { + const invoiceNumber = await nextNumber(tx, 'APINV', new Date(input.invoiceDate).getFullYear()); + return tx.aPInvoice.create({ + data: { + invoiceNumber, + supplierId: supplier.id, + supplierReference: input.supplierReference, + branchSegmentValueId: supplier.branchSegmentValueId, + invoiceDate: new Date(input.invoiceDate), + dueDate: new Date(input.dueDate), + status: 'DRAFT', + subtotal, + taxTotal, + total, + balance: total, + createdById, + lines: { create: computedLines }, + }, + include: { lines: true, supplier: true }, + }); + }); +} + +export async function postInvoice(id: number, createdById: number) { + const invoice = await prisma.aPInvoice.findUniqueOrThrow({ where: { id }, include: { lines: true, supplier: true } }); + if (invoice.status !== 'DRAFT') { + throw new APDocumentStateError('Only draft invoices can be posted'); + } + + const entry = await postSupplierInvoiceToGL(invoice, createdById); + + return prisma.aPInvoice.update({ + where: { id }, + data: { status: 'POSTED', journalEntryId: entry.id }, + include: { lines: true, supplier: true }, + }); +} + +export async function voidInvoiceDraft(id: number) { + const invoice = await prisma.aPInvoice.findUniqueOrThrow({ where: { id } }); + if (invoice.status !== 'DRAFT') { + throw new APDocumentStateError('Only draft invoices can be voided — a posted invoice must be corrected with a debit note'); + } + return prisma.aPInvoice.update({ where: { id }, data: { status: 'VOID' } }); +} + +export function listInvoices(branchScope: BranchScope, supplierId?: number) { + return prisma.aPInvoice.findMany({ + where: { ...branchWhere(branchScope), ...(supplierId ? { supplierId } : {}) }, + include: { supplier: true }, + orderBy: { invoiceDate: 'desc' }, + take: 200, + }); +} + +export function getInvoice(id: number) { + return prisma.aPInvoice.findUnique({ + where: { id }, + include: { + lines: { include: { expenseGLAccount: true, taxCode: true } }, + supplier: true, + allocations: { include: { payment: true, debitNote: true } }, + }, + }); +} diff --git a/src/modules/ap/apPayments.controller.ts b/src/modules/ap/apPayments.controller.ts new file mode 100644 index 0000000..0821b43 --- /dev/null +++ b/src/modules/ap/apPayments.controller.ts @@ -0,0 +1,51 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as apPaymentsService from './apPayments.service'; + +export const createPaymentSchema = z.object({ + supplierId: z.number().int(), + paymentDate: z.string().datetime().or(z.string().date()), + amount: z.number().positive(), + bankGLAccountId: z.number().int(), + reference: z.string().optional(), +}); + +export const allocateSchema = z.object({ + allocations: z.array(z.object({ invoiceId: z.number().int(), amount: z.number().positive() })).min(1), +}); + +export async function createPaymentHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const payment = await apPaymentsService.createPaymentDraft(input, req.user!.id); + res.status(201).json(payment); +} + +export async function postPaymentHandler(req: Request, res: Response): Promise { + const payment = await apPaymentsService.postPayment(Number(req.params.id), req.user!.id); + res.json(payment); +} + +export async function voidPaymentHandler(req: Request, res: Response): Promise { + const payment = await apPaymentsService.voidPaymentDraft(Number(req.params.id)); + res.json(payment); +} + +export async function allocatePaymentHandler(req: Request, res: Response): Promise { + const { allocations } = req.body as z.infer; + const result = await apPaymentsService.allocatePayment(Number(req.params.id), allocations); + res.status(201).json(result); +} + +export async function listPaymentsHandler(req: Request, res: Response): Promise { + const supplierId = req.query.supplierId ? Number(req.query.supplierId) : undefined; + res.json(await apPaymentsService.listPayments(req.branchScope!, supplierId)); +} + +export async function getPaymentHandler(req: Request, res: Response): Promise { + const payment = await apPaymentsService.getPayment(Number(req.params.id)); + if (!payment) { + res.status(404).json({ error: 'Payment not found' }); + return; + } + res.json(payment); +} diff --git a/src/modules/ap/apPayments.routes.ts b/src/modules/ap/apPayments.routes.ts new file mode 100644 index 0000000..d027aec --- /dev/null +++ b/src/modules/ap/apPayments.routes.ts @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + allocatePaymentHandler, + allocateSchema, + createPaymentHandler, + createPaymentSchema, + getPaymentHandler, + listPaymentsHandler, + postPaymentHandler, + voidPaymentHandler, +} from './apPayments.controller'; + +export const apPaymentsRouter = Router(); + +apPaymentsRouter.use(requireAuth); + +apPaymentsRouter.get('/', attachBranchScope, asyncHandler(listPaymentsHandler)); +apPaymentsRouter.get('/:id', asyncHandler(getPaymentHandler)); +apPaymentsRouter.post('/', requirePermission('ap.payment.manage'), validate(createPaymentSchema), asyncHandler(createPaymentHandler)); +apPaymentsRouter.post('/:id/post', requirePermission('ap.payment.manage'), asyncHandler(postPaymentHandler)); +apPaymentsRouter.post('/:id/void', requirePermission('ap.payment.manage'), asyncHandler(voidPaymentHandler)); +apPaymentsRouter.post( + '/:id/allocations', + requirePermission('ap.payment.manage'), + validate(allocateSchema), + asyncHandler(allocatePaymentHandler), +); diff --git a/src/modules/ap/apPayments.service.ts b/src/modules/ap/apPayments.service.ts new file mode 100644 index 0000000..3c0dcc6 --- /dev/null +++ b/src/modules/ap/apPayments.service.ts @@ -0,0 +1,82 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; +import { money } from '../../lib/money'; +import { nextNumber } from '../../lib/sequence'; +import { postPaymentToGL } from './apPosting.service'; +import { APDocumentStateError } from './ap.errors'; +import { allocate, AllocationInput } from './apAllocations.service'; + +export interface CreatePaymentInput { + supplierId: number; + paymentDate: string; + amount: number; + bankGLAccountId: number; + reference?: string; +} + +export async function createPaymentDraft(input: CreatePaymentInput, createdById: number) { + const supplier = await prisma.supplier.findUniqueOrThrow({ where: { id: input.supplierId } }); + const amount = money(input.amount); + + return prisma.$transaction(async (tx) => { + const paymentNumber = await nextNumber(tx, 'APPMT', new Date(input.paymentDate).getFullYear()); + return tx.aPPayment.create({ + data: { + paymentNumber, + supplierId: supplier.id, + branchSegmentValueId: supplier.branchSegmentValueId, + paymentDate: new Date(input.paymentDate), + amount, + unallocatedAmount: amount, + bankGLAccountId: input.bankGLAccountId, + reference: input.reference, + status: 'DRAFT', + createdById, + }, + include: { supplier: true, bankGLAccount: true }, + }); + }); +} + +export async function postPayment(id: number, createdById: number) { + const payment = await prisma.aPPayment.findUniqueOrThrow({ where: { id }, include: { supplier: true } }); + if (payment.status !== 'DRAFT') { + throw new APDocumentStateError('Only draft payments can be posted'); + } + + const entry = await postPaymentToGL(payment, createdById); + + return prisma.aPPayment.update({ + where: { id }, + data: { status: 'POSTED', journalEntryId: entry.id }, + include: { supplier: true, bankGLAccount: true }, + }); +} + +export async function voidPaymentDraft(id: number) { + const payment = await prisma.aPPayment.findUniqueOrThrow({ where: { id } }); + if (payment.status !== 'DRAFT') { + throw new APDocumentStateError('Only draft payments can be voided'); + } + return prisma.aPPayment.update({ where: { id }, data: { status: 'VOID' } }); +} + +export function allocatePayment(paymentId: number, allocations: AllocationInput[]) { + return allocate({ paymentId }, allocations); +} + +export function listPayments(branchScope: BranchScope, supplierId?: number) { + return prisma.aPPayment.findMany({ + where: { ...branchWhere(branchScope), ...(supplierId ? { supplierId } : {}) }, + include: { supplier: true, bankGLAccount: true }, + orderBy: { paymentDate: 'desc' }, + take: 200, + }); +} + +export function getPayment(id: number) { + return prisma.aPPayment.findUnique({ + where: { id }, + include: { supplier: true, bankGLAccount: true, allocations: { include: { invoice: true } } }, + }); +} diff --git a/src/modules/ap/apPosting.service.ts b/src/modules/ap/apPosting.service.ts new file mode 100644 index 0000000..4a9d2db --- /dev/null +++ b/src/modules/ap/apPosting.service.ts @@ -0,0 +1,161 @@ +import { APDebitNote, APDebitNoteLine, APInvoice, APInvoiceLine, APPayment, JournalEntry, Supplier } from '@prisma/client'; +import { prisma } from '../../lib/prisma'; +import { money, sum, ZERO } from '../../lib/money'; +import { resolveControlAccount } from '../../lib/controlAccounts'; +import { PostingService } from '../gl/posting.service'; +import { JournalEntryDraft, JournalLineDraft } from '../gl/posting.types'; + +type InvoiceForPosting = APInvoice & { lines: APInvoiceLine[]; supplier: Supplier }; +type DebitNoteForPosting = APDebitNote & { lines: APDebitNoteLine[]; supplier: Supplier }; +type PaymentForPosting = APPayment & { supplier: Supplier }; + +function groupByAccount(entries: { glAccountId: number; amount: import('../../lib/money').Money }[]) { + const byAccount = new Map(); + for (const entry of entries) { + byAccount.set(entry.glAccountId, (byAccount.get(entry.glAccountId) ?? ZERO).plus(entry.amount)); + } + return byAccount; +} + +/** Dr expense account(s) (subtotal) + Dr Input Tax (tax) / Cr AP Control (total). */ +export async function postSupplierInvoiceToGL(invoice: InvoiceForPosting, createdById: number): Promise { + const apAccount = await resolveControlAccount(prisma, invoice.branchSegmentValueId, 'AP'); + const taxTotal = money(invoice.taxTotal); + const taxAccount = taxTotal.greaterThan(0) ? await resolveControlAccount(prisma, invoice.branchSegmentValueId, 'TAX_INPUT') : null; + + const expenseByAccount = groupByAccount( + invoice.lines.map((line) => ({ glAccountId: line.expenseGLAccountId, amount: money(line.lineSubtotal) })), + ); + + const lines: JournalLineDraft[] = [ + ...Array.from(expenseByAccount.entries()).map(([glAccountId, amount]) => ({ + glAccountId, + debit: amount, + credit: 0, + branchSegmentValueId: invoice.branchSegmentValueId, + description: `Supplier invoice ${invoice.invoiceNumber} — ${invoice.supplier.name}`, + })), + ]; + if (taxAccount && taxTotal.greaterThan(0)) { + lines.push({ + glAccountId: taxAccount.id, + debit: taxTotal, + credit: 0, + branchSegmentValueId: invoice.branchSegmentValueId, + description: `Input tax on supplier invoice ${invoice.invoiceNumber}`, + }); + } + lines.push({ + glAccountId: apAccount.id, + debit: 0, + credit: invoice.total, + branchSegmentValueId: invoice.branchSegmentValueId, + apInvoiceId: invoice.id, + description: `Supplier invoice ${invoice.invoiceNumber} — ${invoice.supplier.name}`, + }); + + const draft: JournalEntryDraft = { + journalCode: 'PURCHASES', + entryDate: invoice.invoiceDate, + description: `AP Invoice ${invoice.invoiceNumber} — ${invoice.supplier.name}`, + sourceModule: 'AP', + sourceType: 'APInvoice', + sourceId: invoice.id, + createdById, + lines, + }; + return PostingService.postEntry(draft); +} + +/** Reverse of a supplier invoice: Dr AP Control / Cr expense account(s) + Cr Input Tax. */ +export async function postDebitNoteToGL(debitNote: DebitNoteForPosting, createdById: number): Promise { + const apAccount = await resolveControlAccount(prisma, debitNote.branchSegmentValueId, 'AP'); + const taxTotal = money(debitNote.taxTotal); + const taxAccount = taxTotal.greaterThan(0) + ? await resolveControlAccount(prisma, debitNote.branchSegmentValueId, 'TAX_INPUT') + : null; + + const expenseByAccount = groupByAccount( + debitNote.lines.map((line) => ({ glAccountId: line.expenseGLAccountId, amount: money(line.lineSubtotal) })), + ); + + const lines: JournalLineDraft[] = [ + { + glAccountId: apAccount.id, + debit: debitNote.total, + credit: 0, + branchSegmentValueId: debitNote.branchSegmentValueId, + description: `Debit note ${debitNote.debitNoteNumber} — ${debitNote.supplier.name}`, + }, + ...Array.from(expenseByAccount.entries()).map(([glAccountId, amount]) => ({ + glAccountId, + debit: 0, + credit: amount, + branchSegmentValueId: debitNote.branchSegmentValueId, + description: `Debit note ${debitNote.debitNoteNumber} — ${debitNote.supplier.name}`, + })), + ]; + if (taxAccount && taxTotal.greaterThan(0)) { + lines.push({ + glAccountId: taxAccount.id, + debit: 0, + credit: taxTotal, + branchSegmentValueId: debitNote.branchSegmentValueId, + description: `Input tax reversed on debit note ${debitNote.debitNoteNumber}`, + }); + } + + const draft: JournalEntryDraft = { + journalCode: 'PURCHASES', + entryDate: debitNote.debitNoteDate, + description: `AP Debit Note ${debitNote.debitNoteNumber} — ${debitNote.supplier.name}`, + sourceModule: 'AP', + sourceType: 'APDebitNote', + sourceId: debitNote.id, + createdById, + lines, + }; + return PostingService.postEntry(draft); +} + +/** Dr AP Control / Cr Bank — allocation against specific invoices happens separately, sub-ledger only. */ +export async function postPaymentToGL(payment: PaymentForPosting, createdById: number): Promise { + const apAccount = await resolveControlAccount(prisma, payment.branchSegmentValueId, 'AP'); + + const draft: JournalEntryDraft = { + journalCode: 'CASH_PAYMENT', + entryDate: payment.paymentDate, + description: `AP Payment ${payment.paymentNumber} — ${payment.supplier.name}`, + sourceModule: 'AP', + sourceType: 'APPayment', + sourceId: payment.id, + createdById, + lines: [ + { + glAccountId: apAccount.id, + debit: payment.amount, + credit: 0, + branchSegmentValueId: payment.branchSegmentValueId, + description: `Payment ${payment.paymentNumber} — ${payment.supplier.name}`, + }, + { + glAccountId: payment.bankGLAccountId, + debit: 0, + credit: payment.amount, + branchSegmentValueId: payment.branchSegmentValueId, + description: `Payment ${payment.paymentNumber} — ${payment.supplier.name}`, + }, + ], + }; + return PostingService.postEntry(draft); +} + +export function calcLine(quantity: number, unitPrice: number, ratePercent: number | null) { + const subtotal = money(quantity).times(unitPrice).toDecimalPlaces(2); + const tax = ratePercent ? subtotal.times(ratePercent).dividedBy(100).toDecimalPlaces(2) : ZERO; + return { subtotal, tax, total: subtotal.plus(tax) }; +} + +export function sumLines(values: import('../../lib/money').Money[]) { + return sum(values); +} diff --git a/src/modules/ap/suppliers.controller.ts b/src/modules/ap/suppliers.controller.ts new file mode 100644 index 0000000..ad3fe3c --- /dev/null +++ b/src/modules/ap/suppliers.controller.ts @@ -0,0 +1,39 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as suppliersService from './suppliers.service'; + +export const createSupplierSchema = z.object({ + code: z.string().min(1), + name: z.string().min(1), + email: z.string().email().optional(), + phone: z.string().optional(), + address: z.string().optional(), + taxNumber: z.string().optional(), + branchSegmentValueId: z.number().int(), + paymentTermsDays: z.number().int().min(0).optional(), +}); + +export const updateSupplierSchema = createSupplierSchema.partial().extend({ isActive: z.boolean().optional() }); + +export async function listSuppliersHandler(req: Request, res: Response): Promise { + res.json(await suppliersService.listSuppliers(req.branchScope!)); +} + +export async function createSupplierHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await suppliersService.createSupplier(input)); +} + +export async function updateSupplierHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.json(await suppliersService.updateSupplier(Number(req.params.id), input)); +} + +export async function getSupplierHandler(req: Request, res: Response): Promise { + const result = await suppliersService.getSupplier(Number(req.params.id)); + if (!result) { + res.status(404).json({ error: 'Supplier not found' }); + return; + } + res.json(result); +} diff --git a/src/modules/ap/suppliers.routes.ts b/src/modules/ap/suppliers.routes.ts new file mode 100644 index 0000000..e1574da --- /dev/null +++ b/src/modules/ap/suppliers.routes.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createSupplierHandler, + createSupplierSchema, + getSupplierHandler, + listSuppliersHandler, + updateSupplierHandler, + updateSupplierSchema, +} from './suppliers.controller'; + +export const suppliersRouter = Router(); + +suppliersRouter.use(requireAuth); + +suppliersRouter.get('/', attachBranchScope, asyncHandler(listSuppliersHandler)); +suppliersRouter.get('/:id', asyncHandler(getSupplierHandler)); +suppliersRouter.post('/', requirePermission('ap.supplier.manage'), validate(createSupplierSchema), asyncHandler(createSupplierHandler)); +suppliersRouter.put( + '/:id', + requirePermission('ap.supplier.manage'), + validate(updateSupplierSchema), + asyncHandler(updateSupplierHandler), +); diff --git a/src/modules/ap/suppliers.service.ts b/src/modules/ap/suppliers.service.ts new file mode 100644 index 0000000..b5773ad --- /dev/null +++ b/src/modules/ap/suppliers.service.ts @@ -0,0 +1,57 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; + +export interface SupplierInput { + code: string; + name: string; + email?: string; + phone?: string; + address?: string; + taxNumber?: string; + branchSegmentValueId: number; + paymentTermsDays?: number; +} + +export function listSuppliers(branchScope: BranchScope) { + return prisma.supplier.findMany({ + where: branchWhere(branchScope), + orderBy: { name: 'asc' }, + }); +} + +export function createSupplier(input: SupplierInput) { + return prisma.supplier.create({ data: input }); +} + +export function updateSupplier(id: number, input: Partial & { isActive?: boolean }) { + return prisma.supplier.update({ where: { id }, data: input }); +} + +export async function getSupplier(id: number) { + const supplier = await prisma.supplier.findUnique({ where: { id } }); + if (!supplier) return null; + + const [invoices, debitNotes, payments] = await Promise.all([ + prisma.aPInvoice.findMany({ + where: { supplierId: id, status: { not: 'DRAFT' } }, + orderBy: { invoiceDate: 'desc' }, + include: { lines: true }, + }), + prisma.aPDebitNote.findMany({ + where: { supplierId: id, status: { not: 'DRAFT' } }, + orderBy: { debitNoteDate: 'desc' }, + include: { lines: true }, + }), + prisma.aPPayment.findMany({ + where: { supplierId: id, status: { not: 'DRAFT' } }, + orderBy: { paymentDate: 'desc' }, + }), + ]); + + const outstandingBalance = invoices.reduce((sum, invoice) => sum + Number(invoice.balance), 0); + const overdueBalance = invoices + .filter((invoice) => Number(invoice.balance) > 0 && invoice.dueDate < new Date()) + .reduce((sum, invoice) => sum + Number(invoice.balance), 0); + + return { supplier, invoices, debitNotes, payments, outstandingBalance, overdueBalance }; +} diff --git a/src/modules/ar/ar.errors.ts b/src/modules/ar/ar.errors.ts new file mode 100644 index 0000000..1ff78ea --- /dev/null +++ b/src/modules/ar/ar.errors.ts @@ -0,0 +1,13 @@ +export class ARDocumentStateError extends Error { + constructor(message: string) { + super(message); + this.name = 'ARDocumentStateError'; + } +} + +export class ARAllocationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ARAllocationError'; + } +} diff --git a/src/modules/ar/arAllocations.service.ts b/src/modules/ar/arAllocations.service.ts new file mode 100644 index 0000000..a130e3d --- /dev/null +++ b/src/modules/ar/arAllocations.service.ts @@ -0,0 +1,70 @@ +import { prisma } from '../../lib/prisma'; +import { money } from '../../lib/money'; +import { ARAllocationError, ARDocumentStateError } from './ar.errors'; + +export interface AllocationInput { + invoiceId: number; + amount: number; +} + +/** + * Applies one or more (invoiceId, amount) allocations against a single receipt or credit note. + * Purely sub-ledger bookkeeping — the cash/credit movement already hit the AR control account + * in aggregate when the receipt/credit note was posted; this only records which invoice(s) it + * settles and reduces their outstanding balances. No GL entries are created here. + */ +export async function allocate( + source: { receiptId?: number; creditNoteId?: number }, + allocations: AllocationInput[], +) { + if (allocations.length === 0) { + throw new ARAllocationError('At least one allocation is required'); + } + + return prisma.$transaction(async (tx) => { + let remaining = money(0); + if (source.receiptId) { + const receipt = await tx.aRReceipt.findUniqueOrThrow({ where: { id: source.receiptId } }); + if (receipt.status !== 'POSTED') throw new ARDocumentStateError('Only a posted receipt can be allocated'); + remaining = money(receipt.unallocatedAmount); + } else if (source.creditNoteId) { + const creditNote = await tx.aRCreditNote.findUniqueOrThrow({ where: { id: source.creditNoteId } }); + if (creditNote.status !== 'POSTED') throw new ARDocumentStateError('Only a posted credit note can be allocated'); + remaining = money(creditNote.balance); + } else { + throw new ARAllocationError('An allocation must reference a receipt or a credit note'); + } + + const totalRequested = allocations.reduce((sum, a) => sum.plus(money(a.amount)), money(0)); + if (totalRequested.greaterThan(remaining)) { + throw new ARAllocationError(`Allocation total (${totalRequested.toFixed(2)}) exceeds the unallocated amount (${remaining.toFixed(2)})`); + } + + for (const line of allocations) { + const amount = money(line.amount); + if (amount.lessThanOrEqualTo(0)) { + throw new ARAllocationError('Each allocation amount must be greater than zero'); + } + const invoice = await tx.aRInvoice.findUniqueOrThrow({ where: { id: line.invoiceId } }); + if (invoice.status !== 'POSTED') { + throw new ARDocumentStateError(`Invoice ${invoice.invoiceNumber} is not posted and cannot receive an allocation`); + } + if (amount.greaterThan(invoice.balance)) { + throw new ARAllocationError(`Allocation of ${amount.toFixed(2)} exceeds invoice ${invoice.invoiceNumber}'s balance of ${money(invoice.balance).toFixed(2)}`); + } + + await tx.aRAllocation.create({ + data: { invoiceId: line.invoiceId, receiptId: source.receiptId, creditNoteId: source.creditNoteId, amount }, + }); + await tx.aRInvoice.update({ where: { id: line.invoiceId }, data: { balance: { decrement: amount } } }); + } + + if (source.receiptId) { + await tx.aRReceipt.update({ where: { id: source.receiptId }, data: { unallocatedAmount: { decrement: totalRequested } } }); + } else if (source.creditNoteId) { + await tx.aRCreditNote.update({ where: { id: source.creditNoteId }, data: { balance: { decrement: totalRequested } } }); + } + + return tx.aRAllocation.findMany({ where: source.receiptId ? { receiptId: source.receiptId } : { creditNoteId: source.creditNoteId } }); + }); +} diff --git a/src/modules/ar/arCreditNotes.controller.ts b/src/modules/ar/arCreditNotes.controller.ts new file mode 100644 index 0000000..1580a44 --- /dev/null +++ b/src/modules/ar/arCreditNotes.controller.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as arCreditNotesService from './arCreditNotes.service'; + +const creditNoteLineSchema = z.object({ + description: z.string().min(1), + quantity: z.number().positive(), + unitPrice: z.number().min(0), + revenueGLAccountId: z.number().int(), + taxCodeId: z.number().int().optional(), +}); + +export const createCreditNoteSchema = z.object({ + customerId: z.number().int(), + creditNoteDate: z.string().datetime().or(z.string().date()), + reference: z.string().optional(), + reason: z.string().optional(), + lines: z.array(creditNoteLineSchema).min(1), +}); + +export const allocateCreditNoteSchema = z.object({ + allocations: z.array(z.object({ invoiceId: z.number().int(), amount: z.number().positive() })).min(1), +}); + +export async function createCreditNoteHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const creditNote = await arCreditNotesService.createCreditNoteDraft(input, req.user!.id); + res.status(201).json(creditNote); +} + +export async function postCreditNoteHandler(req: Request, res: Response): Promise { + const creditNote = await arCreditNotesService.postCreditNote(Number(req.params.id), req.user!.id); + res.json(creditNote); +} + +export async function voidCreditNoteHandler(req: Request, res: Response): Promise { + const creditNote = await arCreditNotesService.voidCreditNoteDraft(Number(req.params.id)); + res.json(creditNote); +} + +export async function allocateCreditNoteHandler(req: Request, res: Response): Promise { + const { allocations } = req.body as z.infer; + const result = await arCreditNotesService.allocateCreditNote(Number(req.params.id), allocations); + res.status(201).json(result); +} + +export async function listCreditNotesHandler(req: Request, res: Response): Promise { + const customerId = req.query.customerId ? Number(req.query.customerId) : undefined; + res.json(await arCreditNotesService.listCreditNotes(req.branchScope!, customerId)); +} + +export async function getCreditNoteHandler(req: Request, res: Response): Promise { + const creditNote = await arCreditNotesService.getCreditNote(Number(req.params.id)); + if (!creditNote) { + res.status(404).json({ error: 'Credit note not found' }); + return; + } + res.json(creditNote); +} diff --git a/src/modules/ar/arCreditNotes.routes.ts b/src/modules/ar/arCreditNotes.routes.ts new file mode 100644 index 0000000..5fb9f5c --- /dev/null +++ b/src/modules/ar/arCreditNotes.routes.ts @@ -0,0 +1,37 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + allocateCreditNoteHandler, + allocateCreditNoteSchema, + createCreditNoteHandler, + createCreditNoteSchema, + getCreditNoteHandler, + listCreditNotesHandler, + postCreditNoteHandler, + voidCreditNoteHandler, +} from './arCreditNotes.controller'; + +export const arCreditNotesRouter = Router(); + +arCreditNotesRouter.use(requireAuth); + +arCreditNotesRouter.get('/', attachBranchScope, asyncHandler(listCreditNotesHandler)); +arCreditNotesRouter.get('/:id', asyncHandler(getCreditNoteHandler)); +arCreditNotesRouter.post( + '/', + requirePermission('ar.creditnote.manage'), + validate(createCreditNoteSchema), + asyncHandler(createCreditNoteHandler), +); +arCreditNotesRouter.post('/:id/post', requirePermission('ar.creditnote.manage'), asyncHandler(postCreditNoteHandler)); +arCreditNotesRouter.post('/:id/void', requirePermission('ar.creditnote.manage'), asyncHandler(voidCreditNoteHandler)); +arCreditNotesRouter.post( + '/:id/allocations', + requirePermission('ar.creditnote.manage'), + validate(allocateCreditNoteSchema), + asyncHandler(allocateCreditNoteHandler), +); diff --git a/src/modules/ar/arCreditNotes.service.ts b/src/modules/ar/arCreditNotes.service.ts new file mode 100644 index 0000000..8f6d0b4 --- /dev/null +++ b/src/modules/ar/arCreditNotes.service.ts @@ -0,0 +1,119 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; +import { sumLines, calcLine, postCreditNoteToGL } from './arPosting.service'; +import { nextNumber } from '../../lib/sequence'; +import { ARDocumentStateError } from './ar.errors'; +import { allocate, AllocationInput } from './arAllocations.service'; + +export interface CreditNoteLineInput { + description: string; + quantity: number; + unitPrice: number; + revenueGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateCreditNoteInput { + customerId: number; + creditNoteDate: string; + reference?: string; + reason?: string; + lines: CreditNoteLineInput[]; +} + +export async function createCreditNoteDraft(input: CreateCreditNoteInput, createdById: number) { + const customer = await prisma.customer.findUniqueOrThrow({ where: { id: input.customerId } }); + + const taxCodeIds = input.lines.map((line) => line.taxCodeId).filter((id): id is number => id != null); + const taxCodes = await prisma.taxCode.findMany({ where: { id: { in: taxCodeIds } } }); + const taxCodeById = new Map(taxCodes.map((taxCode) => [taxCode.id, taxCode])); + + const computedLines = input.lines.map((line, index) => { + const rate = line.taxCodeId ? taxCodeById.get(line.taxCodeId) : undefined; + const { subtotal, tax, total } = calcLine(line.quantity, line.unitPrice, rate ? Number(rate.ratePercent) : null); + return { + lineNumber: index + 1, + description: line.description, + quantity: line.quantity, + unitPrice: line.unitPrice, + revenueGLAccountId: line.revenueGLAccountId, + taxCodeId: line.taxCodeId, + lineSubtotal: subtotal, + lineTax: tax, + lineTotal: total, + }; + }); + + const subtotal = sumLines(computedLines.map((line) => line.lineSubtotal)); + const taxTotal = sumLines(computedLines.map((line) => line.lineTax)); + const total = subtotal.plus(taxTotal); + + return prisma.$transaction(async (tx) => { + const creditNoteNumber = await nextNumber(tx, 'ARCN', new Date(input.creditNoteDate).getFullYear()); + return tx.aRCreditNote.create({ + data: { + creditNoteNumber, + customerId: customer.id, + branchSegmentValueId: customer.branchSegmentValueId, + creditNoteDate: new Date(input.creditNoteDate), + reference: input.reference, + reason: input.reason, + status: 'DRAFT', + subtotal, + taxTotal, + total, + balance: total, + createdById, + lines: { create: computedLines }, + }, + include: { lines: true, customer: true }, + }); + }); +} + +export async function postCreditNote(id: number, createdById: number) { + const creditNote = await prisma.aRCreditNote.findUniqueOrThrow({ where: { id }, include: { lines: true, customer: true } }); + if (creditNote.status !== 'DRAFT') { + throw new ARDocumentStateError('Only draft credit notes can be posted'); + } + + const entry = await postCreditNoteToGL(creditNote, createdById); + + return prisma.aRCreditNote.update({ + where: { id }, + data: { status: 'POSTED', journalEntryId: entry.id }, + include: { lines: true, customer: true }, + }); +} + +export async function voidCreditNoteDraft(id: number) { + const creditNote = await prisma.aRCreditNote.findUniqueOrThrow({ where: { id } }); + if (creditNote.status !== 'DRAFT') { + throw new ARDocumentStateError('Only draft credit notes can be voided'); + } + return prisma.aRCreditNote.update({ where: { id }, data: { status: 'VOID' } }); +} + +export function allocateCreditNote(creditNoteId: number, allocations: AllocationInput[]) { + return allocate({ creditNoteId }, allocations); +} + +export function listCreditNotes(branchScope: BranchScope, customerId?: number) { + return prisma.aRCreditNote.findMany({ + where: { ...branchWhere(branchScope), ...(customerId ? { customerId } : {}) }, + include: { customer: true }, + orderBy: { creditNoteDate: 'desc' }, + take: 200, + }); +} + +export function getCreditNote(id: number) { + return prisma.aRCreditNote.findUnique({ + where: { id }, + include: { + lines: { include: { revenueGLAccount: true, taxCode: true } }, + customer: true, + allocations: { include: { invoice: true } }, + }, + }); +} diff --git a/src/modules/ar/arInvoices.controller.ts b/src/modules/ar/arInvoices.controller.ts new file mode 100644 index 0000000..ba4bf34 --- /dev/null +++ b/src/modules/ar/arInvoices.controller.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as arInvoicesService from './arInvoices.service'; + +const invoiceLineSchema = z.object({ + description: z.string().min(1), + quantity: z.number().positive(), + unitPrice: z.number().min(0), + revenueGLAccountId: z.number().int(), + taxCodeId: z.number().int().optional(), +}); + +export const createInvoiceSchema = z.object({ + customerId: z.number().int(), + invoiceDate: z.string().datetime().or(z.string().date()), + dueDate: z.string().datetime().or(z.string().date()), + reference: z.string().optional(), + lines: z.array(invoiceLineSchema).min(1), +}); + +export async function createInvoiceHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const invoice = await arInvoicesService.createInvoiceDraft(input, req.user!.id); + res.status(201).json(invoice); +} + +export async function postInvoiceHandler(req: Request, res: Response): Promise { + const invoice = await arInvoicesService.postInvoice(Number(req.params.id), req.user!.id); + res.json(invoice); +} + +export async function voidInvoiceHandler(req: Request, res: Response): Promise { + const invoice = await arInvoicesService.voidInvoiceDraft(Number(req.params.id)); + res.json(invoice); +} + +export async function listInvoicesHandler(req: Request, res: Response): Promise { + const customerId = req.query.customerId ? Number(req.query.customerId) : undefined; + res.json(await arInvoicesService.listInvoices(req.branchScope!, customerId)); +} + +export async function getInvoiceHandler(req: Request, res: Response): Promise { + const invoice = await arInvoicesService.getInvoice(Number(req.params.id)); + if (!invoice) { + res.status(404).json({ error: 'Invoice not found' }); + return; + } + res.json(invoice); +} diff --git a/src/modules/ar/arInvoices.routes.ts b/src/modules/ar/arInvoices.routes.ts new file mode 100644 index 0000000..2251e51 --- /dev/null +++ b/src/modules/ar/arInvoices.routes.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createInvoiceHandler, + createInvoiceSchema, + getInvoiceHandler, + listInvoicesHandler, + postInvoiceHandler, + voidInvoiceHandler, +} from './arInvoices.controller'; + +export const arInvoicesRouter = Router(); + +arInvoicesRouter.use(requireAuth); + +arInvoicesRouter.get('/', attachBranchScope, asyncHandler(listInvoicesHandler)); +arInvoicesRouter.get('/:id', asyncHandler(getInvoiceHandler)); +arInvoicesRouter.post('/', requirePermission('ar.invoice.manage'), validate(createInvoiceSchema), asyncHandler(createInvoiceHandler)); +arInvoicesRouter.post('/:id/post', requirePermission('ar.invoice.manage'), asyncHandler(postInvoiceHandler)); +arInvoicesRouter.post('/:id/void', requirePermission('ar.invoice.manage'), asyncHandler(voidInvoiceHandler)); diff --git a/src/modules/ar/arInvoices.service.ts b/src/modules/ar/arInvoices.service.ts new file mode 100644 index 0000000..556957f --- /dev/null +++ b/src/modules/ar/arInvoices.service.ts @@ -0,0 +1,114 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; +import { sumLines, calcLine, postInvoiceToGL } from './arPosting.service'; +import { nextNumber } from '../../lib/sequence'; +import { ARDocumentStateError } from './ar.errors'; + +export interface InvoiceLineInput { + description: string; + quantity: number; + unitPrice: number; + revenueGLAccountId: number; + taxCodeId?: number; +} + +export interface CreateInvoiceInput { + customerId: number; + invoiceDate: string; + dueDate: string; + reference?: string; + lines: InvoiceLineInput[]; +} + +export async function createInvoiceDraft(input: CreateInvoiceInput, createdById: number) { + const customer = await prisma.customer.findUniqueOrThrow({ where: { id: input.customerId } }); + + const taxCodeIds = input.lines.map((line) => line.taxCodeId).filter((id): id is number => id != null); + const taxCodes = await prisma.taxCode.findMany({ where: { id: { in: taxCodeIds } } }); + const taxCodeById = new Map(taxCodes.map((taxCode) => [taxCode.id, taxCode])); + + const computedLines = input.lines.map((line, index) => { + const rate = line.taxCodeId ? taxCodeById.get(line.taxCodeId) : undefined; + const { subtotal, tax, total } = calcLine(line.quantity, line.unitPrice, rate ? Number(rate.ratePercent) : null); + return { + lineNumber: index + 1, + description: line.description, + quantity: line.quantity, + unitPrice: line.unitPrice, + revenueGLAccountId: line.revenueGLAccountId, + taxCodeId: line.taxCodeId, + lineSubtotal: subtotal, + lineTax: tax, + lineTotal: total, + }; + }); + + const subtotal = sumLines(computedLines.map((line) => line.lineSubtotal)); + const taxTotal = sumLines(computedLines.map((line) => line.lineTax)); + const total = subtotal.plus(taxTotal); + + return prisma.$transaction(async (tx) => { + const invoiceNumber = await nextNumber(tx, 'ARINV', new Date(input.invoiceDate).getFullYear()); + return tx.aRInvoice.create({ + data: { + invoiceNumber, + customerId: customer.id, + branchSegmentValueId: customer.branchSegmentValueId, + invoiceDate: new Date(input.invoiceDate), + dueDate: new Date(input.dueDate), + reference: input.reference, + status: 'DRAFT', + subtotal, + taxTotal, + total, + balance: total, + createdById, + lines: { create: computedLines }, + }, + include: { lines: true, customer: true }, + }); + }); +} + +export async function postInvoice(id: number, createdById: number) { + const invoice = await prisma.aRInvoice.findUniqueOrThrow({ where: { id }, include: { lines: true, customer: true } }); + if (invoice.status !== 'DRAFT') { + throw new ARDocumentStateError('Only draft invoices can be posted'); + } + + const entry = await postInvoiceToGL(invoice, createdById); + + return prisma.aRInvoice.update({ + where: { id }, + data: { status: 'POSTED', journalEntryId: entry.id }, + include: { lines: true, customer: true }, + }); +} + +export async function voidInvoiceDraft(id: number) { + const invoice = await prisma.aRInvoice.findUniqueOrThrow({ where: { id } }); + if (invoice.status !== 'DRAFT') { + throw new ARDocumentStateError('Only draft invoices can be voided — a posted invoice must be corrected with a credit note'); + } + return prisma.aRInvoice.update({ where: { id }, data: { status: 'VOID' } }); +} + +export function listInvoices(branchScope: BranchScope, customerId?: number) { + return prisma.aRInvoice.findMany({ + where: { ...branchWhere(branchScope), ...(customerId ? { customerId } : {}) }, + include: { customer: true }, + orderBy: { invoiceDate: 'desc' }, + take: 200, + }); +} + +export function getInvoice(id: number) { + return prisma.aRInvoice.findUnique({ + where: { id }, + include: { + lines: { include: { revenueGLAccount: true, taxCode: true } }, + customer: true, + allocations: { include: { receipt: true, creditNote: true } }, + }, + }); +} diff --git a/src/modules/ar/arPosting.service.ts b/src/modules/ar/arPosting.service.ts new file mode 100644 index 0000000..ff6d4b7 --- /dev/null +++ b/src/modules/ar/arPosting.service.ts @@ -0,0 +1,162 @@ +import { ARCreditNote, ARCreditNoteLine, ARInvoice, ARInvoiceLine, ARReceipt, Customer, JournalEntry } from '@prisma/client'; +import { prisma } from '../../lib/prisma'; +import { money, sum, ZERO } from '../../lib/money'; +import { resolveControlAccount } from '../../lib/controlAccounts'; +import { PostingService } from '../gl/posting.service'; +import { JournalEntryDraft, JournalLineDraft } from '../gl/posting.types'; + +type InvoiceForPosting = ARInvoice & { lines: ARInvoiceLine[]; customer: Customer }; +type CreditNoteForPosting = ARCreditNote & { lines: ARCreditNoteLine[]; customer: Customer }; +type ReceiptForPosting = ARReceipt & { customer: Customer }; + +/** Groups line amounts by GL account so one entry doesn't carry a separate line per invoice line. */ +function groupByAccount(entries: { glAccountId: number; amount: import('../../lib/money').Money }[]) { + const byAccount = new Map(); + for (const entry of entries) { + byAccount.set(entry.glAccountId, (byAccount.get(entry.glAccountId) ?? ZERO).plus(entry.amount)); + } + return byAccount; +} + +/** Dr AR Control (total) / Cr revenue account(s) (subtotal) / Cr Output Tax (tax total). */ +export async function postInvoiceToGL(invoice: InvoiceForPosting, createdById: number): Promise { + const arAccount = await resolveControlAccount(prisma, invoice.branchSegmentValueId, 'AR'); + const taxTotal = money(invoice.taxTotal); + const taxAccount = taxTotal.greaterThan(0) ? await resolveControlAccount(prisma, invoice.branchSegmentValueId, 'TAX_OUTPUT') : null; + + const revenueByAccount = groupByAccount( + invoice.lines.map((line) => ({ glAccountId: line.revenueGLAccountId, amount: money(line.lineSubtotal) })), + ); + + const lines: JournalLineDraft[] = [ + { + glAccountId: arAccount.id, + debit: invoice.total, + credit: 0, + branchSegmentValueId: invoice.branchSegmentValueId, + arInvoiceId: invoice.id, + description: `Invoice ${invoice.invoiceNumber} — ${invoice.customer.name}`, + }, + ...Array.from(revenueByAccount.entries()).map(([glAccountId, amount]) => ({ + glAccountId, + debit: 0, + credit: amount, + branchSegmentValueId: invoice.branchSegmentValueId, + description: `Invoice ${invoice.invoiceNumber} — ${invoice.customer.name}`, + })), + ]; + if (taxAccount && taxTotal.greaterThan(0)) { + lines.push({ + glAccountId: taxAccount.id, + debit: 0, + credit: taxTotal, + branchSegmentValueId: invoice.branchSegmentValueId, + description: `Output tax on invoice ${invoice.invoiceNumber}`, + }); + } + + const draft: JournalEntryDraft = { + journalCode: 'SALES', + entryDate: invoice.invoiceDate, + description: `AR Invoice ${invoice.invoiceNumber} — ${invoice.customer.name}`, + sourceModule: 'AR', + sourceType: 'ARInvoice', + sourceId: invoice.id, + createdById, + lines, + }; + return PostingService.postEntry(draft); +} + +/** Reverse of an invoice: Dr revenue account(s) + Dr Output Tax / Cr AR Control. */ +export async function postCreditNoteToGL(creditNote: CreditNoteForPosting, createdById: number): Promise { + const arAccount = await resolveControlAccount(prisma, creditNote.branchSegmentValueId, 'AR'); + const taxTotal = money(creditNote.taxTotal); + const taxAccount = taxTotal.greaterThan(0) + ? await resolveControlAccount(prisma, creditNote.branchSegmentValueId, 'TAX_OUTPUT') + : null; + + const revenueByAccount = groupByAccount( + creditNote.lines.map((line) => ({ glAccountId: line.revenueGLAccountId, amount: money(line.lineSubtotal) })), + ); + + const lines: JournalLineDraft[] = [ + ...Array.from(revenueByAccount.entries()).map(([glAccountId, amount]) => ({ + glAccountId, + debit: amount, + credit: 0, + branchSegmentValueId: creditNote.branchSegmentValueId, + description: `Credit note ${creditNote.creditNoteNumber} — ${creditNote.customer.name}`, + })), + { + glAccountId: arAccount.id, + debit: 0, + credit: creditNote.total, + branchSegmentValueId: creditNote.branchSegmentValueId, + description: `Credit note ${creditNote.creditNoteNumber} — ${creditNote.customer.name}`, + }, + ]; + if (taxAccount && taxTotal.greaterThan(0)) { + lines.push({ + glAccountId: taxAccount.id, + debit: taxTotal, + credit: 0, + branchSegmentValueId: creditNote.branchSegmentValueId, + description: `Output tax reversed on credit note ${creditNote.creditNoteNumber}`, + }); + } + + const draft: JournalEntryDraft = { + journalCode: 'SALES', + entryDate: creditNote.creditNoteDate, + description: `AR Credit Note ${creditNote.creditNoteNumber} — ${creditNote.customer.name}`, + sourceModule: 'AR', + sourceType: 'ARCreditNote', + sourceId: creditNote.id, + createdById, + lines, + }; + return PostingService.postEntry(draft); +} + +/** Dr Bank / Cr AR Control — allocation against specific invoices happens separately, sub-ledger only. */ +export async function postReceiptToGL(receipt: ReceiptForPosting, createdById: number): Promise { + const arAccount = await resolveControlAccount(prisma, receipt.branchSegmentValueId, 'AR'); + + const draft: JournalEntryDraft = { + journalCode: 'CASH_RECEIPT', + entryDate: receipt.receiptDate, + description: `AR Receipt ${receipt.receiptNumber} — ${receipt.customer.name}`, + sourceModule: 'AR', + sourceType: 'ARReceipt', + sourceId: receipt.id, + createdById, + lines: [ + { + glAccountId: receipt.bankGLAccountId, + debit: receipt.amount, + credit: 0, + branchSegmentValueId: receipt.branchSegmentValueId, + description: `Receipt ${receipt.receiptNumber} — ${receipt.customer.name}`, + }, + { + glAccountId: arAccount.id, + debit: 0, + credit: receipt.amount, + branchSegmentValueId: receipt.branchSegmentValueId, + description: `Receipt ${receipt.receiptNumber} — ${receipt.customer.name}`, + }, + ], + }; + return PostingService.postEntry(draft); +} + +export function calcLine(quantity: number, unitPrice: number, ratePercent: number | null) { + const subtotal = money(quantity).times(unitPrice).toDecimalPlaces(2); + const tax = ratePercent ? subtotal.times(ratePercent).dividedBy(100).toDecimalPlaces(2) : ZERO; + return { subtotal, tax, total: subtotal.plus(tax) }; +} + +export function sumLines(values: import('../../lib/money').Money[]) { + return sum(values); +} diff --git a/src/modules/ar/arReceipts.controller.ts b/src/modules/ar/arReceipts.controller.ts new file mode 100644 index 0000000..f489d91 --- /dev/null +++ b/src/modules/ar/arReceipts.controller.ts @@ -0,0 +1,51 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as arReceiptsService from './arReceipts.service'; + +export const createReceiptSchema = z.object({ + customerId: z.number().int(), + receiptDate: z.string().datetime().or(z.string().date()), + amount: z.number().positive(), + bankGLAccountId: z.number().int(), + reference: z.string().optional(), +}); + +export const allocateSchema = z.object({ + allocations: z.array(z.object({ invoiceId: z.number().int(), amount: z.number().positive() })).min(1), +}); + +export async function createReceiptHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const receipt = await arReceiptsService.createReceiptDraft(input, req.user!.id); + res.status(201).json(receipt); +} + +export async function postReceiptHandler(req: Request, res: Response): Promise { + const receipt = await arReceiptsService.postReceipt(Number(req.params.id), req.user!.id); + res.json(receipt); +} + +export async function voidReceiptHandler(req: Request, res: Response): Promise { + const receipt = await arReceiptsService.voidReceiptDraft(Number(req.params.id)); + res.json(receipt); +} + +export async function allocateReceiptHandler(req: Request, res: Response): Promise { + const { allocations } = req.body as z.infer; + const result = await arReceiptsService.allocateReceipt(Number(req.params.id), allocations); + res.status(201).json(result); +} + +export async function listReceiptsHandler(req: Request, res: Response): Promise { + const customerId = req.query.customerId ? Number(req.query.customerId) : undefined; + res.json(await arReceiptsService.listReceipts(req.branchScope!, customerId)); +} + +export async function getReceiptHandler(req: Request, res: Response): Promise { + const receipt = await arReceiptsService.getReceipt(Number(req.params.id)); + if (!receipt) { + res.status(404).json({ error: 'Receipt not found' }); + return; + } + res.json(receipt); +} diff --git a/src/modules/ar/arReceipts.routes.ts b/src/modules/ar/arReceipts.routes.ts new file mode 100644 index 0000000..f2d3aaf --- /dev/null +++ b/src/modules/ar/arReceipts.routes.ts @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + allocateReceiptHandler, + allocateSchema, + createReceiptHandler, + createReceiptSchema, + getReceiptHandler, + listReceiptsHandler, + postReceiptHandler, + voidReceiptHandler, +} from './arReceipts.controller'; + +export const arReceiptsRouter = Router(); + +arReceiptsRouter.use(requireAuth); + +arReceiptsRouter.get('/', attachBranchScope, asyncHandler(listReceiptsHandler)); +arReceiptsRouter.get('/:id', asyncHandler(getReceiptHandler)); +arReceiptsRouter.post('/', requirePermission('ar.receipt.manage'), validate(createReceiptSchema), asyncHandler(createReceiptHandler)); +arReceiptsRouter.post('/:id/post', requirePermission('ar.receipt.manage'), asyncHandler(postReceiptHandler)); +arReceiptsRouter.post('/:id/void', requirePermission('ar.receipt.manage'), asyncHandler(voidReceiptHandler)); +arReceiptsRouter.post( + '/:id/allocations', + requirePermission('ar.receipt.manage'), + validate(allocateSchema), + asyncHandler(allocateReceiptHandler), +); diff --git a/src/modules/ar/arReceipts.service.ts b/src/modules/ar/arReceipts.service.ts new file mode 100644 index 0000000..f0a37eb --- /dev/null +++ b/src/modules/ar/arReceipts.service.ts @@ -0,0 +1,82 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; +import { money } from '../../lib/money'; +import { nextNumber } from '../../lib/sequence'; +import { postReceiptToGL } from './arPosting.service'; +import { ARDocumentStateError } from './ar.errors'; +import { allocate, AllocationInput } from './arAllocations.service'; + +export interface CreateReceiptInput { + customerId: number; + receiptDate: string; + amount: number; + bankGLAccountId: number; + reference?: string; +} + +export async function createReceiptDraft(input: CreateReceiptInput, createdById: number) { + const customer = await prisma.customer.findUniqueOrThrow({ where: { id: input.customerId } }); + const amount = money(input.amount); + + return prisma.$transaction(async (tx) => { + const receiptNumber = await nextNumber(tx, 'ARRCT', new Date(input.receiptDate).getFullYear()); + return tx.aRReceipt.create({ + data: { + receiptNumber, + customerId: customer.id, + branchSegmentValueId: customer.branchSegmentValueId, + receiptDate: new Date(input.receiptDate), + amount, + unallocatedAmount: amount, + bankGLAccountId: input.bankGLAccountId, + reference: input.reference, + status: 'DRAFT', + createdById, + }, + include: { customer: true, bankGLAccount: true }, + }); + }); +} + +export async function postReceipt(id: number, createdById: number) { + const receipt = await prisma.aRReceipt.findUniqueOrThrow({ where: { id }, include: { customer: true } }); + if (receipt.status !== 'DRAFT') { + throw new ARDocumentStateError('Only draft receipts can be posted'); + } + + const entry = await postReceiptToGL(receipt, createdById); + + return prisma.aRReceipt.update({ + where: { id }, + data: { status: 'POSTED', journalEntryId: entry.id }, + include: { customer: true, bankGLAccount: true }, + }); +} + +export async function voidReceiptDraft(id: number) { + const receipt = await prisma.aRReceipt.findUniqueOrThrow({ where: { id } }); + if (receipt.status !== 'DRAFT') { + throw new ARDocumentStateError('Only draft receipts can be voided'); + } + return prisma.aRReceipt.update({ where: { id }, data: { status: 'VOID' } }); +} + +export function allocateReceipt(receiptId: number, allocations: AllocationInput[]) { + return allocate({ receiptId }, allocations); +} + +export function listReceipts(branchScope: BranchScope, customerId?: number) { + return prisma.aRReceipt.findMany({ + where: { ...branchWhere(branchScope), ...(customerId ? { customerId } : {}) }, + include: { customer: true, bankGLAccount: true }, + orderBy: { receiptDate: 'desc' }, + take: 200, + }); +} + +export function getReceipt(id: number) { + return prisma.aRReceipt.findUnique({ + where: { id }, + include: { customer: true, bankGLAccount: true, allocations: { include: { invoice: true } } }, + }); +} diff --git a/src/modules/ar/customers.controller.ts b/src/modules/ar/customers.controller.ts new file mode 100644 index 0000000..a7dd014 --- /dev/null +++ b/src/modules/ar/customers.controller.ts @@ -0,0 +1,40 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as customersService from './customers.service'; + +export const createCustomerSchema = z.object({ + code: z.string().min(1), + name: z.string().min(1), + email: z.string().email().optional(), + phone: z.string().optional(), + address: z.string().optional(), + taxNumber: z.string().optional(), + branchSegmentValueId: z.number().int(), + creditLimit: z.number().min(0).optional(), + paymentTermsDays: z.number().int().min(0).optional(), +}); + +export const updateCustomerSchema = createCustomerSchema.partial().extend({ isActive: z.boolean().optional() }); + +export async function listCustomersHandler(req: Request, res: Response): Promise { + res.json(await customersService.listCustomers(req.branchScope!)); +} + +export async function createCustomerHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await customersService.createCustomer(input)); +} + +export async function updateCustomerHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.json(await customersService.updateCustomer(Number(req.params.id), input)); +} + +export async function getCustomerHandler(req: Request, res: Response): Promise { + const result = await customersService.getCustomer(Number(req.params.id)); + if (!result) { + res.status(404).json({ error: 'Customer not found' }); + return; + } + res.json(result); +} diff --git a/src/modules/ar/customers.routes.ts b/src/modules/ar/customers.routes.ts new file mode 100644 index 0000000..c523d91 --- /dev/null +++ b/src/modules/ar/customers.routes.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createCustomerHandler, + createCustomerSchema, + getCustomerHandler, + listCustomersHandler, + updateCustomerHandler, + updateCustomerSchema, +} from './customers.controller'; + +export const customersRouter = Router(); + +customersRouter.use(requireAuth); + +customersRouter.get('/', attachBranchScope, asyncHandler(listCustomersHandler)); +customersRouter.get('/:id', asyncHandler(getCustomerHandler)); +customersRouter.post('/', requirePermission('ar.customer.manage'), validate(createCustomerSchema), asyncHandler(createCustomerHandler)); +customersRouter.put( + '/:id', + requirePermission('ar.customer.manage'), + validate(updateCustomerSchema), + asyncHandler(updateCustomerHandler), +); diff --git a/src/modules/ar/customers.service.ts b/src/modules/ar/customers.service.ts new file mode 100644 index 0000000..e7659e9 --- /dev/null +++ b/src/modules/ar/customers.service.ts @@ -0,0 +1,58 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope, branchWhere } from '../../lib/branchFilter'; + +export interface CustomerInput { + code: string; + name: string; + email?: string; + phone?: string; + address?: string; + taxNumber?: string; + branchSegmentValueId: number; + creditLimit?: number; + paymentTermsDays?: number; +} + +export function listCustomers(branchScope: BranchScope) { + return prisma.customer.findMany({ + where: branchWhere(branchScope), + orderBy: { name: 'asc' }, + }); +} + +export function createCustomer(input: CustomerInput) { + return prisma.customer.create({ data: input }); +} + +export function updateCustomer(id: number, input: Partial & { isActive?: boolean }) { + return prisma.customer.update({ where: { id }, data: input }); +} + +export async function getCustomer(id: number) { + const customer = await prisma.customer.findUnique({ where: { id } }); + if (!customer) return null; + + const [invoices, creditNotes, receipts] = await Promise.all([ + prisma.aRInvoice.findMany({ + where: { customerId: id, status: { not: 'DRAFT' } }, + orderBy: { invoiceDate: 'desc' }, + include: { lines: true }, + }), + prisma.aRCreditNote.findMany({ + where: { customerId: id, status: { not: 'DRAFT' } }, + orderBy: { creditNoteDate: 'desc' }, + include: { lines: true }, + }), + prisma.aRReceipt.findMany({ + where: { customerId: id, status: { not: 'DRAFT' } }, + orderBy: { receiptDate: 'desc' }, + }), + ]); + + const outstandingBalance = invoices.reduce((sum, invoice) => sum + Number(invoice.balance), 0); + const overdueBalance = invoices + .filter((invoice) => invoice.balance && Number(invoice.balance) > 0 && invoice.dueDate < new Date()) + .reduce((sum, invoice) => sum + Number(invoice.balance), 0); + + return { customer, invoices, creditNotes, receipts, outstandingBalance, overdueBalance }; +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..1655580 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,44 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as authService from './auth.service'; +import { InvalidHandoffTokenError, WrongCompanyHandoffError } from '../../lib/handoffVerify'; + +export const handoffSchema = z.object({ + token: z.string().min(1), +}); + +export const refreshSchema = z.object({ + refreshToken: z.string().min(1), +}); + +export async function handoffHandler(req: Request, res: Response): Promise { + try { + const { token } = req.body as z.infer; + const session = await authService.verifyHandoff(token); + res.json(session); + } catch (error) { + if (error instanceof InvalidHandoffTokenError || error instanceof WrongCompanyHandoffError) { + res.status(401).json({ error: error.message }); + return; + } + if (error instanceof authService.UserNotProvisionedError) { + res.status(403).json({ error: error.message }); + return; + } + throw error; + } +} + +export function refreshHandler(req: Request, res: Response): void { + try { + const { refreshToken } = req.body as z.infer; + const accessToken = authService.refreshAccessToken(refreshToken); + res.json({ accessToken }); + } catch { + res.status(401).json({ error: 'Invalid or expired refresh token' }); + } +} + +export function meHandler(req: Request, res: Response): void { + res.json({ user: req.user }); +} diff --git a/src/modules/auth/auth.routes.ts b/src/modules/auth/auth.routes.ts new file mode 100644 index 0000000..2d1514f --- /dev/null +++ b/src/modules/auth/auth.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { handoffHandler, handoffSchema, meHandler, refreshHandler, refreshSchema } from './auth.controller'; + +export const authRouter = Router(); + +authRouter.post('/handoff', validate(handoffSchema), asyncHandler(handoffHandler)); +authRouter.post('/refresh', validate(refreshSchema), refreshHandler); +authRouter.get('/me', requireAuth, meHandler); diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..fd30453 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,52 @@ +import { prisma } from '../../lib/prisma'; +import { signAccessToken, signRefreshToken, verifyRefreshToken } from '../../lib/jwt'; +import { InvalidHandoffTokenError, verifyHandoffToken } from '../../lib/handoffVerify'; +import { loadAuthUser } from '../../middleware/auth'; + +export class UserNotProvisionedError extends Error { + constructor(email: string) { + super(`No user exists for ${email} in this company, and auto-provisioning is off`); + this.name = 'UserNotProvisionedError'; + } +} + +/** + * Verifies a handoff token minted by the Account Manager and exchanges it for a session in + * this company app. Local password login no longer exists here — see the implementation plan, + * "Phase A — Account Manager: foundation & handoff". + */ +export async function verifyHandoff(token: string) { + const claims = verifyHandoffToken(token); + + await prisma.$transaction(async (tx) => { + const alreadyUsed = await tx.handoffTokenUse.findUnique({ where: { jti: claims.jti } }); + if (alreadyUsed) { + throw new InvalidHandoffTokenError('This handoff token has already been used'); + } + await tx.handoffTokenUse.create({ data: { jti: claims.jti } }); + }); + + let user = await prisma.user.findUnique({ where: { email: claims.sub } }); + if (!user) { + const company = await prisma.companySettings.findUniqueOrThrow({ where: { id: 1 } }); + if (!company.autoProvisionOnHandoff) { + throw new UserNotProvisionedError(claims.sub); + } + user = await prisma.user.create({ data: { email: claims.sub, fullName: claims.sub } }); + } + if (!user.isActive) { + throw new UserNotProvisionedError(claims.sub); + } + + const authUser = await loadAuthUser(user.id); + if (!authUser) { + throw new UserNotProvisionedError(claims.sub); + } + + return { accessToken: signAccessToken(user.id), refreshToken: signRefreshToken(user.id), user: authUser }; +} + +export function refreshAccessToken(refreshToken: string): string { + const userId = verifyRefreshToken(refreshToken); + return signAccessToken(userId); +} diff --git a/src/modules/company/fiscalPeriods.controller.ts b/src/modules/company/fiscalPeriods.controller.ts new file mode 100644 index 0000000..99e8ba1 --- /dev/null +++ b/src/modules/company/fiscalPeriods.controller.ts @@ -0,0 +1,16 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as fiscalPeriodsService from './fiscalPeriods.service'; + +export const createFiscalYearSchema = z.object({ + year: z.number().int().min(2000).max(2100), +}); + +export async function listFiscalYearsHandler(req: Request, res: Response): Promise { + res.json(await fiscalPeriodsService.listFiscalYears()); +} + +export async function createFiscalYearHandler(req: Request, res: Response): Promise { + const { year } = req.body as z.infer; + res.status(201).json(await fiscalPeriodsService.createFiscalYear(year)); +} diff --git a/src/modules/company/fiscalPeriods.routes.ts b/src/modules/company/fiscalPeriods.routes.ts new file mode 100644 index 0000000..d271f23 --- /dev/null +++ b/src/modules/company/fiscalPeriods.routes.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { createFiscalYearHandler, createFiscalYearSchema, listFiscalYearsHandler } from './fiscalPeriods.controller'; + +export const fiscalPeriodsRouter = Router(); + +fiscalPeriodsRouter.use(requireAuth); + +fiscalPeriodsRouter.get('/fiscal-years', asyncHandler(listFiscalYearsHandler)); +fiscalPeriodsRouter.post( + '/fiscal-years', + requirePermission('admin.fiscalPeriods.manage'), + validate(createFiscalYearSchema), + asyncHandler(createFiscalYearHandler), +); diff --git a/src/modules/company/fiscalPeriods.service.ts b/src/modules/company/fiscalPeriods.service.ts new file mode 100644 index 0000000..54c362a --- /dev/null +++ b/src/modules/company/fiscalPeriods.service.ts @@ -0,0 +1,30 @@ +import { prisma } from '../../lib/prisma'; + +export function listFiscalYears() { + return prisma.fiscalYear.findMany({ orderBy: { year: 'asc' }, include: { periods: { orderBy: { periodNumber: 'asc' } } } }); +} + +/** Creates a fiscal year and its 12 monthly periods, starting at CompanySettings.fiscalYearStartMonth. */ +export async function createFiscalYear(year: number) { + const company = await prisma.companySettings.findUniqueOrThrow({ where: { id: 1 } }); + const startMonth = company.fiscalYearStartMonth; + const startDate = new Date(Date.UTC(year, startMonth - 1, 1)); + + const periods = Array.from({ length: 12 }, (_, index) => { + const periodStart = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth() + index, 1)); + const periodEnd = new Date(Date.UTC(periodStart.getUTCFullYear(), periodStart.getUTCMonth() + 1, 0)); + return { periodNumber: index + 1, startDate: periodStart, endDate: periodEnd }; + }); + + const endDate = periods[periods.length - 1].endDate; + + return prisma.fiscalYear.create({ + data: { + year, + startDate, + endDate, + periods: { create: periods }, + }, + include: { periods: true }, + }); +} diff --git a/src/modules/company/segments.controller.ts b/src/modules/company/segments.controller.ts new file mode 100644 index 0000000..852e2ba --- /dev/null +++ b/src/modules/company/segments.controller.ts @@ -0,0 +1,36 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as segmentsService from './segments.service'; + +export const createSegmentDefinitionSchema = z.object({ + segmentKey: z.string().min(1).max(30), + label: z.string().min(1), + order: z.number().int().min(0), + length: z.number().int().min(1).max(20), + format: z.enum(['NUMERIC', 'ALPHA', 'ALPHANUMERIC']), + isBranchSegment: z.boolean().default(false), +}); + +export const createSegmentValueSchema = z.object({ + segmentDefId: z.number().int(), + code: z.string().min(1), + description: z.string().min(1), +}); + +export async function listSegmentDefinitionsHandler(req: Request, res: Response): Promise { + res.json(await segmentsService.listSegmentDefinitions()); +} + +export async function createSegmentDefinitionHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await segmentsService.createSegmentDefinition(input)); +} + +export async function createSegmentValueHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await segmentsService.createSegmentValue(input)); +} + +export async function listBranchValuesHandler(req: Request, res: Response): Promise { + res.json(await segmentsService.listBranchValues()); +} diff --git a/src/modules/company/segments.routes.ts b/src/modules/company/segments.routes.ts new file mode 100644 index 0000000..d08e657 --- /dev/null +++ b/src/modules/company/segments.routes.ts @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createSegmentDefinitionHandler, + createSegmentDefinitionSchema, + createSegmentValueHandler, + createSegmentValueSchema, + listBranchValuesHandler, + listSegmentDefinitionsHandler, +} from './segments.controller'; + +export const segmentsRouter = Router(); + +segmentsRouter.use(requireAuth); + +segmentsRouter.get('/segment-definitions', asyncHandler(listSegmentDefinitionsHandler)); +segmentsRouter.post( + '/segment-definitions', + requirePermission('admin.segments.manage'), + validate(createSegmentDefinitionSchema), + asyncHandler(createSegmentDefinitionHandler), +); +segmentsRouter.post( + '/segment-values', + requirePermission('admin.segments.manage'), + validate(createSegmentValueSchema), + asyncHandler(createSegmentValueHandler), +); +segmentsRouter.get('/branches', asyncHandler(listBranchValuesHandler)); diff --git a/src/modules/company/segments.service.ts b/src/modules/company/segments.service.ts new file mode 100644 index 0000000..43440e0 --- /dev/null +++ b/src/modules/company/segments.service.ts @@ -0,0 +1,39 @@ +import { prisma } from '../../lib/prisma'; + +export function listSegmentDefinitions() { + return prisma.accountSegmentDefinition.findMany({ + orderBy: { order: 'asc' }, + include: { values: { orderBy: { code: 'asc' } } }, + }); +} + +export interface CreateSegmentDefinitionInput { + segmentKey: string; + label: string; + order: number; + length: number; + format: 'NUMERIC' | 'ALPHA' | 'ALPHANUMERIC'; + isBranchSegment: boolean; +} + +export function createSegmentDefinition(input: CreateSegmentDefinitionInput) { + return prisma.accountSegmentDefinition.create({ data: input }); +} + +export interface CreateSegmentValueInput { + segmentDefId: number; + code: string; + description: string; +} + +export function createSegmentValue(input: CreateSegmentValueInput) { + return prisma.accountSegmentValue.create({ data: input }); +} + +export function listBranchValues() { + return prisma.accountSegmentValue.findMany({ + where: { segmentDef: { isBranchSegment: true }, isActive: true }, + include: { branchProfile: true }, + orderBy: { code: 'asc' }, + }); +} diff --git a/src/modules/gl/glAccounts.controller.ts b/src/modules/gl/glAccounts.controller.ts new file mode 100644 index 0000000..de22b49 --- /dev/null +++ b/src/modules/gl/glAccounts.controller.ts @@ -0,0 +1,43 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as glAccountsService from './glAccounts.service'; + +const glAccountTypeSchema = z.enum(['ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE']); +const debitCreditSchema = z.enum(['DEBIT', 'CREDIT']); +const controlAccountTypeSchema = z.enum(['AR', 'AP', 'STOCK', 'TAX_OUTPUT', 'TAX_INPUT', 'BANK', 'RETAINED_EARNINGS']); + +export const createGLAccountSchema = z.object({ + accountName: z.string().min(1), + accountType: glAccountTypeSchema, + normalBalance: debitCreditSchema, + isControlAccount: z.boolean().optional(), + controlType: controlAccountTypeSchema.optional(), + isPostable: z.boolean().optional(), + segmentSelections: z + .array(z.object({ segmentDefId: z.number().int(), segmentValueId: z.number().int() })) + .min(1), +}); + +export const listGLAccountsQuerySchema = z.object({ + branchScope: z.string().optional(), + accountType: glAccountTypeSchema.optional(), +}); + +export async function createGLAccountHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await glAccountsService.createGLAccount(input)); +} + +export async function listGLAccountsHandler(req: Request, res: Response): Promise { + const { accountType } = req.query as unknown as z.infer; + res.json(await glAccountsService.listGLAccounts(req.branchScope!, accountType)); +} + +export async function getGLAccountHandler(req: Request, res: Response): Promise { + const account = await glAccountsService.getGLAccountByCode(req.params.accountCode); + if (!account) { + res.status(404).json({ error: 'GL account not found' }); + return; + } + res.json(account); +} diff --git a/src/modules/gl/glAccounts.routes.ts b/src/modules/gl/glAccounts.routes.ts new file mode 100644 index 0000000..7339550 --- /dev/null +++ b/src/modules/gl/glAccounts.routes.ts @@ -0,0 +1,25 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createGLAccountHandler, + createGLAccountSchema, + getGLAccountHandler, + listGLAccountsHandler, +} from './glAccounts.controller'; + +export const glAccountsRouter = Router(); + +glAccountsRouter.use(requireAuth); + +glAccountsRouter.get('/', attachBranchScope, asyncHandler(listGLAccountsHandler)); +glAccountsRouter.get('/:accountCode', asyncHandler(getGLAccountHandler)); +glAccountsRouter.post( + '/', + requirePermission('gl.account.manage'), + validate(createGLAccountSchema), + asyncHandler(createGLAccountHandler), +); diff --git a/src/modules/gl/glAccounts.service.ts b/src/modules/gl/glAccounts.service.ts new file mode 100644 index 0000000..dfdbabd --- /dev/null +++ b/src/modules/gl/glAccounts.service.ts @@ -0,0 +1,102 @@ +import { GLAccountType, DebitCredit, ControlAccountType } from '@prisma/client'; +import { prisma } from '../../lib/prisma'; +import { BranchScope } from '../../lib/branchFilter'; + +export interface SegmentSelection { + segmentDefId: number; + segmentValueId: number; +} + +export interface CreateGLAccountInput { + accountName: string; + accountType: GLAccountType; + normalBalance: DebitCredit; + isControlAccount?: boolean; + controlType?: ControlAccountType; + isPostable?: boolean; + segmentSelections: SegmentSelection[]; +} + +export class SegmentSelectionError extends Error { + constructor(message: string) { + super(message); + this.name = 'SegmentSelectionError'; + } +} + +/** Builds "1000-100-500-AMA" from segment values in definition order, using CompanySettings.accountCodeSeparator. */ +async function buildAccountCode(segmentSelections: SegmentSelection[]): Promise { + const definitions = await prisma.accountSegmentDefinition.findMany({ + where: { isActive: true }, + orderBy: { order: 'asc' }, + }); + const company = await prisma.companySettings.findUniqueOrThrow({ where: { id: 1 } }); + + if (segmentSelections.length !== definitions.length) { + throw new SegmentSelectionError( + `Expected a value for every active segment (${definitions.length}), got ${segmentSelections.length}`, + ); + } + + const values = await prisma.accountSegmentValue.findMany({ + where: { id: { in: segmentSelections.map((selection) => selection.segmentValueId) } }, + }); + const valueById = new Map(values.map((value) => [value.id, value])); + + const orderedCodes = definitions.map((definition) => { + const selection = segmentSelections.find((s) => s.segmentDefId === definition.id); + if (!selection) { + throw new SegmentSelectionError(`Missing a value for segment "${definition.label}"`); + } + const value = valueById.get(selection.segmentValueId); + if (!value || value.segmentDefId !== definition.id) { + throw new SegmentSelectionError(`Segment value ${selection.segmentValueId} does not belong to segment "${definition.label}"`); + } + return value.code; + }); + + return orderedCodes.join(company.accountCodeSeparator); +} + +export async function createGLAccount(input: CreateGLAccountInput) { + const accountCode = await buildAccountCode(input.segmentSelections); + + return prisma.gLAccount.create({ + data: { + accountCode, + accountName: input.accountName, + accountType: input.accountType, + normalBalance: input.normalBalance, + isControlAccount: input.isControlAccount ?? false, + controlType: input.controlType, + isPostable: input.isPostable ?? true, + segmentValues: { + create: input.segmentSelections.map((selection) => ({ + segmentDefId: selection.segmentDefId, + segmentValueId: selection.segmentValueId, + })), + }, + }, + include: { segmentValues: { include: { segmentValue: true, segmentDef: true } } }, + }); +} + +export function listGLAccounts(branchScope: BranchScope, accountType?: GLAccountType) { + return prisma.gLAccount.findMany({ + where: { + accountType, + ...(branchScope.mode === 'BRANCH' + ? { segmentValues: { some: { segmentValueId: branchScope.branchSegmentValueId } } } + : {}), + }, + include: { segmentValues: { include: { segmentValue: true, segmentDef: true } } }, + orderBy: { accountCode: 'asc' }, + }); +} + +export function getGLAccountByCode(accountCode: string) { + return prisma.gLAccount.findUnique({ + where: { accountCode }, + include: { segmentValues: { include: { segmentValue: true, segmentDef: true } } }, + }); +} diff --git a/src/modules/gl/journalEntries.controller.ts b/src/modules/gl/journalEntries.controller.ts new file mode 100644 index 0000000..6a48c18 --- /dev/null +++ b/src/modules/gl/journalEntries.controller.ts @@ -0,0 +1,47 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as journalEntriesService from './journalEntries.service'; + +const manualLineSchema = z.object({ + glAccountId: z.number().int(), + debit: z.number().min(0), + credit: z.number().min(0), + description: z.string().optional(), + branchSegmentValueId: z.number().int().optional(), +}); + +export const createManualEntrySchema = z.object({ + journalCode: z.string().min(1), + entryDate: z.string().datetime().or(z.string().date()), + description: z.string().min(1), + lines: z.array(manualLineSchema).min(2), +}); + +export const reverseEntrySchema = z.object({ + reason: z.string().optional(), +}); + +export async function createManualEntryHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + const entry = await journalEntriesService.createManualEntry(input, req.user!.id); + res.status(201).json(entry); +} + +export async function listJournalEntriesHandler(req: Request, res: Response): Promise { + res.json(await journalEntriesService.listJournalEntries(req.branchScope!)); +} + +export async function getJournalEntryHandler(req: Request, res: Response): Promise { + const entry = await journalEntriesService.getJournalEntry(Number(req.params.id)); + if (!entry) { + res.status(404).json({ error: 'Journal entry not found' }); + return; + } + res.json(entry); +} + +export async function reverseJournalEntryHandler(req: Request, res: Response): Promise { + const { reason } = req.body as z.infer; + const reversal = await journalEntriesService.reverseJournalEntry(Number(req.params.id), req.user!.id, reason); + res.status(201).json(reversal); +} diff --git a/src/modules/gl/journalEntries.routes.ts b/src/modules/gl/journalEntries.routes.ts new file mode 100644 index 0000000..6105a57 --- /dev/null +++ b/src/modules/gl/journalEntries.routes.ts @@ -0,0 +1,33 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { attachBranchScope } from '../../middleware/branchScope'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createManualEntryHandler, + createManualEntrySchema, + getJournalEntryHandler, + listJournalEntriesHandler, + reverseEntrySchema, + reverseJournalEntryHandler, +} from './journalEntries.controller'; + +export const journalEntriesRouter = Router(); + +journalEntriesRouter.use(requireAuth); + +journalEntriesRouter.get('/', attachBranchScope, asyncHandler(listJournalEntriesHandler)); +journalEntriesRouter.get('/:id', asyncHandler(getJournalEntryHandler)); +journalEntriesRouter.post( + '/', + requirePermission('gl.journal.post'), + validate(createManualEntrySchema), + asyncHandler(createManualEntryHandler), +); +journalEntriesRouter.post( + '/:id/reverse', + requirePermission('gl.journal.post'), + validate(reverseEntrySchema), + asyncHandler(reverseJournalEntryHandler), +); diff --git a/src/modules/gl/journalEntries.service.ts b/src/modules/gl/journalEntries.service.ts new file mode 100644 index 0000000..ab1b970 --- /dev/null +++ b/src/modules/gl/journalEntries.service.ts @@ -0,0 +1,52 @@ +import { prisma } from '../../lib/prisma'; +import { BranchScope } from '../../lib/branchFilter'; +import { PostingService } from './posting.service'; +import { JournalEntryDraft } from './posting.types'; + +export interface ManualJournalLineInput { + glAccountId: number; + debit: number; + credit: number; + description?: string; + branchSegmentValueId?: number; +} + +export interface CreateManualEntryInput { + journalCode: string; + entryDate: string; + description: string; + lines: ManualJournalLineInput[]; +} + +export function createManualEntry(input: CreateManualEntryInput, createdById: number) { + const draft: JournalEntryDraft = { + journalCode: input.journalCode, + entryDate: new Date(input.entryDate), + description: input.description, + sourceModule: 'GL', + sourceType: 'ManualJournalEntry', + createdById, + lines: input.lines, + }; + return PostingService.postEntry(draft); +} + +export function listJournalEntries(branchScope: BranchScope) { + return prisma.journalEntry.findMany({ + where: branchScope.mode === 'BRANCH' ? { lines: { some: { branchSegmentValueId: branchScope.branchSegmentValueId } } } : {}, + include: { journal: true, lines: { include: { glAccount: true } } }, + orderBy: { entryDate: 'desc' }, + take: 200, + }); +} + +export function getJournalEntry(id: number) { + return prisma.journalEntry.findUnique({ + where: { id }, + include: { journal: true, lines: { include: { glAccount: true } }, createdBy: { select: { id: true, fullName: true, email: true } } }, + }); +} + +export function reverseJournalEntry(id: number, userId: number, reason?: string) { + return PostingService.reverseEntry(id, userId, reason); +} diff --git a/src/modules/gl/journals.controller.ts b/src/modules/gl/journals.controller.ts new file mode 100644 index 0000000..29c2dca --- /dev/null +++ b/src/modules/gl/journals.controller.ts @@ -0,0 +1,19 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as journalsService from './journals.service'; + +export const createJournalSchema = z.object({ + code: z.string().min(1).max(20), + name: z.string().min(1), + journalType: z.enum(['GENERAL', 'SALES', 'PURCHASES', 'CASH_RECEIPT', 'CASH_PAYMENT', 'STOCK', 'BANK', 'CUSTOM']), + defaultBranchSegmentValueId: z.number().int().optional(), +}); + +export async function listJournalsHandler(req: Request, res: Response): Promise { + res.json(await journalsService.listJournals()); +} + +export async function createJournalHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await journalsService.createJournal(input)); +} diff --git a/src/modules/gl/journals.routes.ts b/src/modules/gl/journals.routes.ts new file mode 100644 index 0000000..9f069ab --- /dev/null +++ b/src/modules/gl/journals.routes.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { createJournalHandler, createJournalSchema, listJournalsHandler } from './journals.controller'; + +export const journalsRouter = Router(); + +journalsRouter.use(requireAuth); + +journalsRouter.get('/', asyncHandler(listJournalsHandler)); +journalsRouter.post( + '/', + requirePermission('gl.journal.manage'), + validate(createJournalSchema), + asyncHandler(createJournalHandler), +); diff --git a/src/modules/gl/journals.service.ts b/src/modules/gl/journals.service.ts new file mode 100644 index 0000000..f57b967 --- /dev/null +++ b/src/modules/gl/journals.service.ts @@ -0,0 +1,17 @@ +import { JournalType } from '@prisma/client'; +import { prisma } from '../../lib/prisma'; + +export function listJournals() { + return prisma.journal.findMany({ orderBy: { code: 'asc' } }); +} + +export interface CreateJournalInput { + code: string; + name: string; + journalType: JournalType; + defaultBranchSegmentValueId?: number; +} + +export function createJournal(input: CreateJournalInput) { + return prisma.journal.create({ data: input }); +} diff --git a/src/modules/gl/posting.service.ts b/src/modules/gl/posting.service.ts new file mode 100644 index 0000000..918311f --- /dev/null +++ b/src/modules/gl/posting.service.ts @@ -0,0 +1,136 @@ +import { JournalEntry, JournalEntryStatus } from '@prisma/client'; +import { prisma } from '../../lib/prisma'; +import { money, sum } from '../../lib/money'; +import { nextNumber } from '../../lib/sequence'; +import { ClosedPeriodError, InvalidLineError, JournalEntryDraft, UnbalancedEntryError } from './posting.types'; + +/** + * The ONLY code path in this codebase allowed to write JournalEntry/JournalEntryLine rows + * (enforced by the no-restricted-syntax ESLint rule on prisma.journalEntry / prisma.journalEntryLine + * outside this file, and backstopped by a DB trigger — see the balance-trigger migration). + * AR, AP, and Stock modules build a JournalEntryDraft and call postEntry(); they never write GL rows directly. + */ +export async function postEntry(draft: JournalEntryDraft): Promise { + if (draft.lines.length === 0) { + throw new InvalidLineError('A journal entry must have at least one line'); + } + + const lines = draft.lines.map((line, index) => { + const debit = money(line.debit); + const credit = money(line.credit); + if (debit.lessThan(0) || credit.lessThan(0)) { + throw new InvalidLineError(`Line ${index + 1}: debit and credit must be non-negative`); + } + const debitSet = debit.greaterThan(0); + const creditSet = credit.greaterThan(0); + if (debitSet === creditSet) { + throw new InvalidLineError(`Line ${index + 1}: exactly one of debit or credit must be greater than zero`); + } + return { ...line, debit, credit }; + }); + + const totalDebits = sum(lines.map((line) => line.debit)); + const totalCredits = sum(lines.map((line) => line.credit)); + if (!totalDebits.equals(totalCredits)) { + throw new UnbalancedEntryError(totalDebits.toFixed(2), totalCredits.toFixed(2)); + } + + return prisma.$transaction(async (tx) => { + const journal = await tx.journal.findUniqueOrThrow({ where: { code: draft.journalCode } }); + + const fiscalPeriod = await tx.fiscalPeriod.findFirst({ + where: { status: 'OPEN', startDate: { lte: draft.entryDate }, endDate: { gte: draft.entryDate } }, + include: { fiscalYear: true }, + }); + if (!fiscalPeriod) { + throw new ClosedPeriodError(draft.entryDate); + } + + const entryNumber = await nextNumber(tx, 'JE', fiscalPeriod.fiscalYear.year); + + const entry = await tx.journalEntry.create({ + data: { + entryNumber, + journalId: journal.id, + fiscalPeriodId: fiscalPeriod.id, + entryDate: draft.entryDate, + description: draft.description, + sourceModule: draft.sourceModule, + sourceType: draft.sourceType, + sourceId: draft.sourceId, + status: JournalEntryStatus.POSTED, + reversalOfId: draft.reversalOfId, + totalDebits, + totalCredits, + createdById: draft.createdById, + lines: { + create: lines.map((line, index) => ({ + lineNumber: index + 1, + glAccountId: line.glAccountId, + debit: line.debit, + credit: line.credit, + description: line.description, + branchSegmentValueId: line.branchSegmentValueId, + arInvoiceId: line.arInvoiceId, + apInvoiceId: line.apInvoiceId, + stockMovementId: line.stockMovementId, + currencyCode: line.currencyCode ?? 'BASE', + exchangeRateToBase: line.exchangeRateToBase ? money(line.exchangeRateToBase) : money(1), + })), + }, + }, + include: { lines: true }, + }); + + if (draft.reversalOfId) { + await tx.journalEntry.update({ + where: { id: draft.reversalOfId }, + data: { status: JournalEntryStatus.REVERSED }, + }); + } + + return entry; + }); +} + +/** + * Reverses a posted entry by creating a new balanced entry with every line's debit/credit + * flipped, linked back via reversalOfId. The original is marked REVERSED, never edited or deleted. + */ +export async function reverseEntry(journalEntryId: number, createdById: number, reason?: string): Promise { + const original = await prisma.journalEntry.findUniqueOrThrow({ + where: { id: journalEntryId }, + include: { lines: true, journal: true }, + }); + + if (original.status === JournalEntryStatus.REVERSED) { + throw new InvalidLineError('This journal entry has already been reversed'); + } + + const draft: JournalEntryDraft = { + journalCode: original.journal.code, + entryDate: new Date(), + description: reason ? `Reversal of ${original.entryNumber}: ${reason}` : `Reversal of ${original.entryNumber}`, + sourceModule: original.sourceModule, + sourceType: original.sourceType ?? undefined, + sourceId: original.sourceId ?? undefined, + createdById, + reversalOfId: original.id, + lines: original.lines.map((line) => ({ + glAccountId: line.glAccountId, + debit: line.credit, + credit: line.debit, + description: line.description ?? undefined, + branchSegmentValueId: line.branchSegmentValueId ?? undefined, + arInvoiceId: line.arInvoiceId ?? undefined, + apInvoiceId: line.apInvoiceId ?? undefined, + stockMovementId: line.stockMovementId ?? undefined, + currencyCode: line.currencyCode, + exchangeRateToBase: line.exchangeRateToBase, + })), + }; + + return postEntry(draft); +} + +export const PostingService = { postEntry, reverseEntry }; diff --git a/src/modules/gl/posting.types.ts b/src/modules/gl/posting.types.ts new file mode 100644 index 0000000..92c6bb4 --- /dev/null +++ b/src/modules/gl/posting.types.ts @@ -0,0 +1,48 @@ +import { Prisma, SourceModule } from '@prisma/client'; + +export class UnbalancedEntryError extends Error { + constructor(totalDebits: string, totalCredits: string) { + super(`Journal entry does not balance: debits ${totalDebits} != credits ${totalCredits}`); + this.name = 'UnbalancedEntryError'; + } +} + +export class InvalidLineError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidLineError'; + } +} + +export class ClosedPeriodError extends Error { + constructor(entryDate: Date) { + super(`No open fiscal period covers ${entryDate.toISOString().slice(0, 10)}`); + this.name = 'ClosedPeriodError'; + } +} + +export interface JournalLineDraft { + glAccountId: number; + debit: Prisma.Decimal.Value; + credit: Prisma.Decimal.Value; + description?: string; + branchSegmentValueId?: number; + arInvoiceId?: number; + apInvoiceId?: number; + stockMovementId?: number; + currencyCode?: string; + exchangeRateToBase?: Prisma.Decimal.Value; +} + +export interface JournalEntryDraft { + journalCode: string; + entryDate: Date; + description: string; + sourceModule: SourceModule; + sourceType?: string; + sourceId?: number; + createdById: number; + lines: JournalLineDraft[]; + /** Internal use by reverseEntry(); do not set from calling modules. */ + reversalOfId?: number; +} diff --git a/src/modules/tax/taxCodes.controller.ts b/src/modules/tax/taxCodes.controller.ts new file mode 100644 index 0000000..df93a8c --- /dev/null +++ b/src/modules/tax/taxCodes.controller.ts @@ -0,0 +1,26 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as taxCodesService from './taxCodes.service'; + +export const createTaxCodeSchema = z.object({ + code: z.string().min(1), + name: z.string().min(1), + ratePercent: z.number().min(0).max(100), + isActive: z.boolean().optional(), +}); + +export const updateTaxCodeSchema = createTaxCodeSchema.partial(); + +export async function listTaxCodesHandler(_req: Request, res: Response): Promise { + res.json(await taxCodesService.listTaxCodes()); +} + +export async function createTaxCodeHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await taxCodesService.createTaxCode(input)); +} + +export async function updateTaxCodeHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.json(await taxCodesService.updateTaxCode(Number(req.params.id), input)); +} diff --git a/src/modules/tax/taxCodes.routes.ts b/src/modules/tax/taxCodes.routes.ts new file mode 100644 index 0000000..406ef31 --- /dev/null +++ b/src/modules/tax/taxCodes.routes.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createTaxCodeHandler, + createTaxCodeSchema, + listTaxCodesHandler, + updateTaxCodeHandler, + updateTaxCodeSchema, +} from './taxCodes.controller'; + +export const taxCodesRouter = Router(); + +taxCodesRouter.use(requireAuth); + +taxCodesRouter.get('/', asyncHandler(listTaxCodesHandler)); +taxCodesRouter.post('/', requirePermission('admin.tax.manage'), validate(createTaxCodeSchema), asyncHandler(createTaxCodeHandler)); +taxCodesRouter.put('/:id', requirePermission('admin.tax.manage'), validate(updateTaxCodeSchema), asyncHandler(updateTaxCodeHandler)); diff --git a/src/modules/tax/taxCodes.service.ts b/src/modules/tax/taxCodes.service.ts new file mode 100644 index 0000000..c57b29b --- /dev/null +++ b/src/modules/tax/taxCodes.service.ts @@ -0,0 +1,20 @@ +import { prisma } from '../../lib/prisma'; + +export interface TaxCodeInput { + code: string; + name: string; + ratePercent: number; + isActive?: boolean; +} + +export function listTaxCodes() { + return prisma.taxCode.findMany({ orderBy: { code: 'asc' } }); +} + +export function createTaxCode(input: TaxCodeInput) { + return prisma.taxCode.create({ data: input }); +} + +export function updateTaxCode(id: number, input: Partial) { + return prisma.taxCode.update({ where: { id }, data: input }); +} diff --git a/src/modules/users/roles.controller.ts b/src/modules/users/roles.controller.ts new file mode 100644 index 0000000..bf2d4ba --- /dev/null +++ b/src/modules/users/roles.controller.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as rolesService from './roles.service'; + +export const setRolePermissionsSchema = z.object({ + permissionIds: z.array(z.number().int()), +}); + +export async function listRolesHandler(req: Request, res: Response): Promise { + res.json(await rolesService.listRoles()); +} + +export async function listPermissionsHandler(req: Request, res: Response): Promise { + res.json(await rolesService.listPermissions()); +} + +export async function setRolePermissionsHandler(req: Request, res: Response): Promise { + const { permissionIds } = req.body as z.infer; + res.json(await rolesService.setRolePermissions(Number(req.params.id), permissionIds)); +} diff --git a/src/modules/users/roles.routes.ts b/src/modules/users/roles.routes.ts new file mode 100644 index 0000000..82e38b4 --- /dev/null +++ b/src/modules/users/roles.routes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + listPermissionsHandler, + listRolesHandler, + setRolePermissionsHandler, + setRolePermissionsSchema, +} from './roles.controller'; + +export const rolesRouter = Router(); + +rolesRouter.use(requireAuth, requirePermission('admin.users.manage')); + +rolesRouter.get('/', asyncHandler(listRolesHandler)); +rolesRouter.get('/permissions', asyncHandler(listPermissionsHandler)); +rolesRouter.put('/:id/permissions', validate(setRolePermissionsSchema), asyncHandler(setRolePermissionsHandler)); diff --git a/src/modules/users/roles.service.ts b/src/modules/users/roles.service.ts new file mode 100644 index 0000000..f8fc3e0 --- /dev/null +++ b/src/modules/users/roles.service.ts @@ -0,0 +1,17 @@ +import { prisma } from '../../lib/prisma'; + +export function listRoles() { + return prisma.role.findMany({ include: { permissions: { include: { permission: true } } }, orderBy: { name: 'asc' } }); +} + +export function listPermissions() { + return prisma.permission.findMany({ orderBy: { key: 'asc' } }); +} + +export async function setRolePermissions(roleId: number, permissionIds: number[]) { + await prisma.$transaction([ + prisma.rolePermission.deleteMany({ where: { roleId } }), + prisma.rolePermission.createMany({ data: permissionIds.map((permissionId) => ({ roleId, permissionId })) }), + ]); + return prisma.role.findUniqueOrThrow({ where: { id: roleId }, include: { permissions: { include: { permission: true } } } }); +} diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts new file mode 100644 index 0000000..d457c24 --- /dev/null +++ b/src/modules/users/users.controller.ts @@ -0,0 +1,28 @@ +import { Request, Response } from 'express'; +import { z } from 'zod'; +import * as usersService from './users.service'; + +export const createUserSchema = z.object({ + email: z.string().email(), + fullName: z.string().min(1), + roleIds: z.array(z.number().int()).min(1), + branchSegmentValueIds: z.array(z.number().int()).optional(), +}); + +export const setUserActiveSchema = z.object({ + isActive: z.boolean(), +}); + +export async function listUsersHandler(req: Request, res: Response): Promise { + res.json(await usersService.listUsers()); +} + +export async function createUserHandler(req: Request, res: Response): Promise { + const input = req.body as z.infer; + res.status(201).json(await usersService.createUser(input)); +} + +export async function setUserActiveHandler(req: Request, res: Response): Promise { + const { isActive } = req.body as z.infer; + res.json(await usersService.setUserActive(Number(req.params.id), isActive)); +} diff --git a/src/modules/users/users.routes.ts b/src/modules/users/users.routes.ts new file mode 100644 index 0000000..5f79ff5 --- /dev/null +++ b/src/modules/users/users.routes.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { requireAuth } from '../../middleware/auth'; +import { requirePermission } from '../../middleware/rbac'; +import { validate } from '../../middleware/validate'; +import { asyncHandler } from '../../lib/asyncHandler'; +import { + createUserHandler, + createUserSchema, + listUsersHandler, + setUserActiveHandler, + setUserActiveSchema, +} from './users.controller'; + +export const usersRouter = Router(); + +usersRouter.use(requireAuth, requirePermission('admin.users.manage')); + +usersRouter.get('/', asyncHandler(listUsersHandler)); +usersRouter.post('/', validate(createUserSchema), asyncHandler(createUserHandler)); +usersRouter.patch('/:id/active', validate(setUserActiveSchema), asyncHandler(setUserActiveHandler)); diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts new file mode 100644 index 0000000..07d4f07 --- /dev/null +++ b/src/modules/users/users.service.ts @@ -0,0 +1,43 @@ +import { prisma } from '../../lib/prisma'; + +export function listUsers() { + return prisma.user.findMany({ + select: { + id: true, + email: true, + fullName: true, + isActive: true, + roles: { include: { role: true } }, + branchRestrictions: true, + }, + orderBy: { fullName: 'asc' }, + }); +} + +export interface CreateUserInput { + email: string; + fullName: string; + roleIds: number[]; + branchSegmentValueIds?: number[]; +} + +// A User's identity is established by the Account Manager (see src/lib/handoffVerify.ts) and +// matched here by email — this just pre-provisions the local roles/branch restrictions for +// that email ahead of their first handoff. +export async function createUser(input: CreateUserInput) { + return prisma.user.create({ + data: { + email: input.email, + fullName: input.fullName, + roles: { create: input.roleIds.map((roleId) => ({ roleId })) }, + branchRestrictions: { + create: (input.branchSegmentValueIds ?? []).map((branchSegmentValueId) => ({ branchSegmentValueId })), + }, + }, + include: { roles: { include: { role: true } }, branchRestrictions: true }, + }); +} + +export function setUserActive(userId: number, isActive: boolean) { + return prisma.user.update({ where: { id: userId }, data: { isActive } }); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..f3837ce --- /dev/null +++ b/src/server.ts @@ -0,0 +1,9 @@ +import { createApp } from './app'; +import { env } from './config/env'; + +const app = createApp(); + +app.listen(env.port, () => { + // eslint-disable-next-line no-console + console.log(`API listening on http://localhost:${env.port}`); +}); diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..4f680d0 --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,18 @@ +import { BranchScope } from '../lib/branchFilter'; + +export interface AuthUser { + id: number; + email: string; + roles: string[]; + permissions: string[]; + allowedBranchSegmentValueIds: number[] | null; // null = unrestricted (not a BRANCH_USER) +} + +declare global { + namespace Express { + interface Request { + user?: AuthUser; + branchScope?: BranchScope; + } + } +} diff --git a/temp/mockups/Cross Financial Manage Mockup.png b/temp/mockups/Cross Financial Manage Mockup.png new file mode 100644 index 0000000..985779a Binary files /dev/null and b/temp/mockups/Cross Financial Manage Mockup.png differ diff --git a/tests/integration/ap.posting.spec.ts b/tests/integration/ap.posting.spec.ts new file mode 100644 index 0000000..16e04b5 --- /dev/null +++ b/tests/integration/ap.posting.spec.ts @@ -0,0 +1,235 @@ +/** + * Requires a real, migrated, seeded Postgres database (see SETUP.md) reachable via DATABASE_URL. + * Run with: npm run test:integration + */ +import { PrismaClient } from '@prisma/client'; +import * as apInvoicesService from '../../src/modules/ap/apInvoices.service'; +import * as apDebitNotesService from '../../src/modules/ap/apDebitNotes.service'; +import * as apPaymentsService from '../../src/modules/ap/apPayments.service'; +import { APAllocationError, APDocumentStateError } from '../../src/modules/ap/ap.errors'; + +const prisma = new PrismaClient(); + +async function getAccount(accountCode: string) { + return prisma.gLAccount.findUniqueOrThrow({ where: { accountCode } }); +} + +describe('Accounts Payable — posting and allocation (real DB)', () => { + let adminId: number; + let branchHQId: number; + let apAccount: { id: number }; + let expenseAccount: { id: number }; + let inputTaxAccount: { id: number }; + let bankAccount: { id: number }; + let supplierId: number; + let stdTaxCodeId: number; + + beforeAll(async () => { + const admin = await prisma.user.findUniqueOrThrow({ where: { email: 'joosth2002@icloud.com' } }); + adminId = admin.id; + + apAccount = await getAccount('2000-000-000-HQ'); + expenseAccount = await getAccount('5100-000-000-HQ'); + inputTaxAccount = await getAccount('2200-000-000-HQ'); + bankAccount = await getAccount('1000-000-000-HQ'); + + const hqSegmentValue = await prisma.accountSegmentValue.findFirstOrThrow({ + where: { code: 'HQ', segmentDef: { isBranchSegment: true } }, + }); + branchHQId = hqSegmentValue.id; + + const stdTaxCode = await prisma.taxCode.findUniqueOrThrow({ where: { code: 'STD' } }); + stdTaxCodeId = stdTaxCode.id; + + const supplier = await prisma.supplier.create({ + data: { + code: `AP-TEST-${Date.now()}`, + name: 'Integration Test Supplier', + branchSegmentValueId: branchHQId, + }, + }); + supplierId = supplier.id; + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + it('creates a draft invoice with computed tax and does not touch the GL', async () => { + const beforeJournalCount = await prisma.journalEntry.count(); + + const invoice = await apInvoicesService.createInvoiceDraft( + { + supplierId, + invoiceDate: '2026-03-01', + dueDate: '2026-03-31', + lines: [ + { description: 'Office supplies', quantity: 10, unitPrice: 100, expenseGLAccountId: expenseAccount.id, taxCodeId: stdTaxCodeId }, + { description: 'Zero-rated service', quantity: 1, unitPrice: 50, expenseGLAccountId: expenseAccount.id }, + ], + }, + adminId, + ); + + expect(invoice.status).toBe('DRAFT'); + expect(invoice.subtotal.toString()).toBe('1050'); + expect(invoice.taxTotal.toString()).toBe('150'); + expect(invoice.total.toString()).toBe('1200'); + expect(invoice.balance.toString()).toBe('1200'); + + const afterJournalCount = await prisma.journalEntry.count(); + expect(afterJournalCount).toBe(beforeJournalCount); + }); + + it('posts a supplier invoice to a balanced GL entry: Dr Expense + Dr Input Tax / Cr AP Control', async () => { + const draft = await apInvoicesService.createInvoiceDraft( + { + supplierId, + invoiceDate: '2026-03-05', + dueDate: '2026-04-04', + lines: [{ description: 'Consulting', quantity: 2, unitPrice: 500, expenseGLAccountId: expenseAccount.id, taxCodeId: stdTaxCodeId }], + }, + adminId, + ); + + const posted = await apInvoicesService.postInvoice(draft.id, adminId); + expect(posted.status).toBe('POSTED'); + expect(posted.journalEntryId).not.toBeNull(); + + const entry = await prisma.journalEntry.findUniqueOrThrow({ + where: { id: posted.journalEntryId! }, + include: { lines: true }, + }); + expect(entry.totalDebits.toString()).toBe(entry.totalCredits.toString()); + expect(entry.sourceModule).toBe('AP'); + expect(entry.sourceType).toBe('APInvoice'); + expect(entry.sourceId).toBe(posted.id); + + const apLine = entry.lines.find((line) => line.glAccountId === apAccount.id); + const expenseLine = entry.lines.find((line) => line.glAccountId === expenseAccount.id); + const taxLine = entry.lines.find((line) => line.glAccountId === inputTaxAccount.id); + expect(apLine?.credit.toString()).toBe('1150'); + expect(expenseLine?.debit.toString()).toBe('1000'); + expect(taxLine?.debit.toString()).toBe('150'); + expect(apLine?.apInvoiceId).toBe(posted.id); + }); + + it('rejects posting the same invoice twice', async () => { + const draft = await apInvoicesService.createInvoiceDraft( + { supplierId, invoiceDate: '2026-03-06', dueDate: '2026-04-05', lines: [{ description: 'X', quantity: 1, unitPrice: 10, expenseGLAccountId: expenseAccount.id }] }, + adminId, + ); + await apInvoicesService.postInvoice(draft.id, adminId); + await expect(apInvoicesService.postInvoice(draft.id, adminId)).rejects.toBeInstanceOf(APDocumentStateError); + }); + + it('posts a payment (Dr AP Control / Cr Bank) and allocates it against an open invoice', async () => { + const invoiceDraft = await apInvoicesService.createInvoiceDraft( + { supplierId, invoiceDate: '2026-03-10', dueDate: '2026-04-09', lines: [{ description: 'Goods', quantity: 1, unitPrice: 300, expenseGLAccountId: expenseAccount.id }] }, + adminId, + ); + const invoice = await apInvoicesService.postInvoice(invoiceDraft.id, adminId); + expect(invoice.balance.toString()).toBe('300'); + + const paymentDraft = await apPaymentsService.createPaymentDraft( + { supplierId, paymentDate: '2026-03-15', amount: 300, bankGLAccountId: bankAccount.id }, + adminId, + ); + const payment = await apPaymentsService.postPayment(paymentDraft.id, adminId); + expect(payment.status).toBe('POSTED'); + + const entry = await prisma.journalEntry.findUniqueOrThrow({ where: { id: payment.journalEntryId! }, include: { lines: true } }); + const apLine = entry.lines.find((line) => line.glAccountId === apAccount.id); + const bankLine = entry.lines.find((line) => line.glAccountId === bankAccount.id); + expect(apLine?.debit.toString()).toBe('300'); + expect(bankLine?.credit.toString()).toBe('300'); + + await apPaymentsService.allocatePayment(payment.id, [{ invoiceId: invoice.id, amount: 300 }]); + + const invoiceAfter = await prisma.aPInvoice.findUniqueOrThrow({ where: { id: invoice.id } }); + const paymentAfter = await prisma.aPPayment.findUniqueOrThrow({ where: { id: payment.id } }); + expect(invoiceAfter.balance.toString()).toBe('0'); + expect(paymentAfter.unallocatedAmount.toString()).toBe('0'); + }); + + it('rejects an allocation that exceeds the invoice balance', async () => { + const invoiceDraft = await apInvoicesService.createInvoiceDraft( + { supplierId, invoiceDate: '2026-03-11', dueDate: '2026-04-10', lines: [{ description: 'Goods', quantity: 1, unitPrice: 50, expenseGLAccountId: expenseAccount.id }] }, + adminId, + ); + const invoice = await apInvoicesService.postInvoice(invoiceDraft.id, adminId); + + const paymentDraft = await apPaymentsService.createPaymentDraft( + { supplierId, paymentDate: '2026-03-16', amount: 200, bankGLAccountId: bankAccount.id }, + adminId, + ); + const payment = await apPaymentsService.postPayment(paymentDraft.id, adminId); + + await expect(apPaymentsService.allocatePayment(payment.id, [{ invoiceId: invoice.id, amount: 200 }])).rejects.toBeInstanceOf( + APAllocationError, + ); + + const invoiceAfter = await prisma.aPInvoice.findUniqueOrThrow({ where: { id: invoice.id } }); + expect(invoiceAfter.balance.toString()).toBe('50'); + }); + + it('posts a debit note as the reverse of an invoice and allocates it against the invoice', async () => { + const invoiceDraft = await apInvoicesService.createInvoiceDraft( + { + supplierId, + invoiceDate: '2026-03-12', + dueDate: '2026-04-11', + lines: [{ description: 'Faulty goods', quantity: 1, unitPrice: 100, expenseGLAccountId: expenseAccount.id, taxCodeId: stdTaxCodeId }], + }, + adminId, + ); + const invoice = await apInvoicesService.postInvoice(invoiceDraft.id, adminId); + expect(invoice.total.toString()).toBe('115'); + + const debitNoteDraft = await apDebitNotesService.createDebitNoteDraft( + { + supplierId, + debitNoteDate: '2026-03-13', + reason: 'Return', + lines: [{ description: 'Faulty goods', quantity: 1, unitPrice: 100, expenseGLAccountId: expenseAccount.id, taxCodeId: stdTaxCodeId }], + }, + adminId, + ); + const debitNote = await apDebitNotesService.postDebitNote(debitNoteDraft.id, adminId); + expect(debitNote.total.toString()).toBe('115'); + + const entry = await prisma.journalEntry.findUniqueOrThrow({ where: { id: debitNote.journalEntryId! }, include: { lines: true } }); + expect(entry.totalDebits.toString()).toBe(entry.totalCredits.toString()); + const apLine = entry.lines.find((line) => line.glAccountId === apAccount.id); + const expenseLine = entry.lines.find((line) => line.glAccountId === expenseAccount.id); + expect(apLine?.debit.toString()).toBe('115'); + expect(expenseLine?.credit.toString()).toBe('100'); + + await apDebitNotesService.allocateDebitNote(debitNote.id, [{ invoiceId: invoice.id, amount: 115 }]); + + const invoiceAfter = await prisma.aPInvoice.findUniqueOrThrow({ where: { id: invoice.id } }); + const debitNoteAfter = await prisma.aPDebitNote.findUniqueOrThrow({ where: { id: debitNote.id } }); + expect(invoiceAfter.balance.toString()).toBe('0'); + expect(debitNoteAfter.balance.toString()).toBe('0'); + }); + + it('voids a draft invoice without touching the GL, and refuses to void a posted one', async () => { + const draft = await apInvoicesService.createInvoiceDraft( + { supplierId, invoiceDate: '2026-03-14', dueDate: '2026-04-13', lines: [{ description: 'X', quantity: 1, unitPrice: 10, expenseGLAccountId: expenseAccount.id }] }, + adminId, + ); + const voided = await apInvoicesService.voidInvoiceDraft(draft.id); + expect(voided.status).toBe('VOID'); + + const posted = await apInvoicesService.postInvoice( + ( + await apInvoicesService.createInvoiceDraft( + { supplierId, invoiceDate: '2026-03-14', dueDate: '2026-04-13', lines: [{ description: 'X', quantity: 1, unitPrice: 10, expenseGLAccountId: expenseAccount.id }] }, + adminId, + ) + ).id, + adminId, + ); + await expect(apInvoicesService.voidInvoiceDraft(posted.id)).rejects.toBeInstanceOf(APDocumentStateError); + }); +}); diff --git a/tests/integration/ar.posting.spec.ts b/tests/integration/ar.posting.spec.ts new file mode 100644 index 0000000..fd69940 --- /dev/null +++ b/tests/integration/ar.posting.spec.ts @@ -0,0 +1,235 @@ +/** + * Requires a real, migrated, seeded Postgres database (see SETUP.md) reachable via DATABASE_URL. + * Run with: npm run test:integration + */ +import { PrismaClient } from '@prisma/client'; +import * as arInvoicesService from '../../src/modules/ar/arInvoices.service'; +import * as arCreditNotesService from '../../src/modules/ar/arCreditNotes.service'; +import * as arReceiptsService from '../../src/modules/ar/arReceipts.service'; +import { ARAllocationError, ARDocumentStateError } from '../../src/modules/ar/ar.errors'; + +const prisma = new PrismaClient(); + +async function getAccount(accountCode: string) { + return prisma.gLAccount.findUniqueOrThrow({ where: { accountCode } }); +} + +describe('Accounts Receivable — posting and allocation (real DB)', () => { + let adminId: number; + let branchHQId: number; + let arAccount: { id: number }; + let salesAccount: { id: number }; + let taxAccount: { id: number }; + let bankAccount: { id: number }; + let customerId: number; + let stdTaxCodeId: number; + + beforeAll(async () => { + const admin = await prisma.user.findUniqueOrThrow({ where: { email: 'joosth2002@icloud.com' } }); + adminId = admin.id; + + arAccount = await getAccount('1100-000-000-HQ'); + salesAccount = await getAccount('4000-000-000-HQ'); + taxAccount = await getAccount('2100-000-000-HQ'); + bankAccount = await getAccount('1000-000-000-HQ'); + + const hqSegmentValue = await prisma.accountSegmentValue.findFirstOrThrow({ + where: { code: 'HQ', segmentDef: { isBranchSegment: true } }, + }); + branchHQId = hqSegmentValue.id; + + const stdTaxCode = await prisma.taxCode.findUniqueOrThrow({ where: { code: 'STD' } }); + stdTaxCodeId = stdTaxCode.id; + + const customer = await prisma.customer.create({ + data: { + code: `AR-TEST-${Date.now()}`, + name: 'Integration Test Customer', + branchSegmentValueId: branchHQId, + }, + }); + customerId = customer.id; + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + it('creates a draft invoice with computed tax and does not touch the GL', async () => { + const beforeJournalCount = await prisma.journalEntry.count(); + + const invoice = await arInvoicesService.createInvoiceDraft( + { + customerId, + invoiceDate: '2026-03-01', + dueDate: '2026-03-31', + lines: [ + { description: 'Widgets', quantity: 10, unitPrice: 100, revenueGLAccountId: salesAccount.id, taxCodeId: stdTaxCodeId }, + { description: 'Zero-rated service', quantity: 1, unitPrice: 50, revenueGLAccountId: salesAccount.id }, + ], + }, + adminId, + ); + + expect(invoice.status).toBe('DRAFT'); + expect(invoice.subtotal.toString()).toBe('1050'); + expect(invoice.taxTotal.toString()).toBe('150'); // 15% of 1000, zero-rated line contributes 0 + expect(invoice.total.toString()).toBe('1200'); + expect(invoice.balance.toString()).toBe('1200'); + + const afterJournalCount = await prisma.journalEntry.count(); + expect(afterJournalCount).toBe(beforeJournalCount); + }); + + it('posts an invoice to a balanced GL entry: Dr AR = total, Cr Sales = subtotal, Cr Output Tax = tax', async () => { + const draft = await arInvoicesService.createInvoiceDraft( + { + customerId, + invoiceDate: '2026-03-05', + dueDate: '2026-04-04', + lines: [{ description: 'Consulting', quantity: 2, unitPrice: 500, revenueGLAccountId: salesAccount.id, taxCodeId: stdTaxCodeId }], + }, + adminId, + ); + + const posted = await arInvoicesService.postInvoice(draft.id, adminId); + expect(posted.status).toBe('POSTED'); + expect(posted.journalEntryId).not.toBeNull(); + + const entry = await prisma.journalEntry.findUniqueOrThrow({ + where: { id: posted.journalEntryId! }, + include: { lines: true }, + }); + expect(entry.totalDebits.toString()).toBe(entry.totalCredits.toString()); + expect(entry.sourceModule).toBe('AR'); + expect(entry.sourceType).toBe('ARInvoice'); + expect(entry.sourceId).toBe(posted.id); + + const arLine = entry.lines.find((line) => line.glAccountId === arAccount.id); + const salesLine = entry.lines.find((line) => line.glAccountId === salesAccount.id); + const taxLine = entry.lines.find((line) => line.glAccountId === taxAccount.id); + expect(arLine?.debit.toString()).toBe('1150'); + expect(salesLine?.credit.toString()).toBe('1000'); + expect(taxLine?.credit.toString()).toBe('150'); + expect(arLine?.arInvoiceId).toBe(posted.id); + }); + + it('rejects posting the same invoice twice', async () => { + const draft = await arInvoicesService.createInvoiceDraft( + { customerId, invoiceDate: '2026-03-06', dueDate: '2026-04-05', lines: [{ description: 'X', quantity: 1, unitPrice: 10, revenueGLAccountId: salesAccount.id }] }, + adminId, + ); + await arInvoicesService.postInvoice(draft.id, adminId); + await expect(arInvoicesService.postInvoice(draft.id, adminId)).rejects.toBeInstanceOf(ARDocumentStateError); + }); + + it('posts a receipt (Dr Bank / Cr AR) and allocates it against an open invoice', async () => { + const invoiceDraft = await arInvoicesService.createInvoiceDraft( + { customerId, invoiceDate: '2026-03-10', dueDate: '2026-04-09', lines: [{ description: 'Goods', quantity: 1, unitPrice: 300, revenueGLAccountId: salesAccount.id }] }, + adminId, + ); + const invoice = await arInvoicesService.postInvoice(invoiceDraft.id, adminId); + expect(invoice.balance.toString()).toBe('300'); + + const receiptDraft = await arReceiptsService.createReceiptDraft( + { customerId, receiptDate: '2026-03-15', amount: 300, bankGLAccountId: bankAccount.id }, + adminId, + ); + const receipt = await arReceiptsService.postReceipt(receiptDraft.id, adminId); + expect(receipt.status).toBe('POSTED'); + + const entry = await prisma.journalEntry.findUniqueOrThrow({ where: { id: receipt.journalEntryId! }, include: { lines: true } }); + const bankLine = entry.lines.find((line) => line.glAccountId === bankAccount.id); + const arLine = entry.lines.find((line) => line.glAccountId === arAccount.id); + expect(bankLine?.debit.toString()).toBe('300'); + expect(arLine?.credit.toString()).toBe('300'); + + await arReceiptsService.allocateReceipt(receipt.id, [{ invoiceId: invoice.id, amount: 300 }]); + + const invoiceAfter = await prisma.aRInvoice.findUniqueOrThrow({ where: { id: invoice.id } }); + const receiptAfter = await prisma.aRReceipt.findUniqueOrThrow({ where: { id: receipt.id } }); + expect(invoiceAfter.balance.toString()).toBe('0'); + expect(receiptAfter.unallocatedAmount.toString()).toBe('0'); + }); + + it('rejects an allocation that exceeds the invoice balance', async () => { + const invoiceDraft = await arInvoicesService.createInvoiceDraft( + { customerId, invoiceDate: '2026-03-11', dueDate: '2026-04-10', lines: [{ description: 'Goods', quantity: 1, unitPrice: 50, revenueGLAccountId: salesAccount.id }] }, + adminId, + ); + const invoice = await arInvoicesService.postInvoice(invoiceDraft.id, adminId); + + const receiptDraft = await arReceiptsService.createReceiptDraft( + { customerId, receiptDate: '2026-03-16', amount: 200, bankGLAccountId: bankAccount.id }, + adminId, + ); + const receipt = await arReceiptsService.postReceipt(receiptDraft.id, adminId); + + await expect(arReceiptsService.allocateReceipt(receipt.id, [{ invoiceId: invoice.id, amount: 200 }])).rejects.toBeInstanceOf( + ARAllocationError, + ); + + const invoiceAfter = await prisma.aRInvoice.findUniqueOrThrow({ where: { id: invoice.id } }); + expect(invoiceAfter.balance.toString()).toBe('50'); + }); + + it('posts a credit note as the reverse of an invoice and allocates it against the invoice', async () => { + const invoiceDraft = await arInvoicesService.createInvoiceDraft( + { + customerId, + invoiceDate: '2026-03-12', + dueDate: '2026-04-11', + lines: [{ description: 'Faulty goods', quantity: 1, unitPrice: 100, revenueGLAccountId: salesAccount.id, taxCodeId: stdTaxCodeId }], + }, + adminId, + ); + const invoice = await arInvoicesService.postInvoice(invoiceDraft.id, adminId); + expect(invoice.total.toString()).toBe('115'); + + const creditNoteDraft = await arCreditNotesService.createCreditNoteDraft( + { + customerId, + creditNoteDate: '2026-03-13', + reason: 'Return', + lines: [{ description: 'Faulty goods', quantity: 1, unitPrice: 100, revenueGLAccountId: salesAccount.id, taxCodeId: stdTaxCodeId }], + }, + adminId, + ); + const creditNote = await arCreditNotesService.postCreditNote(creditNoteDraft.id, adminId); + expect(creditNote.total.toString()).toBe('115'); + + const entry = await prisma.journalEntry.findUniqueOrThrow({ where: { id: creditNote.journalEntryId! }, include: { lines: true } }); + expect(entry.totalDebits.toString()).toBe(entry.totalCredits.toString()); + const arLine = entry.lines.find((line) => line.glAccountId === arAccount.id); + const salesLine = entry.lines.find((line) => line.glAccountId === salesAccount.id); + expect(arLine?.credit.toString()).toBe('115'); + expect(salesLine?.debit.toString()).toBe('100'); + + await arCreditNotesService.allocateCreditNote(creditNote.id, [{ invoiceId: invoice.id, amount: 115 }]); + + const invoiceAfter = await prisma.aRInvoice.findUniqueOrThrow({ where: { id: invoice.id } }); + const creditNoteAfter = await prisma.aRCreditNote.findUniqueOrThrow({ where: { id: creditNote.id } }); + expect(invoiceAfter.balance.toString()).toBe('0'); + expect(creditNoteAfter.balance.toString()).toBe('0'); + }); + + it('voids a draft invoice without touching the GL, and refuses to void a posted one', async () => { + const draft = await arInvoicesService.createInvoiceDraft( + { customerId, invoiceDate: '2026-03-14', dueDate: '2026-04-13', lines: [{ description: 'X', quantity: 1, unitPrice: 10, revenueGLAccountId: salesAccount.id }] }, + adminId, + ); + const voided = await arInvoicesService.voidInvoiceDraft(draft.id); + expect(voided.status).toBe('VOID'); + + const posted = await arInvoicesService.postInvoice( + ( + await arInvoicesService.createInvoiceDraft( + { customerId, invoiceDate: '2026-03-14', dueDate: '2026-04-13', lines: [{ description: 'X', quantity: 1, unitPrice: 10, revenueGLAccountId: salesAccount.id }] }, + adminId, + ) + ).id, + adminId, + ); + await expect(arInvoicesService.voidInvoiceDraft(posted.id)).rejects.toBeInstanceOf(ARDocumentStateError); + }); +}); diff --git a/tests/integration/handoff.spec.ts b/tests/integration/handoff.spec.ts new file mode 100644 index 0000000..9baa919 --- /dev/null +++ b/tests/integration/handoff.spec.ts @@ -0,0 +1,82 @@ +/** + * Requires the real, migrated, seeded company-app database (see SETUP.md) reachable via + * DATABASE_URL, and the Manager's real handoff keypair at ../manager/keys/ (see + * manager/SETUP.md — npm run generate-handoff-keys). Run with: npm run test:integration + */ +import fs from 'fs'; +import path from 'path'; +import jwt from 'jsonwebtoken'; +import crypto from 'crypto'; +import request from 'supertest'; +import { createApp } from '../../src/app'; +import { prisma } from '../../src/lib/prisma'; + +const app = createApp(); +const managerPrivateKey = fs.readFileSync(path.resolve(__dirname, '../../manager/keys/handoff-private.pem'), 'utf8'); + +const ADMIN_EMAIL = 'joosth2002@icloud.com'; + +function mintHandoffToken(overrides: Partial<{ sub: string; companySlug: string; expiresIn: number }> = {}) { + const claims = { sub: overrides.sub ?? ADMIN_EMAIL, pid: 1, cid: 1, companySlug: overrides.companySlug ?? 'demo' }; + return jwt.sign(claims, managerPrivateKey, { + algorithm: 'RS256', + expiresIn: overrides.expiresIn ?? 90, + jwtid: crypto.randomUUID(), + }); +} + +describe('Company app — handoff token exchange (real DB, real keypair)', () => { + afterAll(async () => { + await prisma.$disconnect(); + }); + + it('exchanges a valid handoff token for a working session', async () => { + const token = mintHandoffToken(); + const response = await request(app).post('/api/auth/handoff').send({ token }); + expect(response.status).toBe(200); + expect(response.body.accessToken).toBeDefined(); + expect(response.body.user.email).toBe(ADMIN_EMAIL); + + const me = await request(app).get('/api/auth/me').set('Authorization', `Bearer ${response.body.accessToken}`); + expect(me.status).toBe(200); + expect(me.body.user.email).toBe(ADMIN_EMAIL); + }); + + it('rejects the same token twice (single-use / replay protection)', async () => { + const token = mintHandoffToken(); + const first = await request(app).post('/api/auth/handoff').send({ token }); + expect(first.status).toBe(200); + + const second = await request(app).post('/api/auth/handoff').send({ token }); + expect(second.status).toBe(401); + expect(second.body.error).toMatch(/already been used/i); + }); + + it('rejects a token minted for a different company slug', async () => { + const token = mintHandoffToken({ companySlug: 'some-other-company' }); + const response = await request(app).post('/api/auth/handoff').send({ token }); + expect(response.status).toBe(401); + expect(response.body.error).toMatch(/not issued for this company/i); + }); + + it('rejects an expired token', async () => { + const token = mintHandoffToken({ expiresIn: -10 }); + const response = await request(app).post('/api/auth/handoff').send({ token }); + expect(response.status).toBe(401); + }); + + it('rejects a well-formed but unsigned/forged token', async () => { + const forged = jwt.sign({ sub: ADMIN_EMAIL, pid: 1, cid: 1, companySlug: 'demo' }, 'not-the-real-key', { + algorithm: 'HS256', + expiresIn: 90, + jwtid: crypto.randomUUID(), + }); + const response = await request(app).post('/api/auth/handoff').send({ token: forged }); + expect(response.status).toBe(401); + }); + + it('the old POST /api/auth/login route no longer exists', async () => { + const response = await request(app).post('/api/auth/login').send({ email: 'x', password: 'y' }); + expect(response.status).toBe(404); + }); +}); diff --git a/tests/integration/posting.balance.spec.ts b/tests/integration/posting.balance.spec.ts new file mode 100644 index 0000000..1ce9c62 --- /dev/null +++ b/tests/integration/posting.balance.spec.ts @@ -0,0 +1,126 @@ +/** + * Requires a real, migrated, seeded Postgres database (see SETUP.md) reachable via DATABASE_URL. + * Run with: npm run test:integration + */ +import { PrismaClient } from '@prisma/client'; +import { PostingService } from '../../src/modules/gl/posting.service'; +import { UnbalancedEntryError } from '../../src/modules/gl/posting.types'; + +const prisma = new PrismaClient(); + +async function getAccount(accountCode: string) { + return prisma.gLAccount.findUniqueOrThrow({ where: { accountCode } }); +} + +describe('PostingService — double-entry balance invariant (real DB)', () => { + let adminId: number; + let bankHQ: { id: number }; + let salesHQ: { id: number }; + + beforeAll(async () => { + const admin = await prisma.user.findUniqueOrThrow({ where: { email: 'joosth2002@icloud.com' } }); + adminId = admin.id; + bankHQ = await getAccount('1000-000-000-HQ'); + salesHQ = await getAccount('4000-000-000-HQ'); + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + it('posts a balanced entry and persists matching totals', async () => { + const entry = await PostingService.postEntry({ + journalCode: 'GJ', + entryDate: new Date(), + description: 'Integration test — balanced entry', + sourceModule: 'GL', + sourceType: 'IntegrationTest', + createdById: adminId, + lines: [ + { glAccountId: bankHQ.id, debit: 250, credit: 0 }, + { glAccountId: salesHQ.id, debit: 0, credit: 250 }, + ], + }); + + expect(entry.totalDebits.toString()).toBe('250'); + expect(entry.totalCredits.toString()).toBe('250'); + + const lines = await prisma.journalEntryLine.findMany({ where: { journalEntryId: entry.id } }); + const totalDebits = lines.reduce((sum, line) => sum + Number(line.debit), 0); + const totalCredits = lines.reduce((sum, line) => sum + Number(line.credit), 0); + expect(totalDebits).toBe(totalCredits); + }); + + it('rejects an unbalanced entry via the application layer and writes nothing', async () => { + const before = await prisma.journalEntry.count(); + + await expect( + PostingService.postEntry({ + journalCode: 'GJ', + entryDate: new Date(), + description: 'Integration test — unbalanced entry', + sourceModule: 'GL', + sourceType: 'IntegrationTest', + createdById: adminId, + lines: [ + { glAccountId: bankHQ.id, debit: 100, credit: 0 }, + { glAccountId: salesHQ.id, debit: 0, credit: 40 }, + ], + }), + ).rejects.toBeInstanceOf(UnbalancedEntryError); + + const after = await prisma.journalEntry.count(); + expect(after).toBe(before); + }); + + it('rejects an unbalanced write at the database layer even bypassing PostingService', async () => { + // Prove Layer 3 (the deferred trigger) is the real backstop: attempt to insert an + // unbalanced JournalEntryLine set directly via raw SQL, bypassing the service entirely. + // + // Note: with Prisma's interactive transactions, a commit failure caused by a DEFERRED + // constraint trigger is not always surfaced as a rejected promise (observed with + // @prisma/client 5.22 — the engine logs "transaction failed to commit" and correctly + // rolls back at the database level, but the JS promise can still resolve). So the + // authoritative assertion here is "nothing persisted", checked directly against the + // database after the attempt — not whether the promise rejected. + const marker = `TRIGGER-TEST-${Date.now()}`; + + try { + await prisma.$transaction(async (tx) => { + const entry = await tx.journalEntry.create({ + data: { + entryNumber: marker, + journalId: (await tx.journal.findUniqueOrThrow({ where: { code: 'GJ' } })).id, + fiscalPeriodId: ( + await tx.fiscalPeriod.findFirstOrThrow({ where: { status: 'OPEN' } }) + ).id, + entryDate: new Date(), + description: 'Trigger bypass attempt — must be rejected at commit', + sourceModule: 'GL', + status: 'POSTED', + totalDebits: 100, + totalCredits: 100, + createdById: adminId, + }, + }); + await tx.$executeRawUnsafe( + `INSERT INTO "JournalEntryLine" ("journalEntryId", "lineNumber", "glAccountId", debit, credit, "currencyCode", "exchangeRateToBase") + VALUES ($1, 1, $2, 100, 0, 'BASE', 1)`, + entry.id, + bankHQ.id, + ); + await tx.$executeRawUnsafe( + `INSERT INTO "JournalEntryLine" ("journalEntryId", "lineNumber", "glAccountId", debit, credit, "currencyCode", "exchangeRateToBase") + VALUES ($1, 2, $2, 0, 40, 'BASE', 1)`, + entry.id, + salesHQ.id, + ); + }); + } catch { + // Expected outcome on Prisma versions that do propagate the commit failure — fine either way. + } + + const persisted = await prisma.journalEntry.findUnique({ where: { entryNumber: marker } }); + expect(persisted).toBeNull(); + }); +}); diff --git a/tests/unit/posting.validation.spec.ts b/tests/unit/posting.validation.spec.ts new file mode 100644 index 0000000..22a346f --- /dev/null +++ b/tests/unit/posting.validation.spec.ts @@ -0,0 +1,93 @@ +import { InvalidLineError, UnbalancedEntryError } from '../../src/modules/gl/posting.types'; + +const transactionMock = jest.fn(); + +jest.mock('../../src/lib/prisma', () => ({ + prisma: { $transaction: transactionMock }, +})); + +// eslint-disable-next-line import/first +import { postEntry } from '../../src/modules/gl/posting.service'; + +const baseDraft = { + journalCode: 'GJ', + entryDate: new Date('2026-01-15'), + description: 'Test entry', + sourceModule: 'GL' as const, + createdById: 1, +}; + +describe('PostingService.postEntry — pre-write validation (no DB required)', () => { + beforeEach(() => { + transactionMock.mockReset(); + }); + + it('rejects an unbalanced entry before touching the database', async () => { + await expect( + postEntry({ + ...baseDraft, + lines: [ + { glAccountId: 1, debit: 100, credit: 0 }, + { glAccountId: 2, debit: 0, credit: 50 }, + ], + }), + ).rejects.toBeInstanceOf(UnbalancedEntryError); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it('rejects a line with both debit and credit set', async () => { + await expect( + postEntry({ + ...baseDraft, + lines: [ + { glAccountId: 1, debit: 100, credit: 100 }, + { glAccountId: 2, debit: 0, credit: 0 }, + ], + }), + ).rejects.toBeInstanceOf(InvalidLineError); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it('rejects a line with neither debit nor credit set', async () => { + await expect( + postEntry({ + ...baseDraft, + lines: [ + { glAccountId: 1, debit: 0, credit: 0 }, + { glAccountId: 2, debit: 100, credit: 100 }, + ], + }), + ).rejects.toBeInstanceOf(InvalidLineError); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it('rejects a negative amount', async () => { + await expect( + postEntry({ + ...baseDraft, + lines: [ + { glAccountId: 1, debit: -50, credit: 0 }, + { glAccountId: 2, debit: 0, credit: -50 }, + ], + }), + ).rejects.toBeInstanceOf(InvalidLineError); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it('rejects an entry with no lines', async () => { + await expect(postEntry({ ...baseDraft, lines: [] })).rejects.toBeInstanceOf(InvalidLineError); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it('proceeds to the DB transaction once a balanced, valid draft is given', async () => { + transactionMock.mockResolvedValue({ id: 1 }); + await postEntry({ + ...baseDraft, + lines: [ + { glAccountId: 1, debit: 100, credit: 0 }, + { glAccountId: 2, debit: 0, credit: 100 }, + ], + }); + expect(transactionMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6d5aa8e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "prisma/seed.ts", "scripts/**/*.ts"], + "exclude": ["node_modules", "dist", "client"] +}