commit 16c935bb185bdd9e23e4bd336ade0ea68f5f9c50 Author: Lilleman auf Larv Date: Tue Aug 25 16:02:27 2026 +0200 Scaffold ESM TypeScript rewrite as @larvit/smpp 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"] +}