From fea25d74295e3a42aed622b1321e9f34cf24a298 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:03:28 +0200 Subject: [PATCH] first --- .env.example | 18 + .gitignore | 4 + README.md | 197 +- documentation/polstrin-specifika.md | 44 + .../sap-business-one-service-layer.md | 70 + package-lock.json | 2431 +++++++++++++++++ package.json | 15 +- src/SapBusinessOneServiceLayer.ts | 103 + src/auth/SessionManager.ts | 177 ++ src/client/SapB1Client.ts | 152 ++ src/client/axiosTypes.ts | 8 + src/client/odata.ts | 35 + src/config.ts | 47 + src/errors/SapB1Error.ts | 68 + src/index.ts | 853 +++++- src/resources/BusinessPartners.ts | 15 + src/resources/Documents.ts | 38 + src/resources/Items.ts | 10 + src/resources/Resource.ts | 44 + src/resources/StockTransfers.ts | 10 + src/types/entities.ts | 73 + src/types/odata.ts | 34 + src/types/schemas.ts | 19 + src/utils/logger.ts | 27 + src/utils/sleep.ts | 3 + tests/auth.test.ts | 123 + tests/client.test.ts | 154 ++ tests/odata.test.ts | 27 + tests/resources.test.ts | 22 + tests/systemInfo.test.ts | 106 + tsconfig.json | 9 +- 31 files changed, 4906 insertions(+), 30 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 documentation/polstrin-specifika.md create mode 100644 documentation/sap-business-one-service-layer.md create mode 100644 package-lock.json create mode 100644 src/SapBusinessOneServiceLayer.ts create mode 100644 src/auth/SessionManager.ts create mode 100644 src/client/SapB1Client.ts create mode 100644 src/client/axiosTypes.ts create mode 100644 src/client/odata.ts create mode 100644 src/config.ts create mode 100644 src/errors/SapB1Error.ts create mode 100644 src/resources/BusinessPartners.ts create mode 100644 src/resources/Documents.ts create mode 100644 src/resources/Items.ts create mode 100644 src/resources/Resource.ts create mode 100644 src/resources/StockTransfers.ts create mode 100644 src/types/entities.ts create mode 100644 src/types/odata.ts create mode 100644 src/types/schemas.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/sleep.ts create mode 100644 tests/auth.test.ts create mode 100644 tests/client.test.ts create mode 100644 tests/odata.test.ts create mode 100644 tests/resources.test.ts create mode 100644 tests/systemInfo.test.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..64893f5 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# SAP credentials jsou vnitrni secrets (AppFactory portal -> runtime .env). +# Nikdy se neposilaji v requestech, nelogujou a necommituji. +SAP_B1_BASE_URL=https://ws.polstrin.cz:50000 +SAP_B1_COMPANY_DB= +SAP_B1_USERNAME= +SAP_B1_PASSWORD= +SAP_B1_LANGUAGE= +SAP_B1_TIMEOUT_MS=30000 +SAP_B1_REJECT_UNAUTHORIZED=true +SAP_B1_RETRY_COUNT=2 +SAP_B1_RETRY_DELAY_MS=250 + +# Volitelny sdileny klic pro /api routy; kdyz je nastaveny, kazdy request +# musi poslat hlavicku X-Api-Key se stejnou hodnotou. +API_KEY= + +PORT=3000 +ROOT_PATH=/apps/polstrin-sap diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e7f658 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +.env +coverage/ diff --git a/README.md b/README.md index 8c7f886..815f3ce 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,195 @@ -# Polstrin SAP BO1 +# POLSTRIN SAP Business One Service Layer Connector -Node.js TypeScript služba vytvořená přes CSBot Services Portal. +Node.js + TypeScript connector pro SAP Business One Service Layer REST/OData API, dedikovaný instalaci **POLSTRIN DESIGN s.r.o.** (`https://ws.polstrin.cz:50000`, SAP B1 10.0, verze `1000310`). Používá pouze Service Layer (`/b1s/v1`), ne SAP DI API. Vznikl jako kopie obecné služby `sap-bo`; hlavní rozdíly: -## Endpointy +- SAP credentials se **nepředávají v hlavičkách**, ale čtou se z environment variables (AppFactory secrets, viz AGENTS.md „Secrets v parametrech“). +- Jedna sdílená Service Layer session přežívá mezi requesty (re-login při expiraci/401 řeší `SessionManager`). +- Volitelná ochrana `/api` rout sdíleným klíčem: když je nastavený secret `API_KEY`, každý request musí poslat hlavičku `X-Api-Key`. +- Generické endpointy `/api/entities/...` pro práci s libovolným entity setem — hlavně pro UDO tabulky POLSTRIN addonů (`U_ADN_*`, `U_DFX_*`, `U_PVT_*`, `U_VCZ_*`, `VYROBNI_PLAN`, `VYROBNI_DAVKA`). Detaily v [documentation/polstrin-specifika.md](documentation/polstrin-specifika.md). -- GET / -- GET /health +Součástí repozitáře je AppFactory HTTP wrapper s endpointy: + +- `GET /` +- `GET /health` +- `GET /docs` +- `GET /openapi.json` +- `POST /api/session/login` (ověří spojení, vrací verzi Service Layeru a session timeout) +- `POST /api/session/logout` +- `GET /api/system/info` (verze SAP, dostupné entity sety, UDF/UDT/UDO, admin info) +- `GET /api/entities` (seznam všech entity setů) +- `GET|POST /api/entities/{entitySet}`, `GET /api/entities/{entitySet}/all` +- `GET|PATCH|DELETE /api/entities/{entitySet}/{id}` (`?idType=number` pro číselné klíče) +- `GET /api/` +- `GET /api//all` +- `GET /api//{id}` +- `POST /api/` +- `PATCH /api//{id}` +- `DELETE /api//{id}` pouze pro obecně mazatelné resource + +## Instalace + +```bash +npm install +npm run build +npm test +``` + +## Konfigurace (secrets přes environment variables) + +Všechny SAP credentials se nastavují přes AppFactory portál jako variables/secrets; aplikace je čte z environment variables. Nikdy se nepředávají v requestech, nelogují se a necommitují. Lokálně vytvoř `.env` podle `.env.example`: + +```env +SAP_B1_BASE_URL=https://ws.polstrin.cz:50000 +SAP_B1_COMPANY_DB= +SAP_B1_USERNAME= +SAP_B1_PASSWORD= +SAP_B1_LANGUAGE= +SAP_B1_TIMEOUT_MS=30000 +SAP_B1_REJECT_UNAUTHORIZED=true +SAP_B1_RETRY_COUNT=2 +SAP_B1_RETRY_DELAY_MS=250 +API_KEY= +``` + +`SAP_B1_BASE_URL` může být buď root Service Layer hostu, nebo přímo URL končící `/b1s/v1`. Pro self-signed certifikáty lze v interním prostředí nastavit `SAP_B1_REJECT_UNAUTHORIZED=false`; v produkci preferuj důvěryhodný certifikát a ponech `true`. + +## Autentizace HTTP API + +SAP credentials jsou vnitřní secret služby — klienti je neposílají. Když je nastavený secret `API_KEY`, musí každý request na `/api/...` obsahovat hlavičku: + +```http +X-Api-Key: +``` + +Bez nastaveného `API_KEY` jsou `/api` routy otevřené (vhodné jen pro interní síť). Ve Swaggeru (`/docs`) se klíč vyplňuje přes `Authorize`. + +## Použití + +### Login a logout + +```ts +import { SapBusinessOneServiceLayer, loadSapB1ConfigFromEnv } from "./src"; + +const sap = new SapBusinessOneServiceLayer(loadSapB1ConfigFromEnv()); + +await sap.login(); +await sap.logout(); +``` + +Login volá `POST /b1s/v1/Login`, uloží cookies `B1SESSION` a `ROUTEID` a posílá je v dalších requestech. Při expiraci session a odpovědi `401` connector jednou provede re-login a request zopakuje. + +### Vypsání Business Partners + +```ts +const partners = await sap.businessPartners.list({ + select: ["CardCode", "CardName", "CardType"], + filter: "CardType eq 'cCustomer'", + top: 50, + orderby: "CardName asc" +}); + +console.log(partners.value); +``` + +### Načtení všech záznamů přes nextLink + +```ts +const allItems = await sap.items.listAll({ + select: ["ItemCode", "ItemName"], + top: 100 +}); +``` + +Connector podporuje starší `odata.nextLink` i novější `@odata.nextLink`. + +### Vytvoření objednávky + +```ts +const order = await sap.orders.create({ + CardCode: "C001", + DocDueDate: "2026-07-15", + DocumentLines: [ + { + ItemCode: "A00001", + Quantity: 2, + UnitPrice: 100 + } + ] +}); +``` + +### Aktualizace položky + +```ts +await sap.items.update("A00001", { + ItemName: "Updated item name" +}); +``` + +## Resource moduly + +Implementované resource moduly: + +- `businessPartners` +- `items` +- `orders` +- `invoices` +- `purchaseOrders` +- `deliveryNotes` +- `stockTransfers` + +Každý modul má: + +- `list(query?)` +- `listAll(query?)` +- `get(id)` +- `create(data)` +- `update(id, data)` +- `delete(id)` pouze tam, kde je povolené mazání + +U marketing dokumentů (`Orders`, `Invoices`, `PurchaseOrders`, `DeliveryNotes`) a skladových převodek je delete v connectoru záměrně blokovaný. SAP Business One obvykle řeší rušení dokumentů storno/cancel operacemi podle typu dokladu a nastavení firmy. + +## Zpracování chyb + +Connector převádí chyby na `SapB1Error`: + +- `status` HTTP status +- `code` SAP error code, pokud jej Service Layer vrátí +- `message` bezpečná chybová zpráva +- `retryable` příznak pro dočasné chyby + +Retry se používá pro `408`, `429` a `5xx`. Citlivé hodnoty jako heslo, cookies a session tokeny se při logování redigují. + +## Testy + +Testy používají mockované HTTP odpovědi přes axios adapter nebo fake klienty. Nevolají reálný SAP. + +```bash +npm test +``` + +Pokryté oblasti: + +- autentizace a cookies +- logout +- automatický re-login po `401` +- OData query parametry +- resource URL builder + +## AppFactory + +Aplikace poslouchá na `0.0.0.0` a portu `PORT` s výchozí hodnotou `3000`. `ROOT_PATH` se používá pro dokumentaci a testovací requesty za reverse proxy, například `/apps/polstrin-sap`. + +`/docs` je Swagger UI servírované přímo jako HTML (stejně jako sousední `google-service`), +bez statického middleware – proto nevzniká redirect `/docs` → `/docs/`, který by za proxy +zahodil prefix. Assety se načítají z CDN, spec URL je `ROOT_PATH + /openapi.json`. V Swaggeru +použij `Authorize` pro vyplnění `X-Api-Key` (pokud je `API_KEY` nastavený) a potom `Try it out` +u konkrétní operace. OpenAPI `servers` se nastaví z `ROOT_PATH` (jinak `/`), takže za proxy +volá například `/apps/polstrin-sap/api/business-partners`, ne root doménu. + +## TODO ověřit v konkrétní instalaci SAP Business One + +- Přesné enum hodnoty a povinná pole pro jednotlivé entity se mohou lišit podle lokalizace, add-onů a verze SAP Business One. +- U rušení dokladů ověř konkrétní Service Layer akce dostupné pro daný typ dokladu a firemní nastavení. +- U velkých datasetů ověř server-side limity stránkování a maximální povolené `$top`. +- Ověř, zda konkrétní instalace vrací OData metadata ve starším formátu `odata.*` nebo novějším `@odata.*`. diff --git a/documentation/polstrin-specifika.md b/documentation/polstrin-specifika.md new file mode 100644 index 0000000..ff00039 --- /dev/null +++ b/documentation/polstrin-specifika.md @@ -0,0 +1,44 @@ +# Specifika instalace POLSTRIN DESIGN s.r.o. + +Zjištěno z `GET /api/system/info` (10. 7. 2026, testovací databáze). + +## Základní údaje + +- Service Layer: `https://ws.polstrin.cz:50000/b1s/v1` +- Verze SAP Business One: `1000310` (SAP B1 10.0), session timeout 30 minut +- Firma: POLSTRIN DESIGN s.r.o., Hradec Králové, DIČ `CZ49283120` +- Firemní měna `CZK`, systémová měna `EUR`, účtová osnova šablona `U` +- Průběžné vedení skladu metodou **Moving Average**, výchozí sklad `01.100` +- Výchozí kódy DPH: prodej `UP21`, nákup `PP21` +- **Pozor:** dodaný výpis pochází z databáze pojmenované „TEST 10.7.2026 POLSTRIN DESIGN, s.r.o.“ — před produkčním nasazením ověř, že secret `SAP_B1_COMPANY_DB` míří na správnou (produkční) databázi. + +## Nainstalované addony a jejich prefixy + +Instalace obsahuje rozsáhlé customizace. UDO tabulky jsou v Service Layeru dostupné jako entity sety `U__*` (přes `/api/entities/...`), UDF se stejnými prefixy jsou přidané na standardních objektech (artikly `OITM`, obchodní partneři `OCRD`, všechny marketingové doklady, montážní zakázky `OWOR`, kusovníky `OITT` atd.) a vrací se v běžných odpovědích — stačí je uvést v `$select`. + +| Prefix | Oblast | +|---|---| +| `ADN_` / `U_ADN_*` | Evidence majetku (karty majetku, odpisy daňové/účetní, pohyby, inventury, ceniny, pronájmy) | +| `DFX_` / `U_DFX_*` | Intrastat a evidence majetku CZ (hlášení, nomenklatury, číselníky, verze addonu) | +| `PVT_` / `U_PVT_*` | Intrastat (KN8 kódy, kurzy, statistické kódy, tisková data) | +| `VCZ_` / `U_VCZ_*` | Versino CZ – lokalizace a výroba: EET, elektronická banka, platební příkazy (KS/SS/VS), QR platba, kontrolní/souhrnné hlášení DPH, kalkulace, plánování výroby (`MPK*`), pracoviště (`MWPL*`), trasování výroby (`MTTRC`), pick listy, tiskové reporty | +| `B1SYS_` | Systémová CZ lokalizace (EET pole PKP/BKP na dokladech) | +| `BOE*`, `BOO*`, `BOQUOT`, `BOSETTINGS` | Outlook/Office integrace (sync kalendáře, kontaktů, šablony Word/Excel) | +| `VYROBNI_PLAN`, `VYROBNI_DAVKA` | Vlastní UDO pro výrobní plánování | + +Často užitečná pole na dokladech: `VCZ_5030` (číslo dokladu), `VCZ_7042` (variabilní symbol), `VCZ_6502`/`VCZ_7141` (KS/SS), `VCZ_9522`/`VCZ_9192` (vazba na zakázku odběratele a její řádek), `VCZ_5EE*` (EET), `DFX_*`/`PVT_*` (Intrastat), na artiklu `VCZ_5315/5316/5317` (popisy CZ/EN/RU) a `PVT_KINT` (kód Intrastat). + +## Oprávnění SAP uživatele + +Aktuální SAP uživatel **nemá oprávnění** číst `UserTablesMD` a `UserObjectsMD` (v `/api/system/info` se vrací `{ "error": "The logged-on user does not have permission..." }`). Data UDO tabulek jsou přesto dostupná přes jejich `U_*` entity sety; pro čtení metadat UDT/UDO je potřeba uživateli doplnit oprávnění v SAP (General → User-Defined Objects/Tables). + +## Práce s customizacemi přes API + +```http +GET /api/entities # seznam všech entity setů +GET /api/entities/U_VCZ_MKONF?$top=10 # konfigurátor výroby +GET /api/entities/U_ADN_MEM/all # všechny karty majetku +GET /api/entities/VYROBNI_PLAN/'001' # klíč Code (string, default) +GET /api/entities/ProductionOrders/123?idType=number +GET /api/items/A00001 # UDF pole jsou součástí odpovědi +``` diff --git a/documentation/sap-business-one-service-layer.md b/documentation/sap-business-one-service-layer.md new file mode 100644 index 0000000..3ae9227 --- /dev/null +++ b/documentation/sap-business-one-service-layer.md @@ -0,0 +1,70 @@ +# SAP Business One Service Layer Connector (POLSTRIN) + +Connector komunikuje výhradně přes SAP Business One Service Layer `/b1s/v1` instalace POLSTRIN +(`https://ws.polstrin.cz:50000`). Specifika instalace jsou v [polstrin-specifika.md](polstrin-specifika.md). + +Hlavní vlastnosti: + +- login/logout přes Service Layer +- správa `B1SESSION` a `ROUTEID`, jedna sdílená session mezi requesty +- automatický re-login po expiraci session +- SAP credentials výhradně z environment variables (AppFactory secrets), ne z requestů +- volitelná ochrana `/api` rout hlavičkou `X-Api-Key` (secret `API_KEY`) +- OData parametry `$select`, `$filter`, `$top`, `$skip`, `$orderby` +- stránkování přes `odata.nextLink` a `@odata.nextLink` +- generický přístup k libovolnému entity setu přes `/api/entities/...` (POLSTRIN UDO tabulky `U_*`) +- retry pro dočasné chyby +- zod validace konfigurace a hlavních response tvarů + +Bezpečnostní pravidla: + +- konfigurace se čte výhradně z environment variables (`SAP_B1_*`, `API_KEY`) +- SAP credentials se nikdy nepřijímají v requestech, nelogují a nevrací v response +- hesla, cookies a session tokeny se nelogují +- testy nepoužívají reálný SAP přístup +- pro self-signed certifikáty je dostupné `rejectUnauthorized`, ale produkčně se doporučuje důvěryhodný certifikát + +## Detaily komunikace se Service Layer + +- **Login** – `POST /b1s/v1/Login` s `{ CompanyDB, UserName, Password, Language? }`. + `Language` se posílá jako celé číslo (Service Layer očekává `Edm.Int32`); nenumerická + hodnota se vynechá. Z odpovědi se čtou cookies `B1SESSION` a `ROUTEID` (s fallbackem na + `SessionId` v body) a `SessionTimeout` (minuty) pro výpočet expirace. +- **Autentizované requesty** – posílají `Cookie: B1SESSION=…; ROUTEID=…`. Při `401` + connector jednou provede re-login a request zopakuje; re-login nesnižuje retry budget, + takže funguje i při `SAP_B1_RETRY_COUNT=0`. +- **Logout** – `POST /b1s/v1/Logout` se posílá **s aktivní session cookie**, jinak by + Service Layer nevěděl, kterou session ukončit, a nechal by ji běžet až do timeoutu. +- **Verze a system info** – login response obsahuje `Version` (např. `1000230` = SAP B1 + 10.0 PL 23) a `SessionTimeout`; connector si je ukládá (`client.loginInfo`, přežije + logout) a HTTP endpoint `POST /api/session/login` je vrací v odpovědi. + `GET /api/system/info` navíc vrátí přehled instalace: service document (`GET /b1s/v1/` + → dostupné entity sety), `UserFieldsMD` (UDF), `UserTablesMD` (UDT), `UserObjectsMD` + (UDO) a `CompanyService_GetAdminInfo`. Sekce, na kterou SAP uživatel nemá práva, se + vrátí jako `{ "error": "…" }`, aby jedno chybějící oprávnění neshodilo celý přehled + (chyba je v odpovědi vidět, nejde o tiché selhání). +- **Stránkování** – následuje `odata.nextLink` i `@odata.nextLink`. U absolutních + nextLinků se zachová i query string (`$skip` apod.), takže `listAll` nezacyklí. + +## Reverse proxy a Swagger + +Aplikace běží za AppFactory proxy na `/apps/` (Caddy `handle_path` prefix před +předáním do containeru odstraní, takže container vidí routy bez prefixu a z requestu +veřejnou cestu nelze odvodit). Stejný přístup jako sousední služba `google-service`: + +- `/docs` se servíruje **přímo jako HTML** (`GET /docs`), bez `swagger-ui-express` a bez + statického middleware – proto **nevzniká žádný redirect `/docs` → `/docs/`**, který by za + proxy zahodil prefix `/apps/` (to byla příčina bílé stránky). +- Swagger UI assety se načítají z CDN (`unpkg.com/swagger-ui-dist@5`). +- Spec URL je `ROOT_PATH + /openapi.json`, takže odkazuje na veřejné + `/apps//openapi.json`. +- `servers[0].url` se nastaví z `ROOT_PATH` (jinak `/`), takže Swagger `Try it out` volá + `…/apps//api/`. + +| Kontrola | Lokálně (bez ROOT_PATH) | S `ROOT_PATH=/apps/sap-bo` | +|---|---|---| +| `GET /health` | 200 | 200 | +| `GET /docs` (Swagger UI) | 200, žádný redirect | 200, žádný redirect | +| `GET /openapi.json` | 200 | 200 | +| spec URL v `/docs` | `/openapi.json` | `/apps/sap-bo/openapi.json` | +| `servers[0].url` v OpenAPI | `/` | `/apps/sap-bo` | diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8c97a12 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2431 @@ +{ + "name": "polstrin-sap", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "polstrin-sap", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.9", + "dotenv": "^16.4.7", + "express": "^4.18.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.11.30", + "typescript": "^5.4.0", + "vitest": "^4.1.9" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "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.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "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/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "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/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/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/agent-base/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/agent-base/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/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/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.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "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/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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/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/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/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/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-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/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/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/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/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-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "dev": true, + "license": "MIT" + }, + "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-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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.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/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/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/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/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/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/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/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/https-proxy-agent/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/https-proxy-agent/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/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/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "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/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/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/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-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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/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/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/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/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "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/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/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/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/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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "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/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/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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", + "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.3.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/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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 index df10062..5a243a6 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,25 @@ { "name": "polstrin-sap", "version": "1.0.0", + "description": "SAP Business One Service Layer connector dedicated to the POLSTRIN DESIGN installation (credentials via environment secrets).", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { - "express": "^4.18.3" + "axios": "^1.7.9", + "dotenv": "^16.4.7", + "express": "^4.18.3", + "zod": "^3.24.1" }, "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.11.30", - "typescript": "^5.4.0" + "typescript": "^5.4.0", + "vitest": "^4.1.9" } } diff --git a/src/SapBusinessOneServiceLayer.ts b/src/SapBusinessOneServiceLayer.ts new file mode 100644 index 0000000..bcfb4a4 --- /dev/null +++ b/src/SapBusinessOneServiceLayer.ts @@ -0,0 +1,103 @@ +import { SapB1LoginInfo } from "./auth/SessionManager"; +import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client"; +import { SapB1Config } from "./config"; +import { BusinessPartnersResource } from "./resources/BusinessPartners"; +import { DeliveryNotesResource, InvoicesResource, OrdersResource, PurchaseOrdersResource } from "./resources/Documents"; +import { ItemsResource } from "./resources/Items"; +import { StockTransfersResource } from "./resources/StockTransfers"; +import { ODataResponse } from "./types/odata"; + +export interface SapB1SectionError { + error: string; +} + +export interface SapB1SystemInfo { + serviceLayer: SapB1LoginInfo; + entitySets: string[] | SapB1SectionError; + userFields: unknown[] | SapB1SectionError; + userTables: unknown[] | SapB1SectionError; + userObjects: unknown[] | SapB1SectionError; + adminInfo: unknown | SapB1SectionError; +} + +export class SapBusinessOneServiceLayer { + public readonly client: SapB1Client; + public readonly businessPartners: BusinessPartnersResource; + public readonly items: ItemsResource; + public readonly orders: OrdersResource; + public readonly invoices: InvoicesResource; + public readonly purchaseOrders: PurchaseOrdersResource; + public readonly deliveryNotes: DeliveryNotesResource; + public readonly stockTransfers: StockTransfersResource; + + public constructor(config: SapB1Config, options: SapB1ClientOptions = {}) { + this.client = new SapB1Client(config, options); + this.businessPartners = new BusinessPartnersResource(this.client); + this.items = new ItemsResource(this.client); + this.orders = new OrdersResource(this.client); + this.invoices = new InvoicesResource(this.client); + this.purchaseOrders = new PurchaseOrdersResource(this.client); + this.deliveryNotes = new DeliveryNotesResource(this.client); + this.stockTransfers = new StockTransfersResource(this.client); + } + + public login(): Promise { + return this.client.login(); + } + + public logout(): Promise { + return this.client.logout(); + } + + /** + * One-shot overview of the connected SAP Business One installation: Service Layer + * version (from the login response), available entity sets (service document) and + * the customization metadata (UserFieldsMD/UserTablesMD/UserObjectsMD) plus company + * admin info. Sections the SAP user cannot read come back as { error } so one missing + * permission does not fail the whole overview. + */ + public async getSystemInfo(): Promise { + await this.client.login(); + + const [entitySets, userFields, userTables, userObjects, adminInfo] = await Promise.all([ + this.loadSection(() => + this.client + .get>("/") + .then((document) => document.value.map((entitySet) => entitySet.name).sort()) + ), + this.loadSection(() => + this.client.getAll("UserFieldsMD", { + select: ["TableName", "FieldID", "Name", "Type", "SubType", "Description"] + }) + ), + this.loadSection(() => + this.client.getAll("UserTablesMD", { + select: ["TableName", "TableDescription", "TableType"] + }) + ), + this.loadSection(() => + this.client.getAll("UserObjectsMD", { + select: ["Code", "Name", "TableName", "ObjectType"] + }) + ), + this.loadSection(() => this.client.post("CompanyService_GetAdminInfo")) + ]); + + return { + serviceLayer: this.client.loginInfo ?? {}, + entitySets, + userFields, + userTables, + userObjects, + adminInfo + }; + } + + private async loadSection(load: () => Promise): Promise { + try { + return await load(); + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } + } +} diff --git a/src/auth/SessionManager.ts b/src/auth/SessionManager.ts new file mode 100644 index 0000000..6d09038 --- /dev/null +++ b/src/auth/SessionManager.ts @@ -0,0 +1,177 @@ +import "../client/axiosTypes"; +import { AxiosInstance } from "axios"; +import { z } from "zod"; +import { SapB1Config } from "../config"; +import { SapB1Error, toSapB1Error } from "../errors/SapB1Error"; +import { SapB1Logger, noopLogger, sanitizeLogMeta } from "../utils/logger"; + +const LoginResponseSchema = z.object({ + SessionId: z.string().optional(), + Version: z.string().optional(), + SessionTimeout: z.number().optional() +}); + +export interface SapB1Session { + b1session: string; + routeId?: string; + expiresAt?: number; +} + +export interface SapB1LoginInfo { + version?: string; + sessionTimeoutMinutes?: number; +} + +export class SessionManager { + private session?: SapB1Session; + private loginPromise?: Promise; + private lastLoginInfo?: SapB1LoginInfo; + + public constructor( + private readonly http: AxiosInstance, + private readonly config: SapB1Config, + private readonly logger: SapB1Logger = noopLogger + ) {} + + public get currentSession(): SapB1Session | undefined { + return this.session; + } + + // Survives logout on purpose: the login/logout roundtrip in the HTTP layer still + // needs to report which Service Layer version answered. + public get loginInfo(): SapB1LoginInfo | undefined { + return this.lastLoginInfo; + } + + public async getSession(forceRefresh = false): Promise { + if (!forceRefresh && this.session && !this.isExpired(this.session)) { + return this.session; + } + + if (!this.loginPromise) { + this.loginPromise = this.login().finally(() => { + this.loginPromise = undefined; + }); + } + + return this.loginPromise; + } + + public async login(): Promise { + this.logger.info("SAP B1 login request", sanitizeLogMeta({ baseUrl: this.config.baseUrl, companyDB: this.config.companyDB })); + + try { + const response = await this.http.post( + "/Login", + { + CompanyDB: this.config.companyDB, + UserName: this.config.username, + Password: this.config.password, + // Service Layer expects Language as an integer language code (Edm.Int32). + Language: this.normalizeLanguage(this.config.language) + }, + { skipAuth: true } + ); + + const loginData = LoginResponseSchema.parse(response.data); + const cookies = this.extractCookies(response.headers["set-cookie"]); + const b1session = cookies.B1SESSION || loginData.SessionId; + + if (!b1session) { + throw new SapB1Error({ message: "SAP B1 login did not return B1SESSION cookie or SessionId" }); + } + + const timeoutMinutes = typeof loginData.SessionTimeout === "number" ? loginData.SessionTimeout : 30; + this.lastLoginInfo = { + version: loginData.Version, + sessionTimeoutMinutes: loginData.SessionTimeout + }; + this.session = { + b1session, + routeId: cookies.ROUTEID, + expiresAt: Date.now() + Math.max(timeoutMinutes - 1, 1) * 60_000 + }; + + return this.session; + } catch (error) { + // Route login failures through the shared normalizer so the SAP status/code and + // rejection message survive into the response. That is what lets the caller tell a + // SAP-side rejection (wrong CompanyDB/user/password -> HTTP 401/400 with a SAP body) + // apart from a transport problem (wrong BaseUrl/port, TLS, timeout -> no HTTP status). + const sapError = toSapB1Error(error, "POST", "/Login", "SAP B1 login failed"); + this.logger.error( + "SAP B1 login failed", + sanitizeLogMeta({ + baseUrl: this.config.baseUrl, + companyDB: this.config.companyDB, + status: sapError.status, + code: sapError.code, + message: sapError.message + }) + ); + throw sapError; + } + } + + public async logout(): Promise { + if (!this.session) { + return; + } + + const cookieHeader = this.buildCookieHeader(this.session); + + try { + // Logout must carry the active session cookie, otherwise the Service Layer + // cannot identify which session to terminate and leaves it open until timeout. + await this.http.post("/Logout", undefined, { + skipAuth: true, + headers: { Cookie: cookieHeader } + }); + } finally { + this.session = undefined; + } + } + + public buildCookieHeader(session: SapB1Session): string { + const parts = [`B1SESSION=${session.b1session}`]; + + if (session.routeId) { + parts.push(`ROUTEID=${session.routeId}`); + } + + return parts.join("; "); + } + + public clearSession(): void { + this.session = undefined; + } + + private normalizeLanguage(language: string | undefined): number | undefined { + if (language === undefined || language === "") { + return undefined; + } + + const numeric = Number(language); + return Number.isInteger(numeric) ? numeric : undefined; + } + + private isExpired(session: SapB1Session): boolean { + return session.expiresAt !== undefined && session.expiresAt <= Date.now(); + } + + private extractCookies(setCookieHeader: string[] | string | undefined): Record { + const headers = Array.isArray(setCookieHeader) ? setCookieHeader : setCookieHeader ? [setCookieHeader] : []; + const cookies: Record = {}; + + for (const header of headers) { + const [nameValue] = header.split(";"); + const separatorIndex = nameValue.indexOf("="); + + if (separatorIndex > 0) { + cookies[nameValue.slice(0, separatorIndex)] = nameValue.slice(separatorIndex + 1); + } + } + + return cookies; + } +} diff --git a/src/client/SapB1Client.ts b/src/client/SapB1Client.ts new file mode 100644 index 0000000..a00909c --- /dev/null +++ b/src/client/SapB1Client.ts @@ -0,0 +1,152 @@ +import "./axiosTypes"; +import https from "node:https"; +import axios, { AxiosInstance, AxiosRequestConfig, Method } from "axios"; +import { z } from "zod"; +import { SapB1LoginInfo, SessionManager } from "../auth/SessionManager"; +import { SapB1Config, SapB1ConfigSchema } from "../config"; +import { SapB1Error, toSapB1Error } from "../errors/SapB1Error"; +import { ODataQuery, ODataResponse } from "../types/odata"; +import { buildODataParams, extractNextLink } from "./odata"; +import { SapB1Logger, noopLogger, sanitizeLogMeta } from "../utils/logger"; +import { sleep } from "../utils/sleep"; + +export interface SapB1ClientOptions { + logger?: SapB1Logger; + http?: AxiosInstance; +} + +export class SapB1Client { + private readonly http: AxiosInstance; + private readonly logger: SapB1Logger; + public readonly sessionManager: SessionManager; + public readonly config: SapB1Config; + + public constructor(config: SapB1Config, options: SapB1ClientOptions = {}) { + this.config = SapB1ConfigSchema.parse(config); + this.logger = options.logger ?? noopLogger; + this.http = + options.http ?? + axios.create({ + baseURL: this.buildServiceLayerBaseUrl(this.config.baseUrl), + timeout: this.config.timeout, + httpsAgent: new https.Agent({ rejectUnauthorized: this.config.rejectUnauthorized }) + }); + this.sessionManager = new SessionManager(this.http, this.config, this.logger); + } + + public login(): Promise { + return this.sessionManager.getSession(true); + } + + public get loginInfo(): SapB1LoginInfo | undefined { + return this.sessionManager.loginInfo; + } + + public logout(): Promise { + return this.sessionManager.logout(); + } + + public get(path: string, query?: ODataQuery, schema?: z.ZodType): Promise { + return this.request("GET", path, undefined, { params: buildODataParams(query), schema }); + } + + public post(path: string, data?: unknown, schema?: z.ZodType): Promise { + return this.request("POST", path, data, { schema }); + } + + public patch(path: string, data?: unknown, schema?: z.ZodType): Promise { + return this.request("PATCH", path, data, { schema }); + } + + public delete(path: string, schema?: z.ZodType): Promise { + return this.request("DELETE", path, undefined, { schema }); + } + + public async getAll(path: string, query: ODataQuery = {}, schema?: z.ZodType): Promise { + const items: T[] = []; + let nextPath: string | undefined = path; + let nextQuery: ODataQuery | undefined = query; + + while (nextPath) { + const response: ODataResponse = await this.get>(nextPath, nextQuery); + const value = schema ? z.array(schema).parse(response.value) : response.value; + items.push(...value); + const nextLink: string | undefined = extractNextLink(response); + nextPath = nextLink ? this.normalizeNextLink(nextLink) : undefined; + nextQuery = undefined; + } + + return items; + } + + private async request( + method: Method, + path: string, + data?: unknown, + options: AxiosRequestConfig & { schema?: z.ZodType } = {} + ): Promise { + const requestConfig: AxiosRequestConfig = { + ...options, + method, + url: path, + data, + headers: { + ...(options.headers || {}) + } + }; + + let reloginAttempted = false; + + for (let attempt = 0; attempt <= this.config.retryCount; attempt += 1) { + try { + if (!requestConfig.skipAuth) { + const session = await this.sessionManager.getSession(); + requestConfig.headers = { + ...(requestConfig.headers || {}), + Cookie: this.sessionManager.buildCookieHeader(session) + }; + } + + this.logger.debug("SAP B1 request", sanitizeLogMeta({ method, url: path, attempt })); + const response = await this.http.request(requestConfig); + return options.schema ? options.schema.parse(response.data) : (response.data as T); + } catch (error) { + const sapError = toSapB1Error(error, method, path); + const canRelogin = sapError.status === 401 && !requestConfig.skipAuth && !reloginAttempted; + + if (canRelogin) { + // A single re-login on session expiry must not eat into the retry budget, + // so step the counter back and retry the request with a fresh session. + reloginAttempted = true; + this.sessionManager.clearSession(); + await this.sessionManager.getSession(true); + attempt -= 1; + continue; + } + + if (!requestConfig.skipRetry && sapError.retryable && attempt < this.config.retryCount) { + await sleep(this.config.retryDelayMs * (attempt + 1)); + continue; + } + + throw sapError; + } + } + + throw new SapB1Error({ message: "SAP B1 request failed after retries", method, url: path }); + } + + private buildServiceLayerBaseUrl(baseUrl: string): string { + const trimmed = baseUrl.replace(/\/+$/, ""); + return trimmed.endsWith("/b1s/v1") ? trimmed : `${trimmed}/b1s/v1`; + } + + private normalizeNextLink(nextLink: string): string { + try { + const parsed = new URL(nextLink); + return parsed.pathname.replace(/^.*\/b1s\/v1\/?/, "") + parsed.search; + } catch { + return nextLink.replace(/^\/?b1s\/v1\/?/, ""); + } + } +} diff --git a/src/client/axiosTypes.ts b/src/client/axiosTypes.ts new file mode 100644 index 0000000..39d4e30 --- /dev/null +++ b/src/client/axiosTypes.ts @@ -0,0 +1,8 @@ +import "axios"; + +declare module "axios" { + export interface AxiosRequestConfig { + skipAuth?: boolean; + skipRetry?: boolean; + } +} diff --git a/src/client/odata.ts b/src/client/odata.ts new file mode 100644 index 0000000..6be8540 --- /dev/null +++ b/src/client/odata.ts @@ -0,0 +1,35 @@ +import { ODataQuery, ODataQuerySchema } from "../types/odata"; + +export function buildODataParams(query: ODataQuery = {}): Record { + const parsed = ODataQuerySchema.parse(query); + const params: Record = {}; + + if (parsed.select?.length) { + params.$select = parsed.select.join(","); + } + + if (parsed.filter) { + params.$filter = parsed.filter; + } + + if (parsed.top !== undefined) { + params.$top = parsed.top; + } + + if (parsed.skip !== undefined) { + params.$skip = parsed.skip; + } + + if (parsed.orderby) { + params.$orderby = Array.isArray(parsed.orderby) ? parsed.orderby.join(",") : parsed.orderby; + } + + return params; +} + +export function extractNextLink(response: { + "odata.nextLink"?: string; + "@odata.nextLink"?: string; +}): string | undefined { + return response["@odata.nextLink"] || response["odata.nextLink"]; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..b11dc8a --- /dev/null +++ b/src/config.ts @@ -0,0 +1,47 @@ +import "dotenv/config"; +import { z } from "zod"; + +const booleanFromEnv = z + .union([z.boolean(), z.string()]) + .optional() + .transform((value) => { + if (typeof value === "boolean") { + return value; + } + + if (value === undefined || value === "") { + return undefined; + } + + return ["1", "true", "yes", "on"].includes(value.toLowerCase()); + }); + +export const SapB1ConfigSchema = z.object({ + baseUrl: z.string().url(), + companyDB: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + language: z.string().min(1).optional(), + timeout: z.number().int().positive().default(30_000), + rejectUnauthorized: z.boolean().default(true), + retryCount: z.number().int().min(0).default(2), + retryDelayMs: z.number().int().min(0).default(250) +}); + +export type SapB1Config = z.infer; + +export function loadSapB1ConfigFromEnv(env: NodeJS.ProcessEnv = process.env): SapB1Config { + const rejectUnauthorized = booleanFromEnv.parse(env.SAP_B1_REJECT_UNAUTHORIZED); + + return SapB1ConfigSchema.parse({ + baseUrl: env.SAP_B1_BASE_URL, + companyDB: env.SAP_B1_COMPANY_DB, + username: env.SAP_B1_USERNAME, + password: env.SAP_B1_PASSWORD, + language: env.SAP_B1_LANGUAGE || undefined, + timeout: env.SAP_B1_TIMEOUT_MS ? Number(env.SAP_B1_TIMEOUT_MS) : undefined, + rejectUnauthorized, + retryCount: env.SAP_B1_RETRY_COUNT ? Number(env.SAP_B1_RETRY_COUNT) : undefined, + retryDelayMs: env.SAP_B1_RETRY_DELAY_MS ? Number(env.SAP_B1_RETRY_DELAY_MS) : undefined + }); +} diff --git a/src/errors/SapB1Error.ts b/src/errors/SapB1Error.ts new file mode 100644 index 0000000..dd63638 --- /dev/null +++ b/src/errors/SapB1Error.ts @@ -0,0 +1,68 @@ +import axios, { AxiosError, Method } from "axios"; + +export interface SapB1ErrorDetails { + status?: number; + code?: string; + message?: string; + method?: string; + url?: string; + retryable?: boolean; + cause?: unknown; +} + +export class SapB1Error extends Error { + public readonly status?: number; + public readonly code?: string; + public readonly method?: string; + public readonly url?: string; + public readonly retryable: boolean; + public override readonly cause?: unknown; + + public constructor(details: SapB1ErrorDetails) { + super(details.message || "SAP Business One Service Layer request failed"); + this.name = "SapB1Error"; + this.status = details.status; + this.code = details.code; + this.method = details.method; + this.url = details.url; + this.retryable = details.retryable ?? false; + this.cause = details.cause; + } +} + +// Shared normalizer so every SAP call (including login) surfaces the SAP status/code +// and message when the Service Layer actively rejected the request, and falls back to +// the transport-level message (ECONNREFUSED, timeout, TLS, ...) otherwise. This is what +// lets a caller tell "SAP rejected it" apart from "we never reached SAP". +export function toSapB1Error( + error: unknown, + method?: Method, + path?: string, + fallbackMessage = "SAP B1 request failed" +): SapB1Error { + if (error instanceof SapB1Error) { + return error; + } + + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError<{ error?: { code?: string; message?: { value?: string } | string } }>; + const status = axiosError.response?.status; + const sapError = axiosError.response?.data?.error; + const message = + typeof sapError?.message === "string" + ? sapError.message + : sapError?.message?.value || axiosError.message || fallbackMessage; + + return new SapB1Error({ + status, + code: sapError?.code, + message, + method, + url: path, + retryable: status === 408 || status === 429 || (status !== undefined && status >= 500), + cause: error + }); + } + + return new SapB1Error({ message: fallbackMessage, method, url: path, cause: error }); +} diff --git a/src/index.ts b/src/index.ts index fcfe32e..2a1d2c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,848 @@ -import express from "express"; +import express, { Request, Response } from "express"; +import { SapBusinessOneServiceLayer } from "./SapBusinessOneServiceLayer"; +import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client"; +import { buildODataParams, extractNextLink } from "./client/odata"; +import { loadSapB1ConfigFromEnv } from "./config"; +import { SapB1Error } from "./errors/SapB1Error"; +import { ODataQuery } from "./types/odata"; + +export { SessionManager, SapB1Session, SapB1LoginInfo } from "./auth/SessionManager"; +export { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client"; +export { buildODataParams, extractNextLink } from "./client/odata"; +export { loadSapB1ConfigFromEnv, SapB1Config, SapB1ConfigSchema } from "./config"; +export { SapB1Error } from "./errors/SapB1Error"; +export { SapBusinessOneServiceLayer, SapB1SystemInfo, SapB1SectionError } from "./SapBusinessOneServiceLayer"; +export * from "./types/entities"; +export * from "./types/odata"; + +type ResourceName = + | "businessPartners" + | "items" + | "orders" + | "invoices" + | "purchaseOrders" + | "deliveryNotes" + | "stockTransfers"; + +interface ResourceRoute { + slug: string; + tag: string; + property: ResourceName; + sapEntitySet: string; + idName: string; + idType: "string" | "number"; + deleteSupported: boolean; + sampleCreate: Record; + sampleUpdate: Record; +} const app = express(); +app.set("trust proxy", true); const port = Number(process.env.PORT || 3000); -const rootPath = process.env.ROOT_PATH || ""; +const rootPath = normalizeRootPath(process.env.ROOT_PATH || ""); +const serviceName = "SAP Business One (POLSTRIN)"; +const serviceId = "polstrin-sap"; +const companyName = "POLSTRIN DESIGN s.r.o."; + +function normalizeRootPath(value: string): string { + if (!value) { + return ""; + } + + const withLeadingSlash = value.startsWith("/") ? value : "/" + value; + return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash; +} + +function withRootPath(path: string): string { + return rootPath + path; +} + +const resourceRoutes: ResourceRoute[] = [ + { + slug: "business-partners", + tag: "BusinessPartners", + property: "businessPartners", + sapEntitySet: "BusinessPartners", + idName: "CardCode", + idType: "string", + deleteSupported: true, + sampleCreate: { CardCode: "C001", CardName: "Example customer", CardType: "cCustomer" }, + sampleUpdate: { CardName: "Updated customer name" } + }, + { + slug: "items", + tag: "Items", + property: "items", + sapEntitySet: "Items", + idName: "ItemCode", + idType: "string", + deleteSupported: true, + sampleCreate: { ItemCode: "A00001", ItemName: "Example item", InventoryItem: "tYES" }, + sampleUpdate: { ItemName: "Updated item name" } + }, + { + slug: "orders", + tag: "Orders", + property: "orders", + sapEntitySet: "Orders", + idName: "DocEntry", + idType: "number", + deleteSupported: false, + sampleCreate: { CardCode: "C001", DocumentLines: [{ ItemCode: "A00001", Quantity: 1 }] }, + sampleUpdate: { Comments: "Updated by connector" } + }, + { + slug: "invoices", + tag: "Invoices", + property: "invoices", + sapEntitySet: "Invoices", + idName: "DocEntry", + idType: "number", + deleteSupported: false, + sampleCreate: { CardCode: "C001", DocumentLines: [{ ItemCode: "A00001", Quantity: 1 }] }, + sampleUpdate: { Comments: "Updated by connector" } + }, + { + slug: "purchase-orders", + tag: "PurchaseOrders", + property: "purchaseOrders", + sapEntitySet: "PurchaseOrders", + idName: "DocEntry", + idType: "number", + deleteSupported: false, + sampleCreate: { CardCode: "V001", DocumentLines: [{ ItemCode: "A00001", Quantity: 1 }] }, + sampleUpdate: { Comments: "Updated by connector" } + }, + { + slug: "delivery-notes", + tag: "DeliveryNotes", + property: "deliveryNotes", + sapEntitySet: "DeliveryNotes", + idName: "DocEntry", + idType: "number", + deleteSupported: false, + sampleCreate: { CardCode: "C001", DocumentLines: [{ ItemCode: "A00001", Quantity: 1 }] }, + sampleUpdate: { Comments: "Updated by connector" } + }, + { + slug: "stock-transfers", + tag: "StockTransfers", + property: "stockTransfers", + sapEntitySet: "StockTransfers", + idName: "DocEntry", + idType: "number", + deleteSupported: false, + sampleCreate: { StockTransferLines: [{ ItemCode: "A00001", Quantity: 1, FromWarehouseCode: "01", WarehouseCode: "02" }] }, + sampleUpdate: { Comments: "Updated by connector" } + } +]; + +app.use(express.json({ limit: "2mb" })); + +// Optional shared-secret protection of the /api routes: when the API_KEY secret is +// configured through the AppFactory portal, every /api request must send the same +// value in the X-Api-Key header. Docs, health and metadata stay public. +app.use((req: Request, res: Response, next: () => void) => { + const apiKey = process.env.API_KEY; + + if (!apiKey || !req.path.startsWith("/api/")) { + next(); + return; + } + + if (req.get("X-Api-Key") === apiKey) { + next(); + return; + } + + res.status(401).json({ error: { type: "AuthError", message: "Missing or invalid X-Api-Key header" } }); +}); + +// SAP credentials for the POLSTRIN company database come exclusively from +// environment variables (AppFactory secrets, see AGENTS.md "Secrets v parametrech"), +// never from request headers or bodies. One shared instance keeps the Service Layer +// session alive between requests; SessionManager re-logins on expiry or 401. +let sharedSap: SapBusinessOneServiceLayer | undefined; + +function getSap(): SapBusinessOneServiceLayer { + if (!sharedSap) { + sharedSap = new SapBusinessOneServiceLayer(loadSapB1ConfigFromEnv()); + } + + return sharedSap; +} + +async function withSap(action: (sap: SapBusinessOneServiceLayer) => Promise): Promise { + return action(getSap()); +} + +function parseODataQuery(req: Request): ODataQuery { + const query: ODataQuery = {}; + const select = req.query.$select ?? req.query.select; + const filter = req.query.$filter ?? req.query.filter; + const top = req.query.$top ?? req.query.top; + const skip = req.query.$skip ?? req.query.skip; + const orderby = req.query.$orderby ?? req.query.orderby; + + if (typeof select === "string" && select.trim()) { + query.select = select.split(",").map((item) => item.trim()).filter(Boolean); + } + + if (typeof filter === "string" && filter.trim()) { + query.filter = filter; + } + + if (typeof top === "string" && top.trim()) { + query.top = Number(top); + } + + if (typeof skip === "string" && skip.trim()) { + query.skip = Number(skip); + } + + if (typeof orderby === "string" && orderby.trim()) { + query.orderby = orderby; + } + + return query; +} + +function parseId(route: ResourceRoute, rawId: string): string | number { + return route.idType === "number" ? Number(rawId) : rawId; +} + +function routeResource(sap: SapBusinessOneServiceLayer, route: ResourceRoute) { + return sap[route.property]; +} + +function asyncHandler(handler: (req: Request, res: Response) => Promise) { + return (req: Request, res: Response) => { + handler(req, res).catch((error) => sendError(res, error)); + }; +} + +function sendError(res: Response, error: unknown): void { + if (error instanceof SapB1Error) { + res.status(error.status || 502).json({ + error: { + type: error.name, + code: error.code, + message: error.message, + retryable: error.retryable + } + }); + return; + } + + if (error instanceof Error) { + res.status(400).json({ error: { type: error.name, message: error.message } }); + return; + } + + res.status(500).json({ error: { type: "Error", message: "Unexpected error" } }); +} + +function serviceMetadata() { + return { + name: serviceName, + service: serviceId, + company: companyName, + status: "ok", + resources: resourceRoutes.map((route) => route.slug) + }; +} + +// Generic access to any Service Layer entity set. Primarily meant for the POLSTRIN +// user-defined objects exposed as U_* entity sets (U_ADN_* assets add-on, U_DFX_* +// Intrastat, U_PVT_* Intrastat/PVT, U_VCZ_* Versino CZ localization and production, +// VYROBNI_PLAN, VYROBNI_DAVKA) that have no dedicated resource module. +const ENTITY_SET_PATTERN = /^[A-Za-z0-9_.]+$/; + +function requireEntitySet(req: Request): string { + const entitySet = req.params.entitySet; + + if (!ENTITY_SET_PATTERN.test(entitySet)) { + throw new Error(`Invalid entity set name: ${entitySet}`); + } + + return entitySet; +} + +function formatEntityKey(req: Request): string { + const raw = req.params.id; + + if (req.query.idType === "number") { + return String(Number(raw)); + } + + return `'${raw.replace(/'/g, "''")}'`; +} + +function addResourceEndpoints(prefix = "") { + app.post( + `${prefix}/api/session/login`, + asyncHandler(async (_req, res) => { + const serviceLayer = await withSap(async (sap) => { + await sap.login(); + return sap.client.loginInfo ?? {}; + }); + res.json({ status: "ok", ...serviceLayer }); + }) + ); + + app.post( + `${prefix}/api/session/logout`, + asyncHandler(async (_req, res) => { + await getSap().logout(); + res.status(204).send(); + }) + ); + + app.get( + `${prefix}/api/system/info`, + asyncHandler(async (_req, res) => { + const result = await withSap((sap) => sap.getSystemInfo()); + res.json(result); + }) + ); + + app.get( + `${prefix}/api/entities`, + asyncHandler(async (_req, res) => { + const document = await withSap((sap) => sap.client.get<{ value: Array<{ name: string }> }>("/")); + res.json({ value: document.value.map((entitySet) => entitySet.name).sort() }); + }) + ); + + app.get( + `${prefix}/api/entities/:entitySet`, + asyncHandler(async (req, res) => { + const entitySet = requireEntitySet(req); + const result = await withSap((sap) => sap.client.get(entitySet, parseODataQuery(req))); + res.json(result); + }) + ); + + app.get( + `${prefix}/api/entities/:entitySet/all`, + asyncHandler(async (req, res) => { + const entitySet = requireEntitySet(req); + const result = await withSap((sap) => sap.client.getAll(entitySet, parseODataQuery(req))); + res.json({ value: result }); + }) + ); + + app.get( + `${prefix}/api/entities/:entitySet/:id`, + asyncHandler(async (req, res) => { + const entitySet = requireEntitySet(req); + const result = await withSap((sap) => sap.client.get(`${entitySet}(${formatEntityKey(req)})`)); + res.json(result); + }) + ); + + app.post( + `${prefix}/api/entities/:entitySet`, + asyncHandler(async (req, res) => { + const entitySet = requireEntitySet(req); + const result = await withSap((sap) => sap.client.post(entitySet, req.body)); + res.status(201).json(result); + }) + ); + + app.patch( + `${prefix}/api/entities/:entitySet/:id`, + asyncHandler(async (req, res) => { + const entitySet = requireEntitySet(req); + await withSap((sap) => sap.client.patch(`${entitySet}(${formatEntityKey(req)})`, req.body)); + res.status(204).send(); + }) + ); + + app.delete( + `${prefix}/api/entities/:entitySet/:id`, + asyncHandler(async (req, res) => { + const entitySet = requireEntitySet(req); + await withSap((sap) => sap.client.delete(`${entitySet}(${formatEntityKey(req)})`)); + res.status(204).send(); + }) + ); + + for (const route of resourceRoutes) { + const base = `${prefix}/api/${route.slug}`; + + app.get( + base, + asyncHandler(async (req, res) => { + const result = await withSap((sap) => routeResource(sap, route).list(parseODataQuery(req))); + res.json(result); + }) + ); + + app.get( + `${base}/all`, + asyncHandler(async (req, res) => { + const result = await withSap((sap) => routeResource(sap, route).listAll(parseODataQuery(req))); + res.json({ value: result }); + }) + ); + + app.get( + `${base}/:id`, + asyncHandler(async (req, res) => { + const result = await withSap((sap) => routeResource(sap, route).get(parseId(route, req.params.id) as never)); + res.json(result); + }) + ); + + app.post( + base, + asyncHandler(async (req, res) => { + const result = await withSap((sap) => routeResource(sap, route).create(req.body as never)); + res.status(201).json(result); + }) + ); + + app.patch( + `${base}/:id`, + asyncHandler(async (req, res) => { + await withSap((sap) => routeResource(sap, route).update(parseId(route, req.params.id) as never, req.body as never)); + res.status(204).send(); + }) + ); + + if (route.deleteSupported) { + app.delete( + `${base}/:id`, + asyncHandler(async (req, res) => { + await withSap((sap) => routeResource(sap, route).delete(parseId(route, req.params.id) as never)); + res.status(204).send(); + }) + ); + } + } +} + +function commonParameters(route: ResourceRoute) { + return [ + { + name: "id", + in: "path", + required: true, + schema: { type: route.idType }, + description: `${route.idName} in SAP Business One ${route.sapEntitySet}.` + } + ]; +} + +function securityParameters() { + return [ + { + name: "X-Api-Key", + in: "header", + required: false, + schema: { type: "string", format: "password" }, + description: + "Shared service key. Required only when the API_KEY secret is configured for this service. SAP credentials themselves are internal secrets (environment variables) and are never sent in requests." + } + ]; +} + +function entitySetParameter() { + return { + name: "entitySet", + in: "path", + required: true, + schema: { type: "string", pattern: "^[A-Za-z0-9_.]+$" }, + description: "Service Layer entity set name, e.g. Quotations, ProductionOrders or U_VCZ_MKONF." + }; +} + +function entityIdParameters() { + return [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + description: "Entity key. Strings are quoted automatically; UDO tables typically use Code." + }, + { + name: "idType", + in: "query", + required: false, + schema: { type: "string", enum: ["string", "number"], default: "string" }, + description: "Set to number for numeric keys such as DocEntry or AbsEntry." + } + ]; +} + +function odataParameters() { + return [ + { name: "$select", in: "query", required: false, schema: { type: "string" }, description: "Comma separated OData field list." }, + { name: "$filter", in: "query", required: false, schema: { type: "string" }, description: "OData filter expression." }, + { name: "$top", in: "query", required: false, schema: { type: "integer", minimum: 1 }, description: "Maximum records to return." }, + { name: "$skip", in: "query", required: false, schema: { type: "integer", minimum: 0 }, description: "Records to skip." }, + { name: "$orderby", in: "query", required: false, schema: { type: "string" }, description: "OData order by expression." } + ]; +} + +function jsonBody(example: Record) { + return { + required: true, + content: { + "application/json": { + schema: { type: "object", additionalProperties: true }, + example + } + } + }; +} + +function openApiDocument(basePath = "") { + const paths: Record = { + "/": { + get: { + tags: ["Service"], + summary: "Service metadata", + responses: { + "200": { description: "Service status" } + } + } + }, + "/health": { + get: { + tags: ["Service"], + summary: "Health check", + responses: { + "200": { description: "Service is ready to accept traffic" } + } + } + }, + "/openapi.json": { + get: { + tags: ["Service"], + summary: "OpenAPI schema", + responses: { + "200": { description: "OpenAPI document" } + } + } + }, + "/api/session/login": { + post: { + tags: ["Session"], + summary: "Open SAP Business One Service Layer session", + description: + "Verifies connectivity to the POLSTRIN Service Layer. SAP credentials are configured as internal secrets (environment variables) via the AppFactory portal and are never accepted in requests, logged, or returned.", + parameters: securityParameters(), + responses: { + "200": { + description: "Session is available; returns SAP Service Layer version and session timeout", + content: { + "application/json": { + example: { status: "ok", version: "1000230", sessionTimeoutMinutes: 30 } + } + } + }, + "502": { description: "SAP login failed" } + } + } + }, + "/api/session/logout": { + post: { + tags: ["Session"], + summary: "Logout from SAP Business One Service Layer", + parameters: securityParameters(), + responses: { + "204": { description: "Session closed or was not active" } + } + } + }, + "/api/system/info": { + get: { + tags: ["System"], + summary: "SAP Business One system and customization overview", + description: + "Logs in and returns the Service Layer version (from the login response), the list of available entity sets (service document), user-defined fields (UserFieldsMD), user-defined tables (UserTablesMD), user-defined objects (UserObjectsMD) and company admin info (CompanyService_GetAdminInfo). Sections the SAP user cannot read are returned as { \"error\": \"...\" } instead of failing the whole request.", + parameters: securityParameters(), + responses: { + "200": { + description: "System overview", + content: { + "application/json": { + example: { + serviceLayer: { version: "1000230", sessionTimeoutMinutes: 30 }, + entitySets: ["BusinessPartners", "Items", "Orders"], + userFields: [{ TableName: "OCRD", FieldID: 0, Name: "MyField", Type: "db_Alpha", Description: "Custom field" }], + userTables: [{ TableName: "MY_TABLE", TableDescription: "Custom table", TableType: "bott_NoObject" }], + userObjects: [{ Code: "MY_UDO", Name: "My UDO", TableName: "MY_TABLE", ObjectType: "boud_Document" }], + adminInfo: { error: "No permission (example of a section the SAP user cannot read)" } + } + } + } + }, + "502": { description: "SAP login failed" } + } + } + }, + "/api/entities": { + get: { + tags: ["Entities"], + summary: "List all entity sets available in the POLSTRIN installation", + description: "Returns the Service Layer service document, including the U_* entity sets created by the installed add-ons.", + parameters: securityParameters(), + responses: { + "200": { description: "Sorted list of entity set names" } + } + } + }, + "/api/entities/{entitySet}": { + get: { + tags: ["Entities"], + summary: "List records of any entity set", + description: + "Generic passthrough to GET /b1s/v1/{entitySet}. Meant mainly for the POLSTRIN user-defined objects: U_ADN_* (evidence majetku), U_DFX_* (Intrastat), U_PVT_* (Intrastat/PVT), U_VCZ_* (Versino CZ lokalizace a vyroba), VYROBNI_PLAN, VYROBNI_DAVKA.", + parameters: [...securityParameters(), entitySetParameter(), ...odataParameters()], + responses: { + "200": { description: "OData response with value array and optional nextLink" } + } + }, + post: { + tags: ["Entities"], + summary: "Create a record in any entity set", + parameters: [...securityParameters(), entitySetParameter()], + requestBody: jsonBody({ Code: "001", Name: "Example", U_SomeField: "value" }), + responses: { + "201": { description: "Created SAP object" } + } + } + }, + "/api/entities/{entitySet}/all": { + get: { + tags: ["Entities"], + summary: "Load all pages of any entity set", + parameters: [...securityParameters(), entitySetParameter(), ...odataParameters()], + responses: { + "200": { description: "Object with value array containing all loaded records" } + } + } + }, + "/api/entities/{entitySet}/{id}": { + get: { + tags: ["Entities"], + summary: "Get a record by key", + parameters: [...securityParameters(), entitySetParameter(), ...entityIdParameters()], + responses: { + "200": { description: "SAP object" }, + "404": { description: "Object was not found by SAP Service Layer" } + } + }, + patch: { + tags: ["Entities"], + summary: "Update a record by key", + parameters: [...securityParameters(), entitySetParameter(), ...entityIdParameters()], + requestBody: jsonBody({ Name: "Updated value" }), + responses: { + "204": { description: "Updated" } + } + }, + delete: { + tags: ["Entities"], + summary: "Delete a record by key", + parameters: [...securityParameters(), entitySetParameter(), ...entityIdParameters()], + responses: { + "204": { description: "Deleted" } + } + } + } + }; + + for (const route of resourceRoutes) { + const base = `/api/${route.slug}`; + paths[base] = { + get: { + tags: [route.tag], + summary: `List ${route.sapEntitySet}`, + description: `Maps to SAP Service Layer GET /b1s/v1/${route.sapEntitySet}.`, + parameters: [...securityParameters(), ...odataParameters()], + responses: { + "200": { description: "OData response with value array and optional nextLink" } + } + }, + post: { + tags: [route.tag], + summary: `Create ${route.sapEntitySet} record`, + description: `Maps to SAP Service Layer POST /b1s/v1/${route.sapEntitySet}.`, + parameters: securityParameters(), + requestBody: jsonBody(route.sampleCreate), + responses: { + "201": { description: "Created SAP object" } + } + } + }; + + paths[`${base}/all`] = { + get: { + tags: [route.tag], + summary: `Load all ${route.sapEntitySet} pages`, + description: "Follows odata.nextLink/@odata.nextLink until all pages are loaded. Use carefully for large datasets.", + parameters: [...securityParameters(), ...odataParameters()], + responses: { + "200": { description: "Object with value array containing all loaded records" } + } + } + }; + + const itemOperations: Record = { + get: { + tags: [route.tag], + summary: `Get ${route.sapEntitySet} by ${route.idName}`, + description: `Maps to SAP Service Layer GET /b1s/v1/${route.sapEntitySet}().`, + parameters: [...securityParameters(), ...commonParameters(route)], + responses: { + "200": { description: "SAP object" }, + "404": { description: "Object was not found by SAP Service Layer" } + } + }, + patch: { + tags: [route.tag], + summary: `Update ${route.sapEntitySet} by ${route.idName}`, + description: `Maps to SAP Service Layer PATCH /b1s/v1/${route.sapEntitySet}().`, + parameters: [...securityParameters(), ...commonParameters(route)], + requestBody: jsonBody(route.sampleUpdate), + responses: { + "204": { description: "Updated" } + } + } + }; + + if (route.deleteSupported) { + itemOperations.delete = { + tags: [route.tag], + summary: `Delete ${route.sapEntitySet} by ${route.idName}`, + description: `Maps to SAP Service Layer DELETE /b1s/v1/${route.sapEntitySet}(). Availability still depends on SAP object state and permissions.`, + parameters: [...securityParameters(), ...commonParameters(route)], + responses: { + "204": { description: "Deleted" } + } + }; + } + + paths[`${base}/{id}`] = itemOperations; + } + + return { + openapi: "3.0.3", + info: { + title: "SAP Business One connector - POLSTRIN DESIGN s.r.o.", + version: "1.0.0", + description: + "HTTP API over the POLSTRIN SAP Business One Service Layer (10.0, version 1000310). SAP credentials are internal secrets configured as environment variables via the AppFactory portal; requests optionally authenticate with the X-Api-Key header when the API_KEY secret is set. Custom add-ons in this installation expose U_ADN_*, U_DFX_*, U_PVT_* and U_VCZ_* entity sets and UDFs with the same prefixes on standard objects - use /api/entities and $select to work with them." + }, + servers: [{ url: basePath || "/" }], + security: [{ ApiKey: [] }], + tags: [ + { name: "Service" }, + { name: "Session" }, + { name: "System", description: "SAP version and customization overview" }, + { name: "Entities", description: "Generic access to any entity set incl. POLSTRIN UDO tables (U_*)" }, + ...resourceRoutes.map((route) => ({ name: route.tag, description: `SAP Service Layer ${route.sapEntitySet}` })) + ], + paths, + components: { + securitySchemes: { + ApiKey: { type: "apiKey", in: "header", name: "X-Api-Key" } + }, + schemas: { + ODataResponse: { + type: "object", + properties: { + value: { type: "array", items: { type: "object", additionalProperties: true } }, + "odata.nextLink": { type: "string" }, + "@odata.nextLink": { type: "string" } + } + }, + SapB1Error: { + type: "object", + properties: { + error: { + type: "object", + properties: { + type: { type: "string" }, + code: { type: "string" }, + message: { type: "string" }, + retryable: { type: "boolean" } + } + } + } + } + } + } + }; +} + +function renderDocsHtml(): string { + // Same approach as the sibling google-service: serve the Swagger UI page directly at /docs + // (no static-file middleware, so no /docs -> /docs/ redirect that would drop the proxy prefix), + // load the UI assets from CDN, and point the spec URL at ROOT_PATH + /openapi.json so it resolves + // to the public /apps//openapi.json behind the AppFactory reverse proxy. + const specUrl = withRootPath("/openapi.json"); + return ` + + + + + POLSTRIN SAP Business One connector API + + + + +
+ + + +`; +} app.get("/", (_req, res) => { - res.json({ - name: "Polstrin SAP BO1", - service: "polstrin-sap", - status: "ok" - }); + res.json(serviceMetadata()); }); app.get("/health", (_req, res) => { res.json({ status: "ok" }); }); -if (rootPath) { - app.get(rootPath, (_req, res) => { - res.json({ - name: "Polstrin SAP BO1", - service: "polstrin-sap", - status: "ok" - }); - }); +app.get("/docs", (_req, res) => { + res.type("html").send(renderDocsHtml()); +}); - app.get(rootPath + "/health", (_req, res) => { - res.json({ status: "ok" }); +// servers[0].url advertises the public prefix (ROOT_PATH) so Swagger UI "Try it out" targets +// {prefix}/api/..., not the host root. +app.get("/openapi.json", (_req, res) => { + res.json(openApiDocument(rootPath)); +}); + +addResourceEndpoints(); + +if (require.main === module) { + app.listen(port, "0.0.0.0", () => { + console.log(serviceId + " listening on port " + port); }); } -app.listen(port, "0.0.0.0", () => { - console.log("polstrin-sap listening on port " + port); -}); +export { app }; diff --git a/src/resources/BusinessPartners.ts b/src/resources/BusinessPartners.ts new file mode 100644 index 0000000..59484e6 --- /dev/null +++ b/src/resources/BusinessPartners.ts @@ -0,0 +1,15 @@ +import { SapB1Client } from "../client/SapB1Client"; +import { BusinessPartner, BusinessPartnerCreateDto, BusinessPartnerUpdateDto } from "../types/entities"; +import { BusinessPartnerSchema } from "../types/schemas"; +import { Resource } from "./Resource"; + +export class BusinessPartnersResource extends Resource< + BusinessPartner, + BusinessPartnerCreateDto, + BusinessPartnerUpdateDto, + string +> { + public constructor(client: SapB1Client) { + super(client, "BusinessPartners", BusinessPartnerSchema); + } +} diff --git a/src/resources/Documents.ts b/src/resources/Documents.ts new file mode 100644 index 0000000..11c36ab --- /dev/null +++ b/src/resources/Documents.ts @@ -0,0 +1,38 @@ +import { SapB1Client } from "../client/SapB1Client"; +import { MarketingDocument, MarketingDocumentCreateDto, MarketingDocumentUpdateDto } from "../types/entities"; +import { MarketingDocumentSchema } from "../types/schemas"; +import { Resource } from "./Resource"; + +export class OrdersResource extends Resource { + public constructor(client: SapB1Client) { + super(client, "Orders", MarketingDocumentSchema, false); + } +} + +export class InvoicesResource extends Resource { + public constructor(client: SapB1Client) { + super(client, "Invoices", MarketingDocumentSchema, false); + } +} + +export class PurchaseOrdersResource extends Resource< + MarketingDocument, + MarketingDocumentCreateDto, + MarketingDocumentUpdateDto, + number +> { + public constructor(client: SapB1Client) { + super(client, "PurchaseOrders", MarketingDocumentSchema, false); + } +} + +export class DeliveryNotesResource extends Resource< + MarketingDocument, + MarketingDocumentCreateDto, + MarketingDocumentUpdateDto, + number +> { + public constructor(client: SapB1Client) { + super(client, "DeliveryNotes", MarketingDocumentSchema, false); + } +} diff --git a/src/resources/Items.ts b/src/resources/Items.ts new file mode 100644 index 0000000..bd50f75 --- /dev/null +++ b/src/resources/Items.ts @@ -0,0 +1,10 @@ +import { SapB1Client } from "../client/SapB1Client"; +import { Item, ItemCreateDto, ItemUpdateDto } from "../types/entities"; +import { ItemSchema } from "../types/schemas"; +import { Resource } from "./Resource"; + +export class ItemsResource extends Resource { + public constructor(client: SapB1Client) { + super(client, "Items", ItemSchema); + } +} diff --git a/src/resources/Resource.ts b/src/resources/Resource.ts new file mode 100644 index 0000000..ff6f228 --- /dev/null +++ b/src/resources/Resource.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; +import { SapB1Client } from "../client/SapB1Client"; +import { ODataQuery, ODataResponse } from "../types/odata"; + +export class Resource { + public constructor( + protected readonly client: SapB1Client, + protected readonly path: string, + protected readonly schema: z.ZodType, + private readonly supportsDelete = true + ) {} + + public list(query: ODataQuery = {}): Promise> { + return this.client.get>(this.path, query); + } + + public listAll(query: ODataQuery = {}): Promise { + return this.client.getAll(this.path, query, this.schema); + } + + public get(id: TKey): Promise { + return this.client.get(`${this.path}(${this.formatKey(id)})`, undefined, this.schema); + } + + public create(data: TCreate): Promise { + return this.client.post(this.path, data, this.schema); + } + + public update(id: TKey, data: TUpdate): Promise { + return this.client.patch(`${this.path}(${this.formatKey(id)})`, data); + } + + public delete(id: TKey): Promise { + if (!this.supportsDelete) { + throw new Error(`${this.path} does not support delete through this connector`); + } + + return this.client.delete(`${this.path}(${this.formatKey(id)})`); + } + + protected formatKey(id: TKey): string { + return typeof id === "number" ? String(id) : `'${String(id).replace(/'/g, "''")}'`; + } +} diff --git a/src/resources/StockTransfers.ts b/src/resources/StockTransfers.ts new file mode 100644 index 0000000..f4c66ca --- /dev/null +++ b/src/resources/StockTransfers.ts @@ -0,0 +1,10 @@ +import { SapB1Client } from "../client/SapB1Client"; +import { StockTransfer, StockTransferCreateDto, StockTransferUpdateDto } from "../types/entities"; +import { StockTransferSchema } from "../types/schemas"; +import { Resource } from "./Resource"; + +export class StockTransfersResource extends Resource { + public constructor(client: SapB1Client) { + super(client, "StockTransfers", StockTransferSchema, false); + } +} diff --git a/src/types/entities.ts b/src/types/entities.ts new file mode 100644 index 0000000..69771ba --- /dev/null +++ b/src/types/entities.ts @@ -0,0 +1,73 @@ +export interface BusinessPartner { + CardCode: string; + CardName?: string; + CardType?: "cCustomer" | "cSupplier" | "cLid"; + Phone1?: string; + EmailAddress?: string; + Currency?: string; + [key: string]: unknown; +} + +export type BusinessPartnerCreateDto = Pick & Partial; +export type BusinessPartnerUpdateDto = Partial>; + +export interface Item { + ItemCode: string; + ItemName?: string; + ItemsGroupCode?: number; + InventoryItem?: "tYES" | "tNO"; + SalesItem?: "tYES" | "tNO"; + PurchaseItem?: "tYES" | "tNO"; + [key: string]: unknown; +} + +export type ItemCreateDto = Pick & Partial; +export type ItemUpdateDto = Partial>; + +export interface DocumentLine { + ItemCode?: string; + Quantity?: number; + UnitPrice?: number; + WarehouseCode?: string; + [key: string]: unknown; +} + +export interface MarketingDocument { + DocEntry: number; + DocNum?: number; + CardCode?: string; + DocDate?: string; + DocDueDate?: string; + TaxDate?: string; + DocumentLines?: DocumentLine[]; + [key: string]: unknown; +} + +export type MarketingDocumentCreateDto = Omit, "DocEntry" | "DocNum"> & { + CardCode: string; + DocumentLines?: DocumentLine[]; +}; +export type MarketingDocumentUpdateDto = Partial>; + +export interface StockTransferLine { + ItemCode?: string; + Quantity?: number; + WarehouseCode?: string; + FromWarehouseCode?: string; + [key: string]: unknown; +} + +export interface StockTransfer { + DocEntry: number; + DocNum?: number; + DocDate?: string; + FromWarehouse?: string; + ToWarehouse?: string; + StockTransferLines?: StockTransferLine[]; + [key: string]: unknown; +} + +export type StockTransferCreateDto = Omit, "DocEntry" | "DocNum"> & { + StockTransferLines?: StockTransferLine[]; +}; +export type StockTransferUpdateDto = Partial>; diff --git a/src/types/odata.ts b/src/types/odata.ts new file mode 100644 index 0000000..0bbe353 --- /dev/null +++ b/src/types/odata.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +export interface ODataQuery { + select?: string[]; + filter?: string; + top?: number; + skip?: number; + orderby?: string | string[]; +} + +export interface ODataResponse { + value: T[]; + "odata.metadata"?: string; + "odata.nextLink"?: string; + "@odata.context"?: string; + "@odata.nextLink"?: string; +} + +export const ODataQuerySchema = z.object({ + select: z.array(z.string().min(1)).optional(), + filter: z.string().min(1).optional(), + top: z.number().int().positive().optional(), + skip: z.number().int().min(0).optional(), + orderby: z.union([z.string().min(1), z.array(z.string().min(1))]).optional() +}); + +export const ODataResponseSchema = (itemSchema: T) => + z.object({ + value: z.array(itemSchema), + "odata.metadata": z.string().optional(), + "odata.nextLink": z.string().optional(), + "@odata.context": z.string().optional(), + "@odata.nextLink": z.string().optional() + }); diff --git a/src/types/schemas.ts b/src/types/schemas.ts new file mode 100644 index 0000000..2e913ed --- /dev/null +++ b/src/types/schemas.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const UnknownRecordSchema = z.object({}).catchall(z.unknown()); + +export const BusinessPartnerSchema = UnknownRecordSchema.extend({ + CardCode: z.string() +}); + +export const ItemSchema = UnknownRecordSchema.extend({ + ItemCode: z.string() +}); + +export const MarketingDocumentSchema = UnknownRecordSchema.extend({ + DocEntry: z.number() +}); + +export const StockTransferSchema = UnknownRecordSchema.extend({ + DocEntry: z.number() +}); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..c064fc4 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,27 @@ +export interface SapB1Logger { + debug(message: string, meta?: Record): void; + info(message: string, meta?: Record): void; + warn(message: string, meta?: Record): void; + error(message: string, meta?: Record): void; +} + +export const noopLogger: SapB1Logger = { + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined +}; + +export function sanitizeLogMeta(meta: Record): Record { + const forbidden = new Set(["password", "b1session", "routeid", "cookie", "authorization"]); + + return Object.fromEntries( + Object.entries(meta).map(([key, value]) => { + if (forbidden.has(key.toLowerCase())) { + return [key, "[redacted]"]; + } + + return [key, value]; + }) + ); +} diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 0000000..421bda0 --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1,3 @@ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/tests/auth.test.ts b/tests/auth.test.ts new file mode 100644 index 0000000..ec24aee --- /dev/null +++ b/tests/auth.test.ts @@ -0,0 +1,123 @@ +import { AxiosError } from "axios"; +import { describe, expect, it, vi } from "vitest"; +import { SessionManager } from "../src/auth/SessionManager"; +import { SapB1Config } from "../src/config"; +import { SapB1Error } from "../src/errors/SapB1Error"; + +const config: SapB1Config = { + baseUrl: "https://sap.example.local:50000", + companyDB: "SBODEMOUS", + username: "manager", + password: "secret", + timeout: 30_000, + rejectUnauthorized: true, + retryCount: 2, + retryDelayMs: 0 +}; + +describe("SessionManager", () => { + it("logs in and stores B1SESSION and ROUTEID cookies", async () => { + const http = { + post: vi.fn().mockResolvedValue({ + data: { SessionId: "fallback", SessionTimeout: 30 }, + headers: { + "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1", "ROUTEID=.node1; Path=/b1s/v1"] + } + }) + }; + + const sessionManager = new SessionManager(http as never, config); + const session = await sessionManager.getSession(); + + expect(http.post).toHaveBeenCalledWith( + "/Login", + { + CompanyDB: "SBODEMOUS", + UserName: "manager", + Password: "secret", + Language: undefined + }, + { skipAuth: true } + ); + expect(session.b1session).toBe("abc123"); + expect(session.routeId).toBe(".node1"); + expect(sessionManager.buildCookieHeader(session)).toBe("B1SESSION=abc123; ROUTEID=.node1"); + }); + + it("sends the language as an integer code to the Login action", async () => { + const http = { + post: vi.fn().mockResolvedValue({ + data: { SessionId: "abc123", SessionTimeout: 30 }, + headers: {} + }) + }; + + const sessionManager = new SessionManager(http as never, { ...config, language: "3" }); + await sessionManager.getSession(); + + expect(http.post).toHaveBeenCalledWith( + "/Login", + { + CompanyDB: "SBODEMOUS", + UserName: "manager", + Password: "secret", + Language: 3 + }, + { skipAuth: true } + ); + }); + + it("surfaces the SAP status, code and message when SAP rejects the login", async () => { + const rejection = new AxiosError("Request failed with status code 401", "ERR_BAD_REQUEST"); + rejection.response = { + status: 401, + statusText: "Unauthorized", + headers: {}, + config: {} as never, + data: { error: { code: "301", message: { value: "Invalid company database" } } } + }; + const http = { post: vi.fn().mockRejectedValue(rejection) }; + + const sessionManager = new SessionManager(http as never, config); + const error = await sessionManager.getSession().catch((e) => e); + + expect(error).toBeInstanceOf(SapB1Error); + expect(error.status).toBe(401); + expect(error.code).toBe("301"); + expect(error.message).toBe("Invalid company database"); + }); + + it("keeps the generic login message for a transport error with no HTTP response", async () => { + const transportError = new AxiosError("connect ECONNREFUSED 10.0.0.1:50000", "ECONNREFUSED"); + const http = { post: vi.fn().mockRejectedValue(transportError) }; + + const sessionManager = new SessionManager(http as never, config); + const error = await sessionManager.getSession().catch((e) => e); + + expect(error).toBeInstanceOf(SapB1Error); + expect(error.status).toBeUndefined(); + expect(error.message).toBe("connect ECONNREFUSED 10.0.0.1:50000"); + }); + + it("logs out and clears the current session", async () => { + const http = { + post: vi + .fn() + .mockResolvedValueOnce({ + data: { SessionId: "abc123" }, + headers: {} + }) + .mockResolvedValueOnce({ data: {}, headers: {} }) + }; + + const sessionManager = new SessionManager(http as never, config); + await sessionManager.getSession(); + await sessionManager.logout(); + + expect(http.post).toHaveBeenLastCalledWith("/Logout", undefined, { + skipAuth: true, + headers: { Cookie: "B1SESSION=abc123" } + }); + expect(sessionManager.currentSession).toBeUndefined(); + }); +}); diff --git a/tests/client.test.ts b/tests/client.test.ts new file mode 100644 index 0000000..0efe2f1 --- /dev/null +++ b/tests/client.test.ts @@ -0,0 +1,154 @@ +import axios, { AxiosAdapter, AxiosResponse } from "axios"; +import { describe, expect, it } from "vitest"; +import { SapB1Client } from "../src/client/SapB1Client"; +import { SapB1Config } from "../src/config"; + +const config: SapB1Config = { + baseUrl: "https://sap.example.local:50000", + companyDB: "SBODEMOUS", + username: "manager", + password: "secret", + timeout: 30_000, + rejectUnauthorized: true, + retryCount: 1, + retryDelayMs: 0 +}; + +function response(data: unknown, requestConfig: Parameters[0], status = 200): AxiosResponse { + return { + data, + status, + statusText: "OK", + headers: {}, + config: requestConfig + }; +} + +describe("SapB1Client", () => { + it("adds session cookies to resource requests", async () => { + const urls: string[] = []; + const cookies: unknown[] = []; + const http = axios.create({ + adapter: async (requestConfig) => { + urls.push(String(requestConfig.url)); + cookies.push(requestConfig.headers?.Cookie); + + if (requestConfig.url === "/Login") { + return { + ...response({ SessionId: "abc123", SessionTimeout: 30 }, requestConfig), + headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1", "ROUTEID=.node1; Path=/b1s/v1"] } + }; + } + + return response({ value: [{ CardCode: "C001" }] }, requestConfig); + } + }); + + const client = new SapB1Client(config, { http }); + const result = await client.get("BusinessPartners", { top: 1 }); + + expect(result).toEqual({ value: [{ CardCode: "C001" }] }); + expect(urls).toEqual(["/Login", "BusinessPartners"]); + expect(cookies[1]).toBe("B1SESSION=abc123; ROUTEID=.node1"); + }); + + it("re-logins once after a 401 response", async () => { + let resourceCalls = 0; + let loginCalls = 0; + const http = axios.create({ + adapter: async (requestConfig) => { + if (requestConfig.url === "/Login") { + loginCalls += 1; + return { + ...response({ SessionId: `session-${loginCalls}`, SessionTimeout: 30 }, requestConfig), + headers: { "set-cookie": [`B1SESSION=session-${loginCalls}; Path=/b1s/v1`] } + }; + } + + resourceCalls += 1; + + if (resourceCalls === 1) { + return Promise.reject({ + isAxiosError: true, + message: "Unauthorized", + response: { status: 401, data: { error: { message: { value: "Session expired" } } } } + }); + } + + return response({ value: [] }, requestConfig); + } + }); + + const client = new SapB1Client(config, { http }); + await expect(client.get("Items")).resolves.toEqual({ value: [] }); + expect(loginCalls).toBe(2); + expect(resourceCalls).toBe(2); + }); + + it("re-logins after a 401 even when retries are disabled", async () => { + let resourceCalls = 0; + let loginCalls = 0; + const http = axios.create({ + adapter: async (requestConfig) => { + if (requestConfig.url === "/Login") { + loginCalls += 1; + return { + ...response({ SessionId: `session-${loginCalls}`, SessionTimeout: 30 }, requestConfig), + headers: { "set-cookie": [`B1SESSION=session-${loginCalls}; Path=/b1s/v1`] } + }; + } + + resourceCalls += 1; + + if (resourceCalls === 1) { + return Promise.reject({ + isAxiosError: true, + message: "Unauthorized", + response: { status: 401, data: { error: { message: { value: "Session expired" } } } } + }); + } + + return response({ value: [] }, requestConfig); + } + }); + + const client = new SapB1Client({ ...config, retryCount: 0 }, { http }); + await expect(client.get("Items")).resolves.toEqual({ value: [] }); + expect(loginCalls).toBe(2); + expect(resourceCalls).toBe(2); + }); + + it("follows an absolute nextLink while preserving its query string", async () => { + const requestedUrls: string[] = []; + const http = axios.create({ + adapter: async (requestConfig) => { + if (requestConfig.url === "/Login") { + return { + ...response({ SessionId: "abc123", SessionTimeout: 30 }, requestConfig), + headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1"] } + }; + } + + requestedUrls.push(String(requestConfig.url)); + + if (requestConfig.url === "Items") { + return response( + { + value: [{ ItemCode: "A1" }], + "odata.nextLink": "https://sap.example.local:50000/b1s/v1/Items?$skip=20" + }, + requestConfig + ); + } + + return response({ value: [{ ItemCode: "A2" }] }, requestConfig); + } + }); + + const client = new SapB1Client(config, { http }); + const all = await client.getAll<{ ItemCode: string }>("Items"); + + expect(all).toEqual([{ ItemCode: "A1" }, { ItemCode: "A2" }]); + expect(requestedUrls).toEqual(["Items", "Items?$skip=20"]); + }); +}); diff --git a/tests/odata.test.ts b/tests/odata.test.ts new file mode 100644 index 0000000..4925f79 --- /dev/null +++ b/tests/odata.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { buildODataParams, extractNextLink } from "../src/client/odata"; + +describe("OData helpers", () => { + it("builds SAP Service Layer OData query parameters", () => { + expect( + buildODataParams({ + select: ["CardCode", "CardName"], + filter: "CardType eq 'cCustomer'", + top: 25, + skip: 50, + orderby: ["CardName asc", "CardCode desc"] + }) + ).toEqual({ + $select: "CardCode,CardName", + $filter: "CardType eq 'cCustomer'", + $top: 25, + $skip: 50, + $orderby: "CardName asc,CardCode desc" + }); + }); + + it("supports old and new nextLink field names", () => { + expect(extractNextLink({ "@odata.nextLink": "Items?$skip=20" })).toBe("Items?$skip=20"); + expect(extractNextLink({ "odata.nextLink": "Items?$skip=40" })).toBe("Items?$skip=40"); + }); +}); diff --git a/tests/resources.test.ts b/tests/resources.test.ts new file mode 100644 index 0000000..6aa8a71 --- /dev/null +++ b/tests/resources.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it, vi } from "vitest"; +import { BusinessPartnersResource } from "../src/resources/BusinessPartners"; +import { OrdersResource } from "../src/resources/Documents"; + +describe("resources", () => { + it("formats string keys for SAP Service Layer entity URLs", async () => { + const client = { + get: vi.fn().mockResolvedValue({ CardCode: "C'001" }) + }; + const resource = new BusinessPartnersResource(client as never); + + await resource.get("C'001"); + + expect(client.get).toHaveBeenCalledWith("BusinessPartners('C''001')", undefined, expect.anything()); + }); + + it("blocks delete for marketing documents by default", async () => { + const resource = new OrdersResource({} as never); + + await expect(() => resource.delete(1)).toThrow("Orders does not support delete through this connector"); + }); +}); diff --git a/tests/systemInfo.test.ts b/tests/systemInfo.test.ts new file mode 100644 index 0000000..bb12352 --- /dev/null +++ b/tests/systemInfo.test.ts @@ -0,0 +1,106 @@ +import axios, { AxiosAdapter, AxiosResponse } from "axios"; +import { describe, expect, it } from "vitest"; +import { SapBusinessOneServiceLayer } from "../src/SapBusinessOneServiceLayer"; +import { SapB1Config } from "../src/config"; + +const config: SapB1Config = { + baseUrl: "https://sap.example.local:50000", + companyDB: "SBODEMOUS", + username: "manager", + password: "secret", + timeout: 30_000, + rejectUnauthorized: true, + retryCount: 0, + retryDelayMs: 0 +}; + +function response(data: unknown, requestConfig: Parameters[0], status = 200): AxiosResponse { + return { + data, + status, + statusText: "OK", + headers: {}, + config: requestConfig + }; +} + +describe("getSystemInfo", () => { + it("collects version, entity sets and customization metadata, isolating section errors", async () => { + const http = axios.create({ + adapter: async (requestConfig) => { + const url = String(requestConfig.url); + + if (url === "/Login") { + return { + ...response({ SessionId: "abc123", Version: "1000230", SessionTimeout: 30 }, requestConfig), + headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1"] } + }; + } + + if (url === "/Logout") { + return response({}, requestConfig); + } + + if (url === "/") { + return response( + { value: [{ name: "Items", url: "Items" }, { name: "BusinessPartners", url: "BusinessPartners" }] }, + requestConfig + ); + } + + if (url === "UserFieldsMD") { + return response({ value: [{ TableName: "OCRD", FieldID: 0, Name: "MyField", Type: "db_Alpha" }] }, requestConfig); + } + + if (url === "UserTablesMD") { + return response({ value: [{ TableName: "MY_TABLE", TableDescription: "Custom table" }] }, requestConfig); + } + + if (url === "UserObjectsMD") { + return response({ value: [] }, requestConfig); + } + + if (url === "CompanyService_GetAdminInfo") { + return Promise.reject({ + isAxiosError: true, + message: "Forbidden", + response: { status: 403, data: { error: { message: { value: "No permission" } } } } + }); + } + + throw new Error(`Unexpected request URL: ${url}`); + } + }); + + const sap = new SapBusinessOneServiceLayer(config, { http }); + const info = await sap.getSystemInfo(); + + expect(info.serviceLayer).toEqual({ version: "1000230", sessionTimeoutMinutes: 30 }); + expect(info.entitySets).toEqual(["BusinessPartners", "Items"]); + expect(info.userFields).toEqual([{ TableName: "OCRD", FieldID: 0, Name: "MyField", Type: "db_Alpha" }]); + expect(info.userTables).toEqual([{ TableName: "MY_TABLE", TableDescription: "Custom table" }]); + expect(info.userObjects).toEqual([]); + expect(info.adminInfo).toEqual({ error: "No permission" }); + }); + + it("keeps login info available after logout so the login endpoint can report the version", async () => { + const http = axios.create({ + adapter: async (requestConfig) => { + if (requestConfig.url === "/Login") { + return { + ...response({ SessionId: "abc123", Version: "1000230", SessionTimeout: 30 }, requestConfig), + headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1"] } + }; + } + + return response({}, requestConfig); + } + }); + + const sap = new SapBusinessOneServiceLayer(config, { http }); + await sap.login(); + await sap.logout(); + + expect(sap.client.loginInfo).toEqual({ version: "1000230", sessionTimeoutMinutes: 30 }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 81a634e..05dd830 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,11 @@ "outDir": "dist", "rootDir": "src", "strict": true, - "esModuleInterop": true - } + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["tests", "dist", "node_modules"] }