Scaffold ESM TypeScript rewrite as @larvit/smpp
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.claude
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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],
|
||||||
|
},
|
||||||
|
);
|
||||||
Generated
+1281
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, Record<number, string>> = {};
|
||||||
|
|
||||||
|
for (const [groupName, group] of Object.entries(consts)) {
|
||||||
|
const byId: Record<number, string> = {};
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(group)) {
|
||||||
|
byId[value] = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
constsById[groupName] = byId;
|
||||||
|
}
|
||||||
@@ -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<T> =
|
||||||
|
| ({ err: Error } & Partial<Record<keyof T, undefined>>)
|
||||||
|
| ({ err?: undefined } & T);
|
||||||
|
|
||||||
|
export type VoidResult = { err?: Error };
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user