From 16c935bb185bdd9e23e4bd336ade0ea68f5f9c50 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 25 Aug 2026 16:02:27 +0200 Subject: [PATCH] Scaffold ESM TypeScript rewrite as @larvit/smpp --- .editorconfig | 13 + .gitignore | 3 + AGENTS.md | 107 ++++ CLAUDE.md | 1 + LICENSE | 22 + README.md | 208 +++++++ compose.yaml | 10 + eslint.config.js | 26 + package-lock.json | 1281 +++++++++++++++++++++++++++++++++++++++++ package.json | 59 ++ src/defs/constants.ts | 114 ++++ src/result.ts | 9 + test/result.test.ts | 29 + todo.md | 225 ++++++++ tsconfig.build.json | 9 + tsconfig.json | 27 + 16 files changed, 2143 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 compose.yaml create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/defs/constants.ts create mode 100644 src/result.ts create mode 100644 test/result.test.ts create mode 100644 todo.md create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a4fb2bc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yaml,yml}] +indent_size = 2 +indent_style = space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28ebeef --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.claude +dist +node_modules diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..27eee63 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,107 @@ +# AGENTS.md + +Guidance for LLM agents working in this repository. Human-facing documentation lives in +[README.md](README.md); the remaining work is tracked in [todo.md](todo.md). + +## What this is + +A ground-up TypeScript rewrite of `larvitsmpp` 0.4.0, published as `@larvit/smpp` 1.0.0. The branch +started from an orphan commit — no history from 0.4.0 is carried over. The 0.4.0 source is still +readable on the `master` branch of the same repository and is the reference for protocol behaviour, +not for structure or style. + +The library's value is its very small API. Do not grow the public surface without being asked. + +## Hard rules + +These are not preferences. Breaking one is a defect. + +1. **Nothing throws.** Every fallible function returns (or resolves to) a DTO carrying an optional + `err`. No `throw`, no rejected promises, no exceptions as control flow. Node APIs that throw are + wrapped at the boundary and converted into a result. Programmer errors (bad arguments) are + results too. +2. **Log messages are static strings.** Every dynamic value goes into `@larvit/log` metadata. Never + interpolate, never concatenate. + - GOOD: `log.debug('sendSms() - splitting message', { parts: msgs.length, to });` + - BANNED: `log.debug('sendSms() - splitting into ' + msgs.length + ' parts');` +3. **No `error` event.** Node makes an unhandled `error` event throw, which would break rule 1. + Sessions emit `sessionError`, servers emit `serverError`. +4. **No casts, no non-null assertions.** `as`, `as unknown as` and `!` are all banned. Parse untyped + input once through a type guard at the boundary; everything past it is typed. + +## Architecture + +``` +src/ + index.ts Public surface. Named exports only, no default export. + client.ts client() -> { err, session } + server.ts server() -> { err, server }, server owns the listener + close() + session.ts Session: framing, sequence numbers, the send window, events + sms.ts The live handle emitted as the 'sms' event (sendResp/sendDlr) + message.ts Encoding detection, splitting, bit counting, SMPP date formatting + pdu.ts pduToObj / objToPdu / pduReturn — synchronous, result-returning + defs/ + commands.ts The 33 commands, their ids and ordered parameter lists + constants.ts consts + constsById (TON, NPI, ENCODING, MESSAGE_STATE, …) + encodings.ts GSM 03.38, LATIN1, UCS2 and detection + errors.ts errors + errorsById (ESME_*) + filters.ts Per-field encode/decode hooks (time, message, callback_num, …) + tlvs.ts TLV definitions, tlvsById + types.ts Wire types: int8/int16/int32/string/cstring/buffer/arrays +``` + +Dependency direction is one way: `defs` knows nothing above it, `pdu` uses `defs`, `session` uses +`pdu`, and `client`/`server` use `session`. Nothing reaches back up. + +**Parameter order is wire order.** The key order inside `cmds.*.params` is the order the fields are +written to and read from the buffer. Never sort those alphabetically — the alphabetical-ordering +convention applies everywhere else, but here it corrupts every PDU. + +## Toolchain + +Run everything through the container; never invoke node or npm on the host. + +```bash +docker compose run --rm node npm install +docker compose run --rm node npm test +docker compose run --rm node npm run build +``` + +- Tests are `.ts` and run directly under Node's type stripping — no build step in the dev loop. +- Source imports use `.ts` extensions; `rewriteRelativeImportExtensions` emits `.js` into `dist`. +- `erasableSyntaxOnly` is on, so no enums, no namespaces, no parameter properties. Use `as const` + objects plus union types. +- The published floor is Node 18, but the dev container runs Node 24 (type stripping needs it). CI + compiles the tests and runs them on 18/20/22/24, so the floor is verified rather than asserted. +- `typescript` is pinned to the 6.x line because `typescript-eslint` peer-requires `<6.1.0`. Move to + TypeScript 7 once that constraint lifts. + +## Defects found in 0.4.0 + +Confirmed by reading the 0.4.0 source. The rewrite fixes all of them; each needs a regression test +naming the behaviour, and the wire-affecting ones are cross-checked against a reference +implementation (see todo.md). + +| Defect | 0.4.0 behaviour | +| --- | --- | +| LATIN1 never decodes | `decodeMsg` loops `consts.ENCODING` without breaking, so `data_coding` 0x03 lands on the alias `ISO_8859_1`, which has no decoder, and silently falls back to ASCII | +| Oversized segments | `splitMsg` emits 152 GSM chars + 6-byte UDH = 158 octets, over the 140-octet limit; UCS2 gets 66 chars where 67 fit | +| DLR month off by one | `smppDate()` uses `getMonth()` (0-based) without `+1`, so January renders as `00` | +| Non-standard DLR status | Receipts emit `stat:UNDELIVERABLE`; the spec's field is 7 characters (`UNDELIV`) | +| Flash destroys UCS2 | `flash: true` overwrites `data_coding` with 0x10, discarding the UCS2 alphabet, which needs 0x18 | +| Shared concat reference | The concatenation reference counter is a module-level global shared by every session in the process | +| `send()` never times out | Each call adds a listener keyed on the sequence number; a peer that never answers leaks it and the promise never settles | +| `tls: true` is not TLS | Constructs a bare `new tls.Socket()` with no handshake instead of `tls.connect()` | +| Alphanumeric sender TON | `sendSms` hardcodes `source_addr_ton` to 1 (international) even for alphanumeric senders, which require TON 5 | +| Text-only DLRs refused | `deliver_sm` without both `message_state` and `receipted_message_id` TLVs is rejected with `ESME_RINVTLVSTREAM`, so Kannel-style receipts are unusable | +| Unbounded reassembly | Incomplete long-SMS groups are capped by nothing and swept only when other traffic arrives, after 24 hours | +| Dead DLR aggregation | `longSmsDlrs` is allocated to merge per-segment receipts and then never used | + +## Conventions + +- Hard tabs. Alphabetical ordering for keys, imports and lists unless order is logic-significant + (see the wire-order note above). +- Comments are the exception, not the default — see the root `CLAUDE.md` rules. Do not write file + preambles or restate what the code says. +- Test data uses real randomised UUID v7 values, never `aaaa-0000` placeholders. +- `message_id` values the library generates are UUID v7. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8b7cbf4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +See [AGENTS.md](AGENTS.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ea5363 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Larv IT AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba03071 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# @larvit/smpp + +A simplified implementation of the SMPP protocol, in TypeScript. ESM only, types included. + +Successor to [larvitsmpp](https://www.npmjs.com/package/larvitsmpp) 0.4.0. The API is the same shape +it has always been — connect, send an SMS, listen for delivery reports — with callbacks replaced by +promises and the rough edges taken off. + +> **Status: not released.** This branch is a rewrite in progress and nothing here is published yet. +> The API below is the agreed design and is being implemented against it; see +> [todo.md](todo.md) for what is done and what is not. Use `larvitsmpp` 0.4.0 until 1.0.0 ships. + +## Requirements + +Node 18 or later. + +## Install + +```bash +npm install @larvit/smpp +``` + +## Client + +The simplest possible client — connects to localhost:2775 with no credentials and sends a message: + +```javascript +import { client } from '@larvit/smpp'; + +const { err, session } = await client(); +if (err) throw err; + +await session.sendSms({ + from: '46701113311', + message: 'Hello world', + to: '46709771337', +}); + +await session.unbind(); +``` + +With connection parameters, a delivery report and logging: + +```javascript +import { Log } from '@larvit/log'; +import { client } from '@larvit/smpp'; + +const log = new Log('debug'); + +const { err, session } = await client({ + host: 'smpp.somewhere.com', + log, + password: 'bar', + port: 2775, + username: 'foo', +}); +if (err) throw err; + +session.on('dlr', dlr => { + // dlr.smsId, dlr.statusMsg, dlr.statusId +}); + +const { err: sendErr, smsIds } = await session.sendSms({ + dlr: true, + from: '46701113311', + message: '«baff»', + to: '46709771337', +}); +``` + +## Server + +The simplest possible server — no authentication, listening on port 2775: + +```javascript +import { server } from '@larvit/smpp'; + +const { err, server: smpp } = await server(); +if (err) throw err; + +smpp.on('session', session => { + session.on('sms', async sms => { + // sms.from, sms.to, sms.message, sms.dlr + }); +}); +``` + +With authentication and delivery reports: + +```javascript +import { server } from '@larvit/smpp'; + +const { err, server: smpp } = await server({ + // Replace with your own auth. Returning an object attaches it to session.userData. + authenticate: async ({ password, systemId }) => { + if (systemId !== 'foo' || password !== 'bar') return false; + + return { userData: { userId: 123 } }; + }, +}); +if (err) throw err; + +smpp.on('session', session => { + session.on('sms', async sms => { + // Responding is part of the protocol, not optional. + // Defaults to ESME_ROK; see the SMPP spec for the other status codes. + await sms.sendResp(); + + if (sms.dlr) { + await sms.sendDlr(); // same as sms.sendDlr('DELIVERED') + } + }); +}); + +await smpp.close(); +``` + +`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`, +`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. + +## Errors + +Nothing in this library throws. Every fallible call returns a result carrying an optional `err`, so +failures are handled in one place instead of two: + +```javascript +const { err, session } = await client({ host: 'smpp.somewhere.com' }); +if (err) return; + +const { err: sendErr, smsIds } = await session.sendSms({ from, message, to }); +``` + +Runtime failures on a live connection arrive as `sessionError` and `serverError` events. They are +deliberately not called `error`: Node turns an unhandled `error` event into a thrown exception, which +is exactly what this library promises not to do. + +## Sessions + +### Events + +| Event | Fires when | +| --- | --- | +| `sms` | An SMS arrives. Carries `sendResp()` and `sendDlr()`. | +| `dlr` | A delivery report arrives, one per segment. | +| `messageDlr` | Every segment of a multipart message has been reported on. | +| `close` | The socket closed. | +| `reconnected` | The client re-bound after a drop (only with `reconnect` configured). | +| `sessionError` | Something failed on a live session. | +| `data` | Raw bytes arrived on the socket. | +| `incomingPdu` | A complete PDU arrived, as a buffer. | +| `incomingPduObj` | The same PDU, parsed into an object. | + +### Methods + +`sendSms()`, `send()`, `sendReturn()`, `unbind()` and `close()`. `send()` reaches any of the 33 SMPP +commands the codec knows, not just the four the session handles natively. + +## Migrating from larvitsmpp 0.4.0 + +- **The package is now `@larvit/smpp`** and is ESM only. `require()` no longer works. +- **Callbacks are gone.** `client`, `server`, `sendSms`, `sendResp` and `sendDlr` are all promises + resolving to a result object with an optional `err`. Nothing rejects. +- **`server()` resolves once, when it is listening**, and gives you a handle with `close()` and a + `session` event. It no longer calls your callback once per incoming connection. +- **`checkuserpass` is now `authenticate`**, takes `{ password, systemId }` and returns `false` or + `{ userData }`. +- **Renamed options:** `enqLinkTiming` → `enquireLinkInterval`, server `timeout` → `idleTimeout`. +- **`larvitsmpp.utils` is gone.** Its contents are named exports: `bitCount`, `decodeMessage`, + `encodeMessage`, `objToPdu`, `pduReturn`, `pduToObj`, `smppDate`, `splitMessage`. The PDU codec is + synchronous and returns `{ err, pduObj }` / `{ err, buffer }`. +- **`pduObj.isResp()` is now the standalone `isResp(pduObj)`.** +- **The `error` event is `sessionError`** (and `serverError` on the server handle). +- **`log`** takes a [`@larvit/log`](https://www.npmjs.com/package/@larvit/log) instance instead of a + `larvitutils` one, and is silent by default. + +### Behaviour that changed on the wire + +0.4.0 had a number of protocol defects. Fixing them changes the bytes it puts on the wire, so if you +have worked around any of these, remove the workaround: + +- Multipart segments were 158 octets, over the 140-octet limit. They are now correctly sized. +- LATIN1 (`data_coding` 0x03) was silently decoded as ASCII, corrupting the message. +- Delivery receipt dates were a month off, and the status field read `UNDELIVERABLE` where the spec + defines the 7-character `UNDELIV`. +- `flash: true` discarded UCS2, mangling flash messages containing non-GSM characters. +- The multipart reference counter was shared by every session in the process. +- `tls: true` never performed a handshake, so the connection was not actually encrypted. +- Alphanumeric senders were sent with TON 1 (international) instead of TON 5. +- Delivery receipts carrying only the standard receipt text, with no TLVs — what Kannel and several + other SMSCs send — were rejected outright. They are now parsed. + +## Development + +Everything runs in the container; nothing is installed on the host. + +```bash +docker compose run --rm node npm install +docker compose run --rm node npm test # lint, typecheck and tests +docker compose run --rm node npm run build +``` + +Tests are TypeScript and run directly under Node's type stripping, so there is no build step in the +development loop. CI additionally compiles and runs them on Node 18, 20, 22 and 24 to verify the +supported range. + +## License + +MIT diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f37bde7 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,10 @@ +services: + node: + image: node:24.18.0-bookworm-slim + init: true + user: "1000:1000" + working_dir: /app + volumes: + - .:/app + environment: + NPM_CONFIG_CACHE: /tmp/npm-cache diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6e53bef --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,26 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist/'] }, + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + '@typescript-eslint/no-non-null-assertion': 'error', + 'no-console': 'error', + }, + }, + { + files: ['eslint.config.js'], + extends: [tseslint.configs.disableTypeChecked], + }, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b63b9d6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1281 @@ +{ + "name": "larvitsmpp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "larvitsmpp", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@larvit/log": "2.3.0" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@types/node": "22.20.1", + "eslint": "10.9.1", + "typescript": "6.0.3", + "typescript-eslint": "8.68.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@larvit/log": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@larvit/log/-/log-2.3.0.tgz", + "integrity": "sha512-gLRMDWrFnoMGdAr9NXMXRKOUeceU6Qd/CW0c6qDO+DLFT1xWrqO2ubsHyPqPzlZHVUO3w3Bk7kOWo6C2Ykn4ZQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", + "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", + "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", + "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", + "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", + "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", + "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", + "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", + "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", + "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", + "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", + "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c65b83 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "@larvit/smpp", + "version": "1.0.0", + "description": "Simplified SMPP implementation", + "keywords": [ + "esm", + "pdu", + "sms", + "smpp", + "typescript" + ], + "homepage": "https://github.com/larvit/larvitsmpp", + "bugs": { + "url": "https://github.com/larvit/larvitsmpp/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/larvit/larvitsmpp.git" + }, + "license": "MIT", + "author": { + "name": "Mikael 'Lilleman' Göransson", + "email": "lilleman@larvit.se", + "url": "http://github.com/larvit/larvitsmpp" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "build": "tsc --project tsconfig.build.json", + "lint": "eslint . && tsc --noEmit", + "prepack": "npm run build", + "test": "npm run lint && node --test 'test/**/*.test.ts'" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@types/node": "22.20.1", + "eslint": "10.9.1", + "typescript": "6.0.3", + "typescript-eslint": "8.68.0" + }, + "dependencies": { + "@larvit/log": "2.3.0" + } +} diff --git a/src/defs/constants.ts b/src/defs/constants.ts new file mode 100644 index 0000000..5b7f2f6 --- /dev/null +++ b/src/defs/constants.ts @@ -0,0 +1,114 @@ +export const consts = { + BROADCAST_AREA_FORMAT: { + ALIAS: 0x00, + ELLIPSOID_ARC: 0x01, + NAME: 0x00, + POLYGON: 0x02, + }, + BROADCAST_FREQUENCY_INTERVAL: { + DAYS: 0x0B, + HOURS: 0x0A, + MAX_POSSIBLE: 0x00, + MINUTES: 0x09, + MONTHS: 0x0D, + SECONDS: 0x08, + WEEKS: 0x0C, + YEARS: 0x0E, + }, + ENCODING: { + ASCII: 0x01, + BINARY: 0x04, + CYRILLIC: 0x06, + EXTENDED_KANJI_JIS: 0x0D, + FLASH: 0x10, + HEBREW: 0x07, + IA5: 0x01, + ISO_2022_JP: 0x0A, + ISO_8859_1: 0x03, + ISO_8859_5: 0x06, + ISO_8859_8: 0x07, + JIS: 0x05, + KS_C_5601: 0x0E, + LATIN1: 0x03, + PICTOGRAM: 0x09, + UCS2: 0x08, + X_0208_1990: 0x05, + X_0212_1990: 0x0D, + }, + ESM_CLASS: { + CONVERSATION_ABORT: 0x18, + DATAGRAM: 0x01, + DELIVERY_ACKNOWLEDGEMENT: 0x08, + FORWARD: 0x02, + INTERMEDIATE_DELIVERY: 0x20, + MC_DELIVERY_RECEIPT: 0x04, + SET_REPLY_PATH: 0x80, + STORE_FORWARD: 0x03, + UDH_INDICATOR: 0x40, + USER_ACKNOWLEDGEMENT: 0x10, + }, + MESSAGE_STATE: { + ACCEPTED: 6, + DELETED: 4, + DELIVERED: 2, + ENROUTE: 1, + EXPIRED: 3, + REJECTED: 8, + SCHEDULED: 0, + SKIPPED: 9, + UNDELIVERABLE: 5, + UNKNOWN: 7, + }, + NETWORK: { + CDMA: 0x03, + GENERIC: 0x00, + GSM: 0x01, + TDMA: 0x02, + }, + NPI: { + DATA: 0x03, + ERMES: 0x0A, + INTERNET: 0x0E, + IP: 0x0E, + ISDN: 0x01, + LAND_MOBILE: 0x06, + NATIONAL: 0x08, + PRIVATE: 0x09, + TELEX: 0x04, + UNKNOWN: 0x00, + WAP: 0x12, + }, + REGISTERED_DELIVERY: { + DELIVERY_ACKNOWLEDGEMENT: 0x04, + FAILURE: 0x02, + FINAL: 0x01, + INTERMEDIATE: 0x10, + SUCCESS: 0x03, + USER_ACKNOWLEDGEMENT: 0x08, + }, + TON: { + ABBREVIATED: 0x06, + ALPHANUMERIC: 0x05, + INTERNATIONAL: 0x01, + NATIONAL: 0x02, + NETWORK_SPECIFIC: 0x03, + SUBSCRIBER_NUMBER: 0x04, + UNKNOWN: 0x00, + }, +} as const; + +export type ConstGroup = keyof typeof consts; +export type MessageState = keyof typeof consts.MESSAGE_STATE; + +// Aliased values (NPI.IP === NPI.INTERNET === 0x0E) resolve to whichever name sorts last. +export const constsById: Record> = {}; + +for (const [groupName, group] of Object.entries(consts)) { + const byId: Record = {}; + + for (const [name, value] of Object.entries(group)) { + byId[value] = name; + } + + constsById[groupName] = byId; +} diff --git a/src/result.ts b/src/result.ts new file mode 100644 index 0000000..9570760 --- /dev/null +++ b/src/result.ts @@ -0,0 +1,9 @@ +/** + * Either a failure carrying err, or a success carrying T. Both branches declare every key, so + * callers can destructure once and let `if (err)` narrow what is left. + */ +export type Result = + | ({ err: Error } & Partial>) + | ({ err?: undefined } & T); + +export type VoidResult = { err?: Error }; diff --git a/test/result.test.ts b/test/result.test.ts new file mode 100644 index 0000000..2353956 --- /dev/null +++ b/test/result.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { Result } from '../src/result.ts'; + +function parse(input: string): Result<{ value: number }> { + const value = Number(input); + + if (Number.isNaN(value)) return { err: new Error('not a number') }; + + return { value }; +} + +test('destructuring a result narrows on err', () => { + const { err, value } = parse('42'); + + if (err) { + assert.fail('should have parsed'); + } + + // Fails to compile if narrowing does not remove undefined from value. + assert.equal(value.toFixed(0), '42'); +}); + +test('a failed result carries err and nothing else', () => { + const { err, value } = parse('nope'); + + assert.ok(err instanceof Error); + assert.equal(value, undefined); +}); diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..9f42706 --- /dev/null +++ b/todo.md @@ -0,0 +1,225 @@ +# todo.md + +Remaining work for the `@larvit/smpp` 1.0.0 rewrite. Read [AGENTS.md](AGENTS.md) first — the hard +rules there constrain every item below. + +## How to resume + +```bash +docker compose run --rm node npm install +docker compose run --rm node npm test +``` + +Work top to bottom. Each task states what "done" means. Tests come before implementation: write the +failing test for the behaviour, then implement until it passes. The 0.4.0 implementation is on the +`master` branch of this repository and is the reference for protocol behaviour — read it, do not +copy its structure. + +## The agreed API + +Settled with the maintainer before implementation started. Do not change any of it without asking; +it is the contract the tasks below implement. + +```ts +import { client, server, consts, defs, errors, objToPdu, pduToObj } from '@larvit/smpp'; + +// --- client --------------------------------------------------------------- +const { err, session } = await client({ + addrNpi: 0, // optional, bind field + addrTon: 0, // optional, bind field + addressRange: '', // optional, bind field + bindType: 'transceiver', // 'transceiver' | 'transmitter' | 'receiver' + enquireLinkInterval: 20000, + host: 'localhost', + interfaceVersion: 0x50, // optional, bind field + log, // @larvit/log LogInt, silent by default + maxOutstanding: 10, // in-flight requests before sends queue + password: 'pass', + port: 2775, + reconnect: { maxDelay: 30000, minDelay: 1000 }, // optional, opt-in + responseTimeout: 30000, + signal, // optional AbortSignal + systemType: '', // optional, bind field + tls: false, // boolean or tls.ConnectionOptions + username: 'user', +}); + +const { err, pduObjs, smsIds } = await session.sendSms({ + dlr: false, + encoding: 'UCS2', // optional, overrides auto-detection + flash: false, + from: 'MyBrand', // alphanumeric -> TON 5, digits -> TON 1 + message: 'Hello world', + scheduleDeliveryTime: undefined, // optional + to: '46709771337', + validityPeriod: undefined, // optional +}, { signal }); // optional per-call AbortSignal + +await session.unbind(); +await session.close(); + +// --- server --------------------------------------------------------------- +const { err, server: smpp } = await server({ + authenticate: async ({ password, systemId }) => { + if (systemId !== 'foo' || password !== 'bar') return false; + + return { userData: { userId: 123 } }; // attached to session.userData + }, + idleTimeout: 40000, + log, + maxReassembly: 1000, + port: 2775, + reassemblyTimeout: 300000, + signal, + tls: false, +}); + +smpp.port; // resolved port, useful when 0 was requested +await smpp.close(); + +smpp.on('session', session => { + session.on('sms', async sms => { + const { err } = await sms.sendResp(); // defaults to ESME_ROK + if (sms.dlr) await sms.sendDlr('DELIVERED'); // defaults to DELIVERED + }); +}); +``` + +### Events + +| Emitter | Event | Payload | +| --- | --- | --- | +| server | `session` | `Session` | +| server | `serverError` | `Error` | +| session | `sms` | `Sms` (live handle: `sendResp()`, `sendDlr()`) | +| session | `dlr` | one per segment — `{ doneDate?, errorCode?, smsId, statusId, statusMsg }` | +| session | `messageDlr` | merged once all segments of a message are accounted for — adds `segments` and the worst status across them | +| session | `sessionError` | `Error` | +| session | `close` | — | +| session | `reconnected` | — (only when `reconnect` is configured) | +| session | `data` | `Buffer` | +| session | `incomingPdu` | `Buffer` | +| session | `incomingPduObj` | `PduObject` | + +### Rules the API follows + +- **Never throws.** Everything fallible resolves to `{ err?, … }`. See AGENTS.md rule 1. +- **Named exports only.** No default export. `defs` stays as a grouped export alongside the + individual tables (`cmds`, `consts`, `encodings`, `errors`, `tlvs`, `types`, and the `*ById` maps). +- **`utils` is gone.** Its contents are named exports: `bitCount`, `decodeMessage`, `encodeMessage`, + `objToPdu`, `pduReturn`, `pduToObj`, `smppDate`, `splitMessage`. +- **The PDU codec is synchronous** and returns `{ err?, pduObj? }` / `{ err?, buffer? }`. +- **Low-level surface stays public**, including `session.sock`, `session.send()` and + `session.sendReturn()` — that is how the other 29 commands are reached. +- `pduObj.isResp()` is now the standalone export `isResp(pduObj)`; PDU objects are pure data. + +## Tasks + +### 1. Definition tables — `src/defs/` + +- [x] `constants.ts` — `consts` + `constsById`. +- [ ] `errors.ts` — all `ESME_*` codes plus `errorsById`. Port from 0.4.0 `lib/defs.js`; note that + 0.4.0 has a typo, `ESME_RINVBCASTCHANIND = 0x011` (three digits), which should be `0x0111`. + Verify every code against the SMPP 3.4/5.0 spec while porting. +- [ ] `types.ts` — wire types `int8`, `int16`, `int32`, `string`, `cstring`, `buffer`, + `dest_address_array`, `unsuccess_sme_array`, and the `tlv` variants. Each is + `{ default, read(buffer, offset, length?), size(value), write(value, buffer, offset) }`. + `read` must be bounds-checked and return a result rather than throwing. +- [ ] `encodings.ts` — GSM 03.38 (`ASCII`), `LATIN1`, `UCS2`, `FLASH` (alias of ASCII) and `detect`. + Drop `iconv-lite`: LATIN1 is `Buffer.from(str, 'latin1')`, UCS2 is `Buffer.from(str, + 'utf16le').swap16()` (copy before swapping, and reject odd-length input on decode). +- [ ] `filters.ts` — `time`, `message`, `billing_identification`, `broadcast_area_identifier`, + `broadcast_content_type`, `broadcast_frequency_interval`, `callback_num`, `callback_num_atag`. +- [ ] `tlvs.ts` — the 67 TLV definitions plus `tlvsById` and the two aliases + (`alert_on_msg_delivery`, `failed_broadcast_area_identifier`). +- [ ] `commands.ts` — all 33 commands with ids and **wire-ordered** parameter lists, plus `cmdsById`. + +### 2. Message helpers — `src/message.ts` + +- [ ] `bitCount(msg, encoding?)`, `encodeMessage`, `decodeMessage`, `smppDate`, `splitMessage`. +- [ ] **Fix:** segments are 134 GSM characters or 67 UCS2 characters, so that segment + 6-byte UDH + is exactly 140 octets. 0.4.0 produces 152/66. +- [ ] **Fix:** `smppDate` must add 1 to `getMonth()` and zero-pad correctly. +- [ ] **Fix:** LATIN1 must actually decode — resolve `data_coding` to a concrete encoding, not to + whichever alias happens to sort last. +- [ ] The concatenation reference counter is per session, not module-global. `splitMessage` therefore + takes the reference as an argument instead of owning a counter. + +### 3. PDU codec — `src/pdu.ts` + +- [ ] `pduToObj(buffer)` → `{ err?, pduObj? }`, `objToPdu(obj)` → `{ err?, buffer? }`, + `pduReturn(pdu, status?, params?, tlvs?)` → `{ err?, buffer? }`, `isResp(pduObj)`. +- [ ] Keep the trailing-NULL-octet retry for `short_message` that 0.4.0 has — real peers send it. +- [ ] Guard `cmdLength` against a maximum before allocating, so a hostile peer cannot ask for a 4 GiB + buffer. 0.4.0 has no such guard. +- [ ] Per-command typed params: `pduToObj` returns a union discriminated on `cmdName`, and + `objToPdu` narrows `params` to the named command's fields. + +### 4. Session — `src/session.ts` + +- [ ] Framing off the socket. Replace 0.4.0's `Buffer.concat` per chunk with an accumulating reader — + concatenating the whole queue on every `data` event is quadratic. +- [ ] Sequence numbers, wrapping at 2147483646. +- [ ] `send()` with `responseTimeout`, `maxOutstanding` windowing and a queue, and `AbortSignal`. + Listeners must be removed on every exit path — timeout, abort and close included. +- [ ] `sendReturn()`, `unbind()`, `close()`. +- [ ] `submit_sm` handling, including long-SMS reassembly with `maxReassembly` and a real + `reassemblyTimeout` timer. +- [ ] `deliver_sm` handling: prefer the TLVs, fall back to parsing the receipt text + (`id:… sub:001 dlvrd:1 submit date:… done date:… stat:DELIVRD err:0 text:…`). +- [ ] Per-segment `dlr` plus merged `messageDlr`. +- [ ] `enquire_link` and `unbind` handling. + +### 5. Sms handle — `src/sms.ts` + +- [ ] `sendResp(status?)` and `sendDlr(status?)` returning result DTOs. +- [ ] Generated `message_id` values are UUID v7. +- [ ] **Fix:** `sendDlr` emits the 7-character spec status (`UNDELIV`, not `UNDELIVERABLE`). + +### 6. Client and server — `src/client.ts`, `src/server.ts` + +- [ ] `client()` — bind by `bindType`, all bind fields optional with 0.4.0's defaults. +- [ ] **Fix:** real TLS via `tls.connect()` / `tls.createServer()`, accepting an options object. +- [ ] Opt-in reconnect with exponential backoff between `minDelay` and `maxDelay`, re-binding and + emitting `reconnected`. Decide and document what happens to in-flight sends across a reconnect. +- [ ] `server()` — resolves once when listening, exposes `port`, `close()` and the `session` event. +- [ ] `authenticate` hook; `userData` attached to the session. +- [ ] `idleTimeout` drops silent peers. + +### 7. Public surface — `src/index.ts` + +- [ ] Named exports only, matching "The agreed API" above exactly. + +### 8. Tests + +Port the 0.4.0 suite (`test/01_encodings.js` … `test/04_session.js` on `master`) and extend it. Every +defect in the AGENTS.md table needs a test that fails against 0.4.0 behaviour. + +- [ ] Encodings, wire types, PDU round-trips, return PDUs, message sizing and splitting. +- [ ] Sessions: bind, auth success and failure, simple SMS, long SMS, DLRs, and the Kannel capture + with the large UDH from `test/04_session.js`. +- [ ] **Interop:** add the reference `smpp` package (farhadi/node-smpp) as a dev dependency and assert + both directions — our encoder against its parser, its encoder against our parser. This is how + the wire-format fixes are validated; the maintainer asked specifically for proof that the + corrected framing is right rather than differently wrong. +- [ ] Reassembly bounds, response timeouts, abort, and the send window. + +### 9. CI and release + +- [ ] `.github/workflows/test.yaml` — lint, typecheck and test on Node 18, 20, 22 and 24. The 18/20 + legs need the tests compiled first, since type stripping needs Node 22.18+. +- [ ] `.github/workflows/release.yaml` — `npm publish --provenance` on a `v*` tag. +- [ ] `renovate.json`. +- [ ] Delete nothing from `master`; this branch simply does not carry `.travis.yml`. + +### 10. Release chores + +- [ ] Fill in README usage docs as each part lands — right now the README documents the target API, + and it must not claim anything that is not implemented and tested. +- [ ] `npm deprecate larvitsmpp` pointing at `@larvit/smpp` once 1.0.0 is published. Maintainer's + call to run it; not something CI should do. + +## Open questions + +None outstanding. Everything above was settled with the maintainer; anything genuinely new that comes +up during implementation should be asked rather than assumed. diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..1797c71 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f4d9a10 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "declaration": true, + "declarationMap": true, + "erasableSyntaxOnly": true, + "exactOptionalPropertyTypes": true, + "isolatedModules": true, + "lib": ["ES2023"], + "module": "nodenext", + "moduleResolution": "nodenext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "rewriteRelativeImportExtensions": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2023", + "types": ["node"], + "verbatimModuleSyntax": true + }, + "include": ["eslint.config.ts", "src", "test"] +}